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を押すと、展開されてからすぐに実行されます。

筆者は以前vimのiabのよう略語展開の記事を参考に略語展開を実装していたのですが、zenoを使うと細かく条件が指定できて好きです。

ファジー補完

補完機能があります。次の動画は、ビルトインされている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 ' 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ではありません。

略語展開の設定の書き方

略語展開の設定はsnippets(複数形)に書きます。

config.yml
snippets:
- name: "docker compose"
keyword: "DC"
snippet: "docker-compose"

keywordに書いたものがsnippetに展開されます。nameは自分が分かりやすいやつにしておけばOKです。

カーソル位置を変える

{{foo_bar}}のようにプレースホルダーを書くと、展開後にその位置にカーソルが飛びます。

config.yml
- name: "vhs for blog"
keyword: "VHS"
snippet: "vhs {{tape_path}} && mv ./*.mp4 $VITE_EXTERNAL_FOLDER"

複数行に分割して書く

YAMLなので複数行に分けて書けます。長いコマンドが見やすくなりますね。展開時は1行になります。

config.yml
- 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を指定するとよいでしょう。

config.yml
- name: "grep"
keyword: "G"
snippet: "| grep"
context:
lbuffer: '.+\s'

これは「現在のカーソルの左側が.+\sに当てはまるなら」という条件指定です。

Terminal window
cat foo.md G<Space>
# ↓展開
cat foo.md | grep
Terminal window
# 展開されない例
cat foo.mdG<Space>
G<Space>

実装を見たところ、他にはrbufferbufferglobalがあるっぽいです。

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

補完の設定は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

筆者のカスタマイズ

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

パイプライン系

まずは「他のコマンドと組み合わせて使う前提」のパイプライン系です。

config.yml
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はよく使ってます。

Terminal window
mkdir foo CD
# ↓ 展開
mkdir foo && cd $_
# fooを作成してfooに移動

Dockerコマンドの補完

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

config.yml
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 〇〇の補完も設定しました。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
zle accept-line
}
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
zle accept-line
}
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

エイリアスとの使い分け

前述のとおり、zenoの略語展開はキーワードEnterですぐに実行できるため、zshのエイリアスと同じように扱えます。では、エイリアスはもう不要でしょうか?

エイリアスと比較したときのzenoの特徴は次のとおりです。

  • 「Enterならすぐに実行」「Spaceなら展開」と選べる
  • コマンド履歴には展開された状態で残る

登録したコマンドの引数部分などを変更してから実行する可能性があるならzenoを使いましょう。修正する可能性がなくてコマンド履歴をスッキリさせたいならエイリアスというように使い分けられます。

たとえばsedawkのようなOSによって微妙に違うコマンドを統一したいケースではエイリアスのほうがよいでしょう。

.zshrc
case ${OSTYPE} in
darwin*)
alias xargs="gxargs"
alias sed="gsed"
alias awk="gawk"
;;
esac

略語の思想

筆者は今までの癖で略語をDCのように大文字にしてますが、別に小文字でも問題ありません。単純に既存のコマンド名と被らせたくないからやってます。

トラブルシューティング

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

動かない

動かない場合、まずは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便利です。