理系学生日記

おまえはいつまで学生気分なのか

z.sh における precmd への多重登録問題とその対策について

naoya さんのエントリ見て z というユーティリティの存在を知った。

Tracks your most used directories, based on ’frecency’.

After a short learning phase, z will take you to the most ’frecent’
directory that matches ALL of the regexes given on the command line.

https://github.com/rupa/z

これを導入すると、よく使うディレクトリを自動で記憶しておいてくれるようになる。そして、導入時に合わせて提供されるようになる z コマンドを使うと、ディレクトリ名の一部を入力するだけで、記憶しておいたディレクトリから自動でリコメンドしてくれるようになって便利。
zsh使いなら効率改善のため知っておきたいAUTOJUMP - Glide Note にも書いてあるとおり、

コマンドライン作業の10〜20%はcdコマンドのため、ディレクトリ移動の動作が 改善すると必然的に作業効率も向上する

ということであって、それに導入の手間も今は brew install と .{zsh,bash}rc に僅かな設定を追加するだけなので、入れない手は無いとおもう(autojump でも良いけど)。


ところで、この z の「使うディレクトリを自動で記憶する」という仕組みについてなのだけれど、zsh を使用している場合は、ざっくり言うと次のような仕組みになっている。compctl は単に zsh かどうかの判別だと思うが、重要なのは _z_precmd という関数を準備しておいて、それを precmd_functions に突っ込むということをしているという点。
precmd_functions については man zshmisc(1) を参照して欲しいのだけれど、この配列に関数を突っ込んでおくと、「プロンプトを表示する前に」zsh がその関数(複数可)を実行してくれるようになる。つまり、以下の実装は、プロンプトが出る度に、自分がいるディレクトリ ($PWD) を _z で記録していく、ということを実施してるという意味になる。

elif compctl &> /dev/null; then
 [ "$_Z_NO_PROMPT_COMMAND" ] || {
  # populate directory list, avoid clobbering any other precmds
  if [ "$_Z_NO_RESOLVE_SYMLINKS" ]; then
    _z_precmd() {
      _z --add "${PWD:a}"
    }
  else
    _z_precmd() {
      _z --add "${PWD:A}"
    }
  fi
  precmd_functions+=(_z_precmd)
 }

ここで問題となるのは、precmd_functions への追加が「無条件」に行われるということ。
たとえば、次のように 2 度 .zshrc を読み込むと、同じ _z_precmd が 2 個 precmd_functions に登録されて、プロンプトを表示する度に二度 _z_precmd が実行されるということになる。

$ . ~/.zshrc
$ . ~/.zshrc

_z_precmd の中で実行される _z 関数は、ディレクトリの使用頻度なんかを計算したりする処理を実行することもあり、本来の使用頻度からは誤差を生じ得るし、たとえそうでなくとも、重複実行は避けた方が良い。
これを簡単に実現する方法があって、add-zsh-hook を使用して precmd_functions に登録するという方法になる。具体的なパッチは以下になる。

--- z.sh.orig   2013-01-19 20:26:00.000000000 +0900
+++ z.sh        2013-01-19 20:55:34.000000000 +0900
@@ -187,6 +187,9 @@
  }
 elif compctl &> /dev/null; then
  [ "$_Z_NO_PROMPT_COMMAND" ] || {
+
+  autoload -Uz add-zsh-hook
+
   # populate directory list, avoid clobbering any other precmds
   if [ "$_Z_NO_RESOLVE_SYMLINKS" ]; then
     _z_precmd() {
@@ -197,7 +200,7 @@
       _z --add "${PWD:A}"
     }
   fi
-  precmd_functions+=(_z_precmd)
+  add-zsh-hook precmd _z_precmd
  }
  # zsh tab completion
  _z_zsh_tab_completion() {

これで _z の多重起動は防止できる。