zshの補完をサクッと作れる便利プラグインzeno.zshの使い方
dotfilesを育てるにつれ、zshの「短縮入力」「独自のfzf補完」が多くなって管理が大変になりました。これを解決したくて、vim-jpやXのタイムラインでよく見かけるzeno.zshを使ってみました。便利だったので「使い方」「筆者のカスタマイズ」「トラブルシューティング」を解説します。
zeno.zshとは?
zeno.zshはコマンド入力が爆速になるZsh・Fishのプラグインです。機能は次のとおり。
略語展開(Abbrev snippet)
略語展開は「ある文字を入力したら特定の文字列として展開する」機能です。
「DC<Space>と入力したらdocker-composeに変換する」みたいなことができます。
DC<Space>これがこうなる。
docker-composeちなみにSpaceじゃなくてEnterを押すと、展開されてからすぐに実行されます。
筆者は以前vimのiabのよう略語展開の記事を参考に略語展開を実装していたのですが、zenoを使うと細かく条件が指定できて好きです。
ファジー補完
補完機能があります。次の動画は、ビルトインされているGitコマンドの補完です。git addの後に<C-i>の入力で補完を起動しています。
ビルトインの補完だけではなく、カスタマイズで追加可能です(後でたっぷり解説)。
リッチな履歴UI
履歴から選択したコマンドを現在のコマンドラインに挿入できる機能があります。

fzf純正の履歴補完もありますが、zenoの方はプレビュー付きです。長いコマンドでも見やすいです。
リポジトリ管理のghqと連携
リポジトリ管理CLIghqで管理しているリポジトリに飛べる機能もあります。

zenoの導入方法
インストール
zeno自体はREADMEに従ってインストールします。
たとえばOh My Zshのプラグインとしては対応していないため、適当な場所にクローンして読み込みます。
ここでは実際に筆者がインストールした手順を紹介します。
筆者のdotfilesのインストールスクリプトで、他のプラグインと同じように~/.oh-my-zsh/custom/plugins/zenoにダウンロードしました。
ZSH_CUSTOM=$HOME/.oh-my-zsh/customif [[ ! -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内で読み込みます。
source $HOME/.oh-my-zsh/custom/plugins/zeno/zeno.zsh.zshrc側の設定
プラグインの読み込み後、キーバインドを設定しましょう。
# ...# この行までにプラグインを読み込みんでおく
export ZENO_HOME=~/.config/zenoif [[ -n $ZENO_LOADED ]]; then # 推奨 bindkey " " zeno-auto-snippet bindkey '^m' zeno-auto-snippet-and-accept-line bindkey '^i' zeno-completion
bindkey '^x ' zeno-insert-space # zenoを発動させないでSpace bindkey '^x^m' accept-line # zenoを発動させないでaccept
# 入れると便利 bindkey '^r' zeno-history-selection bindkey '^x^f' zeno-ghq-cdfi^mはCtrl+mまたはEnterという意味です。
yamlの設定ファイルの書き方
設定ファイルの場所は$ZENO_HOME/config.ymlや~/.config/zeno/config.ymlです。詳しくはREADMEを見てください。
※ 拡張子は.ymlです。.yamlではありません。
略語展開の設定の書き方
略語展開の設定はsnippets(複数形)に書きます。
snippets: - name: "docker compose" keyword: "DC" snippet: "docker-compose"keywordに書いたものがsnippetに展開されます。nameは自分が分かりやすいやつにしておけばOKです。
カーソル位置を変える
{{foo_bar}}のようにプレースホルダーを書くと、展開後にその位置にカーソルが飛びます。
- name: "vhs for blog" keyword: "VHS" snippet: "vhs {{tape_path}} && mv ./*.mp4 $VITE_EXTERNAL_FOLDER"複数行に分割して書く
YAMLなので複数行に分けて書けます。長いコマンドが見やすくなりますね。展開時は1行になります。
- name: "tmux popup neovim" keyword: "TN" snippet: tmux popup -E -w 95% -h 95% -d '#{pane_current_path}' 'nvim -c "{{command_here)}}"'正規表現で展開条件を指定する
〇〇 | grepのようなパイプ処理部分を展開したい場合、context.lbufferを指定するとよいでしょう。
- name: "grep" keyword: "G" snippet: "| grep" context: lbuffer: '.+\s'これは「現在のカーソルの左側が.+\sに当てはまるなら」という条件指定です。
cat foo.md G<Space># ↓展開cat foo.md | grep# 展開されない例cat foo.mdG<Space>G<Space>実装を見たところ、他にはrbuffer、buffer、globalがあるっぽいです。
ファジー補完の設定の書き方
補完の設定はcompletions(複数形)に書きます。
次の例はkillでプロセスIDを補完する例です。1つずつ見ていきましょう。
completions: - name: kill pid patterns: - "^kill( .*)? $" sourceCommand: "LANG=C ps -ef | sed 1d" callback: "awk '{print $2}'"patterns(複数形)に当てはまる時にzeno-completionのキーバインドを入力したら発動します。
このときの補完候補の取得コマンドをsourceCommandに書きます。今回のケースではそのままだと1行目がヘッダーになるため、| sed 1dで取り除きます。
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列目)だけを「補完結果」として扱うため、callbackにawk '{print $2}'を指定します。callbackはなくてもよいです。
0 314 1 0 Tue03PM ?? 2:19.31 /usr/libexec/logdヘッダーを設定する
先ほどはsedを使ってヘッダーを削除しましたが、fzfのheader-linesというオプションを使うと「指定した行までをヘッダーとして扱い選択候補には入れない」という設定ができます。
よってこんな設定がよいでしょう。
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}'"筆者のカスタマイズ
ここからは筆者のカスタマイズを紹介します。
パイプライン系
まずは「他のコマンドと組み合わせて使う前提」のパイプライン系です。
snippets: - name: "cd there" keyword: "CD" snippet: "&& cd $_" context: lbuffer: '.+\s'
- name: "copy" keyword: "CC" snippet: "| pbcopy" context: lbuffer: '.+\s'
- name: "grep" keyword: "G" snippet: "| grep" context: lbuffer: '.+\s'
- name: "null" keyword: "NULL" snippet: ">/dev/null 2>&1" context: lbuffer: '.+\s'CDはよく使ってます。
mkdir foo CD# ↓ 展開mkdir foo && cd $_# fooを作成してfooに移動Dockerコマンドの補完
docker stop 〇〇などの補完を作成しています。
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 〇〇の補完も設定しました。pnpmやBunにも対応しています。
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側でキーバインドも設定しました。
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 zle accept-line}zle -N fzf_npm_scriptsbindkey "^Xn" fzf_npm_scriptsこれで、Ctrlx+nを入力すると、そのリポジトリに適したパッケージマネージャーでnpmのscriptsが実行できます。
実行まではやらずに、単にコマンドラインを書き換えたい場合は最後のzle accept-lineを消してください。
以前別の記事で独自のシェルスクリプトを書いてましたが、zenoを使うことでかなり見やすくなりました。
mise run 〇〇の補完
miseのタスクランナーの実行コマンドmin run 〇〇の補完も設定しました。mise runでも一覧は出ますが履歴に残りません。fzfの方が慣れたUIで選べて履歴にも残るため、zenoを採用しています。
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側でキーバインドを設定しました。
function mise_tasks() { BUFFER="mise run " zle end-of-line zle zeno-completion zle accept-line}
zle -N mise_tasksbindkey "^Xm" mise_tasksnpm runに引き続き、miseについても以前別の記事にて独自のシェルスクリプトを書いており、それが見やすく記述できました。
履歴選択をtmux対応させる
履歴選択のzeno-history-selectionをtmux対応させたかったので次のように設定しました。
function history_popup() { ZENO_ENABLE_FZF_TMUX=1 \ ZENO_FZF_TMUX_OPTIONS="--tmux 80%" \ zeno-history-selection}
zle -N history_popupbindkey '^r' history_popupedit-command-lineとの組み合わせ技
このブログでは何度か紹介していますが、ウィジェットedit-command-lineを組み合わせると便利です。
こいつを使うと、現在の入力内容をテキストエディタで編集できます。展開後にちょっと修正したいな、という時に便利です。
autoload -Uz edit-command-linezle -N edit-command-linebindkey "^O" edit-command-line環境変数EDITORでテキストエディタを指定できます。
公式ドキュメント:26.7.1 Widgets
エイリアスとの使い分け
前述のとおり、zenoの略語展開はキーワードEnterですぐに実行できるため、zshのエイリアスと同じように扱えます。では、エイリアスはもう不要でしょうか?
エイリアスと比較したときのzenoの特徴は次のとおりです。
- 「Enterならすぐに実行」「Spaceなら展開」と選べる
- コマンド履歴には展開された状態で残る
登録したコマンドの引数部分などを変更してから実行する可能性があるならzenoを使いましょう。修正する可能性がなくてコマンド履歴をスッキリさせたいならエイリアスというように使い分けられます。
たとえばsedやawkのようなOSによって微妙に違うコマンドを統一したいケースではエイリアスのほうがよいでしょう。
case ${OSTYPE} in darwin*) alias xargs="gxargs" alias sed="gsed" alias awk="gawk" ;;esac略語の思想
筆者は今までの癖で略語をDCのように大文字にしてますが、別に小文字でも問題ありません。単純に既存のコマンド名と被らせたくないからやってます。
トラブルシューティング
遭遇しがちなトラブルと解決方法をまとめました。
動かない
動かない場合、まずはREADMEの設定例で動くかどうか確かめましょう。
読み込めているか
環境変数ZENO_LOADEDで、そもそもzenoが読み込まれているか確認します。1が返ってくれば読み込まれています。
echo $ZENO_LOADED1が返ってこない場合は.zshrcを見直してください。
zinitやfisherならプラグイン名をtypoしてないか- それ以外なら
zeno.zshファイルをsourceしているか
- それ以外なら
.zshrc変更後に再読み込みしたか?source ~/.zshrc- tmuxで新しいペインを開くとか
環境変数を設定してみる
筆者の環境だと環境変数ZENO_HOMEを設定しないと動きませんでした。
export ZENO_HOME=~/.config/zeno設定のtypo
特定の展開・補完だけ動かない場合、typoの可能性があります。snippets・completions・patternsなどは複数形なので要注意です。
シンタックス関連がおかしい
コマンドの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便利です。