shell.nixとdirenvでプロジェクトごとに補完をいい感じにする

概要

HERPでは shell.nix をgit repositoryのrootに置いて依存関係を解決しているが、PATH がprojectごとに設定されるだけなので 例えば kubectl の補完が有効になるわけではない。

その課題を解決するために shell.nixzshの補完もうまく済ませるワザップを今回は書く。

zsh completion について

zsh: 20 Completion System

zsh の completionの仕組みをざっくり書くと、 FPATHをいい感じにして compinit すれば FPATHから解決されるcompletion用のscriptが実行され補完される

手法

FPATH用のderivation

まず、completionを集めたderivationを作る。

buildEnv というderivationの特定のディレクトリの集合体を作る関数があるのでそれを使う

FPATH = "${nixpkgs.buildEnv {
     name = "zsh-comp";
     paths = buildInputs;
     pathsToLink = "/share/zsh";
   }}/share/zsh/site-functions";

FPATHの設定

上記で作った FPATH$HOME/.zshrc で追加された FPATH に足せばいいのだが

安易な方法として shell.nix にFPATHを足してしまうと、そのまま上書きされて既存の補完の全てを失ってしまう

なので、 project_FPATH のような環境変数に一時的に保存する。

このまま nix-shell --run zsh のように実行する場合は, ZDOTDIR をshell.nix中で上書きするなどのコードをを足せば問題ないのだが、 いちいち実行するのが面倒なので direnvを使っている場合は工夫が必要になる。

use nix
path_add FPATH $project_FPATH

以下の理由でこのコードは動きそうに見えて動かない(人によっては動く)

direnvは .envrc の評価をbash上で行っており、評価した結果の差分を既存のshellに足すような実装になっている。(コレは echo $0 してあげるとわかる)

実はzshFPATH は exportされていない変数なので (コレは echo ${(t)FPATH} でわかる、コレをlocal変数と言っていいのかはわからない) bash上で評価した際に存在していない変数として扱われFPATHが上書きされる

なので direnvの評価が始まる前、つまり ~/.zshrc などで export FPATH (typeset でも可) しておく必要がある

export FPATH してない人間向け対応

世の中には export FPATH していない人間もいるので、そう言った人が projectに入って direnv allow した瞬間に壊れて cd するのも困るのは不親切という考え方もあるので対応をする。(考え方によっては学習機会を奪っている。人間は困らないと進化しないので)

上記で書いたように 評価がbash上で行っているので export -p した中に FPATH が含まれていなければ export FPATH されていないことがわかる

よって以下のようになる

use nix

if export -p|grep "declare -x FPATH" ; then
  path_add FPATH $project_FPATH
fi

compinitの設定

あとは compinit するだけなのだが毎回 compinit していてはダルい(ダルい)

なので direnv がhookで評価した後にFPATHが書き変わっていれば compinit するようなhookを作ってあげれば良い

export COMPINIT_DIFF=""
_chpwd_compinit() {
  if [ -n "$IN_NIX_SHELL" -a "$COMPINIT_DIFF" != "$DIRENV_DIFF" ]; then
    compinit -u
    COMPINIT_DIFF="$DIRENV_DIFF"
    echo "compinited !"
  fi
}
if [[ -z ''${precmd_functions[(r)_chpwd_compinit]} ]]; then
  precmd_functions=( ''${precmd_functions[@]} _chpwd_compinit )
fi
if [[ -z ''${chpwd_functions[(r)_chpwd_compinit]} ]]; then
  chpwd_functions=( ''${chpwd_functions[@]} _chpwd_compinit )
fi

FPATH の diffを取ると言ったが色々考えた結果,nix-shell中でdirenv が実行されたときにcompinitされればいいので上記のようにした。(compinit以外のこともしたくなるかもしれない)

蛇足として一応書いておくが autoload compinit は他でするように

終わりに

nix-shelldirenv の組み合わせは環境を揃える手法として有名だが、実は補完まで揃えることができる。 各個人のzshrcに追記する必要はあるがオススメできる手法である。

余談

herp.careers

HERPのSREチームはnixを使って各種ツールの環境を揃えている。