概要
zshの補完やスニペット展開をfzfベースで強化するzeno.zshを導入した。ついでに、.zshrcに散在していたエイリアスをzenoのスニペット機能に移行した。そのまとめ。
clと入力してスペースを押すと、

ターミナルの入力行がclaude --dangerously-skip-permissionsに展開される。

エイリアスの問題
シェルのエイリアスは便利だが、既存のコマンドと名前が衝突する問題がある。
例えばclaude --dangerously-skip-permissionsを短く打ちたくてalias cc='claude --dangerously-skip-permissions'としたとする。ところがccはC言語のコンパイラとして既に存在するコマンドで、このエイリアスを設定した瞬間にccコマンドが壊れる。
今は問題なくても、将来的に衝突するかもしれない。短いキーワードほど衝突の可能性は高い。エイリアスはシェル全体に影響するので、衝突するとスクリプトやビルドツールが壊れたりする。
zeno.zshのスニペット機能はこの問題を根本的に解決する。zenoのスニペットはターミナルの入力行でキーワードを展開するだけで、シェルのコマンド名前空間には一切影響しない。clと入力してスペースを押すと、ターミナルの入力行がclaude --dangerously-skip-permissionsに変わる。それだけ。clというコマンドが上書きされるわけではない。
zeno.zshとは
zeno.zshはfzfベースでzshの補完、スニペット展開、履歴検索を強化するプラグイン。Denoで動いている。
自分の環境はOh My Zshを使っているので、カスタムプラグインとしてインストールした。
インストール手順
前提として、denoとfzfがインストール済みであること。
# Oh My Zshカスタムプラグインとしてクローン
git clone https://github.com/yuki-yano/zeno.zsh.git \
${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zenodenoが見つからずサイレントに何もしない
zeno.zshはプラグインの読み込み時にwhence -p denoでdenoの存在チェックをしていて、見つからないとサイレントに何もせず終了する。エラーも出ない。
自分の環境では、denoは~/.deno/envでPATHに追加されるのだが、これが.zshrc.local(oh-my-zshの後に読まれるファイル)に書いてあった。つまりプラグイン読み込み時点ではdenoがPATHに存在しない。
.zshrcの先頭、oh-my-zshのsourceより前にdenoのenv読み込みを移動して解決した。
# .zshrcの先頭(oh-my-zshより前に読み込む)
[ -f "$HOME/.deno/env" ] && . "$HOME/.deno/env"macOSでスニペットが展開されない: ZENO_HOMEの設定
zeno.zshの設定ファイルは~/.config/zeno/config.ymlに置く。Linux/WSLではこれが$XDG_CONFIG_HOMEのデフォルト値(~/.config)と一致するので何もしなくてもzenoが設定を見つける。
macOSの場合、$XDG_CONFIG_HOMEが未設定だと、XDGライブラリのデフォルトが~/.configにならない。zenoのソースコードを確認すると、設定ファイルの探索順は以下の通り。
$ZENO_HOME/config.yml$XDG_CONFIG_HOME/zeno/config.yml$XDG_CONFIG_DIRS内のzeno/config.yml- XDGライブラリのデフォルトディレクトリ
macOSでは1も2も該当せず、4のデフォルトが~/Library/Application Support等になるため、~/.config/zeno/config.ymlに設定を置いてもzenoが見つけられない。
$ZENO_LOADEDが1なのにスニペットが展開されないという症状になる。zenoのプラグイン自体は正常にロードされているが、設定ファイルが読めていないのでスニペットが空のまま動いている状態。
解決方法は、.zshrcのプラグイン読み込みより前にZENO_HOMEを設定すること。
export ZENO_HOME="$HOME/.config/zeno"
plugins=(git zeno).zshrcの設定
# Deno(oh-my-zshより前に読み込む必要あり)
[ -f "$HOME/.deno/env" ] && . "$HOME/.deno/env"
# ...
export ZENO_HOME="$HOME/.config/zeno"
plugins=(git zeno)
source $ZSH/oh-my-zsh.sh
# zeno.zsh keybindings
if [[ -n $ZENO_LOADED ]]; then
bindkey ' ' zeno-auto-snippet
bindkey '^m' zeno-auto-snippet-and-accept-line
bindkey '^i' zeno-completion
bindkey '^r' zeno-history-selection
bindkey '^x^s' zeno-insert-snippet
fi$ZENO_LOADEDはzeno.zshが正常にロードされた場合にセットされる環境変数。これで、zenoが読み込まれていないときにキーバインドが壊れるのを防いでいる。
設定ファイルのシンボリックリンク管理
zeno.zshの設定ファイルは~/.config/zeno/config.ymlに置く。この設定ファイルをGitリポジトリで管理するために、シンボリックリンクを使っている。
dotfiles管理用のリポジトリにzeno/config.ymlを置いて、~/.config/zenoへシンボリックリンクを張る方式。
ln -s /path/to/dotconfigetc/zeno ~/.config/zenoこうするとGitリポジトリ側で設定を編集すれば、そのまま反映される。
エイリアスからスニペットへの移行
zeno.zshが動くようになったので、.zshrcに散在していたエイリアスをzenoのスニペット設定に移行した。
移行前: .zshrcのエイリアス
alias killds='find ./ -name ".DS_Store" -depth -exec rm {} \;'
alias cpwd='pwd | pbcopy'
alias ccb='git branch --show-current | pbcopy'
alias vpr='gh pr view --web'
alias fpush='git push --force-with-lease'
alias cl='claude --dangerously-skip-permissions'移行後: ~/.config/zeno/config.yml
snippets:
- name: kill .DS_Store
keyword: killds
snippet: find ./ -name ".DS_Store" -depth -exec rm {} \;
- name: copy pwd
keyword: cpwd
snippet: pwd | pbcopy
- name: copy current branch
keyword: ccb
snippet: git branch --show-current | pbcopy
- name: view pr in browser
keyword: vpr
snippet: gh pr view --web
- name: force push with lease
keyword: fpush
snippet: git push --force-with-lease
- name: claude skip permissions
keyword: cl
snippet: claude --dangerously-skip-permissionsエイリアスとスニペットの違い
aliasの場合、キーワードを入力してEnterを押すと、そのまま展開後のコマンドが実行される。展開後の内容は見えない。
zenoスニペットの場合、キーワードを入力してスペースを押すと、キーワードが展開されてコマンドライン上に表示される。実行前にコマンド全体が見える。久しぶりに使うコマンドで「これ何だっけ」とならずに済む。fpushを打ったらgit push --force-with-leaseが表示されて、そのまま実行するかどうかを判断できる。
oh-my-zshのgitプラグインを外した
この移行作業の中で、oh-my-zshのgitプラグインが大量のgitエイリアス(gs、ga、gd等)を登録していることに気付いた。自分はこれらを全く使っていなかった。使っていないのにグローバルにエイリアスが登録されているのは邪魔だし、先述のエイリアスの衝突問題もある。gitプラグインを外した。
余談
エイリアスをスニペットに移行して良かったのは、実行前にコマンド全体が確認できるようになったこと。特にfpushのような、間違えたくないコマンドで展開後の内容が見えるのは安心感がある。
スニペット定義は単純なYAMLファイルにkeywordとsnippetを書くだけ。シンプルなものの方がうまくいくというのはよくある話で、今回もそうだった。