zshの補完をサクッと作れる便利プラグインzeno.zshの使い方

dotfilesを育てるにつれ、zshの「短縮入力」「独自のfzf補完」が多くなって管理が大変になりました。これを解決したくて、vim-jpやXのタイムラインでよく見かけるzeno.zshを使ってみました。便利だったので「使い方」「筆者のカスタマイズ」「トラブルシューティング」を解説します。

zeno.zshとは?

zeno.zshはコマンド入力が爆速になるZsh・Fishのプラグインです。機能は次のとおり。

略語展開・スニペット(Abbrev snippet)

略語展開は「ある文字を入力したら特定の文字列として展開する」機能です。

DC<Space>と入力したらdocker-composeに変換する」みたいなことができます。

Terminal window
DC<Space>

これがこうなります。

Terminal window
docker-compose

ちなみにSpaceじゃなくてEnterを押すと、展開されてからすぐに実行されます。

ファジー補完

補完機能があります。次の動画は、ビルトインされているGitコマンドの補完です。git addの後に<C-i>の入力で補完を起動しています。

ビルトインの補完だけではなく、カスタマイズで追加可能です(後でたっぷり解説)。

リッチな履歴UI

履歴から選択したコマンドを現在のコマンドラインに挿入できる機能があります。

zeno-history-selectionの実行例

fzf純正の履歴補完もありますが、zenoの方はプレビュー付きです。長いコマンドでも見やすいです。

リポジトリ管理のghqと連携

リポジトリ管理CLIghqで管理しているリポジトリに飛べる機能もあります。

zeno-ghq-cdの実行例

zenoの導入方法

zenoはDenofzfが必要です。

インストール

zeno自体はREADMEに従ってインストールします。
たとえばOh My Zshのプラグインとしては対応していないため、適当な場所にクローンして読み込みます。

ここでは実際に筆者がインストールした手順を紹介します。

筆者のdotfilesのインストールスクリプトで、他のプラグインと同じように~/.oh-my-zsh/custom/plugins/zenoにダウンロードしました。

インストールスクリプト
ZSH_CUSTOM=$HOME/.oh-my-zsh/custom
if [[ ! -d "$ZSH_CUSTOM/plugins/zeno" ]]; then
git clone https://github.com/yuki-yano/zeno.zsh.git "$ZSH_CUSTOM/plugins/zeno"
fi

上記でcloneしたリポジトリにあるzeno.zshファイルを.zshrc内で読み込みます。

.zshrc
source $HOME/.oh-my-zsh/custom/plugins/zeno/zeno.zsh

.zshrc側の設定

プラグインの読み込み後、キーバインドを設定しましょう。

.zshrc
# ...
# この行までにプラグインを読み込みんでおく
export ZENO_HOME=~/.config/zeno
if [[ -n $ZENO_LOADED ]]; then
# 推奨
bindkey " " zeno-auto-snippet
bindkey '^m' zeno-auto-snippet-and-accept-line
bindkey '^i' zeno-completion
bindkey '^x^p' zeno-insert-snippet
bindkey '^x ' zeno-insert-space # zenoを発動させないでSpace
bindkey '^x^m' accept-line # zenoを発動させないでaccept
# 入れると便利
bindkey '^r' zeno-history-selection
bindkey '^x^f' zeno-ghq-cd
fi

^mCtrl+mまたはEnterという意味です。

yamlの設定ファイルの書き方

設定ファイルの場所は$ZENO_HOME/config.yml~/.config/zeno/config.ymlです。詳しくはREADMEを見てください。

※ 拡張子は.ymlです。.yamlではありません。

略語展開・スニペットの設定の書き方

別の記事に切り出しました。

ファジー補完の設定の書き方

補完の設定はcompletions(複数形)に書きます。

次の例はkillでプロセスIDを補完する例です。1つずつ見ていきましょう。

config.yml
completions:
- name: kill pid
patterns:
- "^kill( .*)? $"
sourceCommand: "LANG=C ps -ef | sed 1d"
callback: "awk '{print $2}'"

patterns(複数形)に当てはまる時にzeno-completionのキーバインドを入力したら発動します。

このときの補完候補の取得コマンドをsourceCommandに書きます。今回のケースではそのままだと1行目がヘッダーになるため、| sed 1dで取り除きます。

ps -efの例
UID PID PPID C STIME TTY TIME CMD
0 1 0 0 Tue03PM ?? 5:55.79 /sbin/launchd
0 314 1 0 Tue03PM ?? 2:19.31 /usr/libexec/logd
0 315 1 0 Tue03PM ?? 0:17.26 /usr/libexec/smd
...

補完候補の文字列は次のようになります。

0 1 0 0 Tue03PM ?? 5:55.79 /sbin/launchd
0 314 1 0 Tue03PM ?? 2:19.31 /usr/libexec/logd
0 315 1 0 Tue03PM ?? 0:17.26 /usr/libexec/smd
...

最終的には選んだ行のPIDの部分(=2列目)だけを「補完結果」として扱うため、callbackawk '{print $2}'を指定します。callbackはなくてもよいです。

0 314 1 0 Tue03PM ?? 2:19.31 /usr/libexec/logd

ヘッダーを設定する

先ほどはsedを使ってヘッダーを削除しましたが、fzfのheader-linesというオプションを使うと「指定した行までをヘッダーとして扱い選択候補には入れない」という設定ができます。

よってこんな設定がよいでしょう。

config.yml
completions:
- name: kill pid
patterns:
- "^kill( .*)? $"
sourceCommand: "LANG=C ps -ef | sed 1d"
sourceCommand: "LANG=C ps -ef"
options:
--header-lines: 1
callback: "awk '{print $2}'"

筆者のカスタマイズ

ここからは筆者のカスタマイズを紹介します。

Dockerコマンドの補完

docker stop 〇〇などの補完を作成しています。

config.yml
completions:
- name: docker stop
patterns:
- "^docker stop $"
sourceCommand: "docker ps"
options:
--header-lines: 1
--tmux: "80%"
--prompt: "'Docker Stop> '"
--no-select-1: true
callback: "awk '{print $1}'"
- name: docker rm(コンテナ)
patterns:
- "^docker rm $"
sourceCommand: "docker ps -a"
options:
--header-lines: 1
--tmux: "80%"
--multi: true
--prompt: "'Docker Remove Container> '"
--no-select-1: true
callback: "awk '{print $1}'"
- name: docker rmi(イメージ)
patterns:
- "^docker rmi $"
sourceCommand: "docker images"
options:
--header-lines: 1
--tmux: "80%"
--multi: true
--prompt: "'Docker Remove Image> '"
--no-select-1: true
callback: "awk '{print $3}'"

optionsはfzfに渡すオプションです。たとえば--tmuxを使えばtmuxで開けます。

npm run 〇〇の補完

npm run 〇〇の補完も設定しました。pnpmBunにも対応しています。

config.yml
completions:
- name: npm scripts
patterns:
- "^(npm|pnpm|bun) run $"
sourceCommand:
jq -r '.scripts | to_entries | .[] | .key + " = " + .value' package.json
options:
--no-select-1: true
callback: "awk '{print $1}'"

ただ、これだけだとnpm run<C-i>と入力するのが面倒です。そこで、.zshrc側でキーバインドも設定しました。

.zshrc
function fzf_npm_scripts() {
if ! type jq > /dev/null; then
echo 'jq command is required'
zle send-break
return 1
fi
local prefix="npm"
local git_root=$(git rev-parse --show-superproject-working-tree --show-toplevel 2>/dev/null | head -1)
if [[ -f pnpm-lock.yaml || ( -n "$git_root" && -f "$git_root/pnpm-lock.yaml" ) ]]; then
prefix="pnpm"
elif [[ -f bun.lock || ( -n "$git_root" && -f "$git_root/bun.lock" ) ]]; then
prefix="bun"
fi
BUFFER="$prefix run "
zle end-of-line
zle zeno-completion # from zeno
if [[ "$BUFFER" != "$prefix run " ]]; then
zle accept-line
fi
}
zle -N fzf_npm_scripts
bindkey "^Xn" fzf_npm_scripts

これで、Ctrlx+nを入力すると、そのリポジトリに適したパッケージマネージャーでnpmのscriptsが実行できます。

実行まではやらずに、単にコマンドラインを書き換えたい場合は最後のzle accept-lineを消してください。

以前別の記事で独自のシェルスクリプトを書いてましたが、zenoを使うことでかなり見やすくなりました。

mise run 〇〇の補完

miseのタスクランナーの実行コマンドmin run 〇〇の補完も設定しました。
mise runでも一覧は出ますが履歴に残りません。fzfの方が慣れたUIで選べて履歴にも残るため、zenoを採用しています。

config.yml
completions:
- name: mise run
patterns:
- "^mise run $"
sourceCommand: "mise tasks --no-header"
options:
--no-select-1: true
callback: "awk '{print $1}'"

筆者はmise run<C-i>の入力が面倒であるため、こちらも.zshrc側でキーバインドを設定しました。

.zshrc
function mise_tasks() {
BUFFER="mise run "
zle end-of-line
zle zeno-completion
if [[ "$BUFFER" != "mise run " ]]; then
zle accept-line
fi
}
zle -N mise_tasks
bindkey "^Xm" mise_tasks

npm runに引き続き、miseについても以前別の記事にて独自のシェルスクリプトを書いており、それが見やすく記述できました。

履歴選択をtmux対応させる

履歴選択のzeno-history-selectionをtmux対応させたかったので次のように設定しました。

.zshrc
function history_popup() {
ZENO_ENABLE_FZF_TMUX=1 \
ZENO_FZF_TMUX_OPTIONS="--tmux 80%" \
zeno-history-selection
}
zle -N history_popup
bindkey '^r' history_popup

edit-command-lineとの組み合わせ技

このブログでは何度か紹介していますが、ウィジェットedit-command-lineを組み合わせると便利です。
こいつを使うと、現在の入力内容をテキストエディタで編集できます。展開後にちょっと修正したいな、という時に便利です。

.zshrc
autoload -Uz edit-command-line
zle -N edit-command-line
bindkey "^O" edit-command-line

環境変数EDITORでテキストエディタを指定できます。

公式ドキュメント:26.7.1 Widgets

トラブルシューティング

遭遇しがちなトラブルと解決方法をまとめました。

動かない

動かない場合、まずはREADMEの設定例で動くかどうか確かめましょう。

読み込めているか

環境変数ZENO_LOADEDで、そもそもzenoが読み込まれているか確認します。1が返ってくれば読み込まれています。

Terminal window
echo $ZENO_LOADED

1が返ってこない場合は.zshrcを見直してください。

  • zinitfisherならプラグイン名をtypoしてないか
    • それ以外ならzeno.zshファイルをsourceしているか
  • .zshrc変更後に再読み込みしたか?
    • source ~/.zshrc
    • tmuxで新しいペインを開くとか

環境変数を設定してみる

筆者の環境だと環境変数ZENO_HOMEを設定しないと動きませんでした。

.zshrc
export ZENO_HOME=~/.config/zeno

設定のtypo

特定の展開・補完だけ動かない場合、typoの可能性があります。snippetscompletionspatternsなどは複数形なので要注意です。

シンタックス関連がおかしい

コマンドのTab補完時に次のようなエラーが出ました。

_zsh_highlight:69: bad set of key/value pairs for associative array

どうやらzsh-syntax-highlightingと一緒に使うと出るようです(参考:Issue #57)。公式で推奨されているfast-syntax-highlightingの方を使いましょう。


以上、zeno.zshの紹介でした。久しぶりにdotfiles盆栽ができました。fzfの補完って自分で書くとそれなりに長くて読みづらかったり、コマンド名やキーバインドを覚えるのが面倒でした。これが解消されたのでzeno便利です。

スニペット機能の方は別の記事に書いてます。ますので併せてどうぞ。