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
筆者のカスタマイズ
ここからは筆者のカスタマイズを紹介します。
パイプライン系
まずは「他のコマンドと組み合わせて使う前提」のパイプライン系です。
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 | sed 1d" options: --tmux: "80%" --prompt: "'Docker Stop> '" --no-select-1: true callback: "awk '{print $1}'"
- name: docker rm(コンテナ) patterns: - "^docker rm $" sourceCommand: "docker ps -a | sed 1d" options: --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 | sed 1d" options: --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_tasks
npm 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_popup
edit-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_LOADED
1
が返ってこない場合は.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便利です。