.zsh_historyファイルをイジる
tl;dr
- zshのコマンド履歴を保存するファイル
.zsh_history
をイジりたい - エスケープ処理が行われているので、雑にイジると壊れる
- イジっても壊れないようにするためのCLIとNeovim/Vim Pluginを作った
.zsh_history
ファイルとは
.zsh_history
は、ご存じ zsh のコマンド履歴を保存するファイルです。
コマンドを実行した履歴が .zsh_history
に保存されます。
$ echo "foo"
$ cat ~/.zsh_history
...
: 1745283180:0;echo "foo"
zshのコマンド履歴は、様々な方法で呼び出して再利用できるのも周知のことでしょう。
-
Ctrl + p
やCtrl + n
での履歴の前後移動 -
history
コマンドやそのエイリアス先であるfc
コマンドでの参照・再利用 -
!!
や!n
といったヒストリ展開 -
Ctrl + r
でのインクリメンタルサーチ
などなど。
これらの機能は、zshが .zsh_history
を利用して実現されています。
historyをイジる
コマンド履歴を利用する方法は色々あるのですが、ときに
- 秘匿情報が残ったので消したい
- 再利用性の高いコマンドに書き換えたい
など、これを加工したいと考えることがあります。
そこで~/.zsh_history
をエディタで開くと、文字化けのような[1]内容に悩まされることがあるかもしれません。
: 1745283180:0;echo "foo"
: 1745283185:0;echo "ほぃ�" # echo "ほげ" を実行したはずなのに…
これを、雑にエディタで加工・保存すると、時に履歴が壊れてしまうことがあります。
$ echo "ほぃ # 過去に実行した`echo "ほげ"`を呼び出したいのに、履歴が壊れて正しく呼び出せなくなってしまう
なぜ壊れるのか
.zsh_history
は、zshが独自に定義したフォーマットで保存されています。
man zshoptions
のEXTENDED_HISTORY
には以下のように説明されています。
EXTENDED_HISTORY <C>
Save each command's beginning timestamp (in seconds since the epoch) and the duration (in seconds) to the history file. The format
of this prefixed data is:`: <beginning time>:<elapsed seconds>;<command>`.
先の節の例を見ると、
-
: 1745283180:0;
の部分は、コマンドを実行時刻(unix epoc)と、コマンドの実行時間(seconds) -
echo "foo"
の部分は、実行したコマンド
になっているのが分かります。
そして、この時 コマンドの内容は、メタ文字を含む場合、エスケープされて保存されています。 [2]
このエスケープ処理が、zshの独自フォーマットで行われているため、雑にエディタで加工・保存すると履歴が壊れる原因になりえるのです。[3]
どう回避するか
端的に言えば、.zsh_hisotry
ファイルをイジる場合には、これらの特殊なエスケープ処理を回避する必要があります。
- 編集前にエスケープ処理を除去し
- 編集し
- 編集後にエスケープ処理を施す
エスケープ処理の内容
どのようなエスケープ処理が行われているかは、マニュアル上では見つけられませんでしたが、
zshのソースコードにはそれらしいものが見つかります。[2:1]
metafy
/unmetafy
という関数で、それぞれエスケープ/除去相当の処理をしているようです。
metafy
: https://github.com/zsh-users/zsh/blob/dd21cda278a64d1949c284282d7305ea22564052/Src/utils.c#L4820-L4905
unmetafy
: https://github.com/zsh-users/zsh/blob/dd21cda278a64d1949c284282d7305ea22564052/Src/utils.c#L4930-L4953
メモリ確保の処理やポインタ演算やマクロが多くちょっと読みにくいですが、要は
-
metafy
は、0x00
または0x83-0xa2
は0x20
とxorして0x83
を前置する -
unmetafy
は、0x83
を見つけたらこれを除去して続く1バイトを0x20
とxorする
という処理になっています(当然ですが、両者は逆の処理になっています)。
解決策を作った
このmetafy/unmetafyを処理するものを作って、安全に編集できるようにしました。[4]
- CLI (Go): zshist
- Vim/Neovim Plugin (via Denops): denops-zshist.vim
CLIは zshist encode
でエスケープ処理を施し、 zshist decode
でエスケープ処理を除去することができます。
Vim/Neovim Pluginは、.zsh_history
を開いたときに自動でエスケープ処理を除去し、保存時にエスケープ処理を施すようにしています。
補足1: ヒストリの書き換え→再利用
実のところ、再利用時に実行するコマンドを書き換えるだけであればfc
コマンドで置換をかけたり編集したりといった方法で解決できるので、
永続的に書き換えたいと思うシーンは多くないかもしれません。
$ echo "foo"
foo
$ fc -s foo=bar
echo "bar"
bar
$ fc -l
1 echo "foo"
2 echo "bar"
man zshbuiltins
には、fc
コマンドの詳細が書かれています。
EXTENDED_HISTORY
オプション
補足2: 引用したEXTENDED_HISTORY
はオプションになっているので、これを有効にしていない場合、コマンドの実行時刻や実行時間は保存されません。
$ unsetopt EXTENDED_HISTORY
$ echo "foo"
$ cat ~/.zsh_history
echo "foo"
今回作ったものは、EXTENDED_HISTORY
オプションが有効な場合にのみ対応しています。
今のzshはデフォルトで有効になっているので、特に意識する必要はないかもしれません。
Discussion