この3連休、夢中でコーディングしてあるツールを作っている最中、.zsh_historyの特異な挙動を発見しましたので書き残しておきます。
一体何が起こったのか?
Goでツールを作っていたのですが、.zsh_historyを読み込んでファイル内容を出力したところ以下のようになりました。
| $ go run main.go | |
| vim pre-commit.sample | |
| cp pre-commit.sample pre-commit | |
| vim .git/hooks/pre-commit | |
| vim pre-commit | |
| git commit --allow-empty -m "SideCIぃ�胰�ぃ�ぃ�ぃ�め僦��PR" |
思いっきり文字化けしている・・・
見るに、日本語が文字化けしている様子。
で、あれー?と思ったので.zsh_historyをvimで開いてみました。
| git commit --allow-empty -m "SideCIã<81><83>¬è<83>°½ã<81><83><81>ã<81><83>¿ã<81><83>¿ã<82><81>å<83>¦<83>PR" |
???🤔
.zsh_historyの特異な性質を知る
さて、なんでこのように日本語が記述されていないのでしょうか?
調べたところ以下のブログ記事に辿り着きました。
ここに興味深い文言が。
zshのヒストリファイルは、保存するデータの各バイトにおいて、それが 0x80-0x9d または 0xa0 のとき、その前に 0x83 を入れて、続くバイトの 6bit 目を反転させるようになっている。
ほほう?🤔
これがどういうことか、具体的な例を使って見ていきましょう。
例として「あいうえお」という文字列を使います。
この「あいうえお」を.zsh_historyで見ると以下のようになります。
ã<81><82>ã<81><83>¤ã<81><83>¦ã<81><83>¨ã<81><83>ª
.zsh_historyはlatin1(ISO/IEC 8859-1)という形式になりますので、その文字コード表を元にすると以下のように16進数に直すことが出来ます。
(分かりやすいように適宜スペースを挿入しています)
E38182 E38183 A4 E38183 A6 E38183 A8 E38183 AA
ここで、 0x80-0x9d または 0xa0 のとき、その前に 0x83 を入れて というのを考えると、上記の 83 に当たる所が該当します。
今回だと、該当するのは A4 , A6 , A8 , AA ですね。
なので、 続くバイトの 6bit 目を反転させるようになっている というルールに従って、6bit目を更に反転させると、
84 , 86 , 88 , 8A になります。
で、このルールに従って書き直すと以下のようになります。
E38182 E38183 84 E38183 86 E38183 88 E38183 8A
そして、更にマーキングに使っている 83 を削除すると
E38182 E38184 E38186 E38188 E3818A
といった形になります。
これを、Unicodeの文字コード表に照らし合わせると、あら不思議。
あいうえお
と読み解くことが出来ます。
Goだとどうする?
さて、ここまで理解した上で、Goだと以下のようにParse処理を書くことが出来ます。
| func parseNonAscii(latin1Byte []byte) string { | |
| isMarking := false | |
| var byteBuffer []byte | |
| for _, codePoint := range latin1Byte { | |
| // 131は0x83の10進数表現 | |
| if codePoint == 131 { | |
| isMarking = true | |
| continue | |
| } | |
| if isMarking { | |
| // 6bit目を反転させるために | |
| // 0x20をXORする | |
| invertCodePoint := codePoint ^ 32 | |
| byteBuffer = append(byteBuffer, invertCodePoint) | |
| isMarking = false | |
| } else { | |
| byteBuffer = append(byteBuffer, codePoint) | |
| } | |
| } | |
| return string(byteBuffer) | |
| } |
これに scanner.Scan 等で取得してきた文字列を突っ込むことで、見事に日本語に変換してくれます。
.zsh_historyの特異な性質を知ることが出来、また一つ学びになりました。
何故にこのような処理を非ASCII文字に対して行っているのかは謎なところですが・・・🤔