🐚

zinit をしっかりと理解する

21 min read

2021-12-03 追記

zdharma/zinit が吹っ飛んだので zdharma-continuum/zinit に移行しましょう。

tl; dr

Zinit Wiki の Introduction を読みながら、zinit の書き方をキチンと理解する。

はじめに

今年の G/W は緊急事態宣言で何も予定が立てられませんでしたので、少し時間をかけて ~/.zshrc を見直すことにしました。その過程で「俺、zinit の書き方を全然理解できていないな」と感じたので、Zinit Wiki の Introduction をベースに、zinit の書き方を理解していくことにしました。

https://zdharma.github.io/zinit/wiki/

実は一度も読んだことが無かった Zinit Wiki を読み進めて自分の理解を整理しただけなので、英語を読むのが全く苦ではない人は最初から Zinit Wiki を読みましょう。なにげに Zinit Wiki の日本語翻訳は無さそうなので、これから zinit を始める人にとっては日本語で取っ付きやすいかと思ったので、ほとんど殴り書きですが公開することにしました。

隙あらば自分語り (読み飛ばして OK)

自分が SIer でインフラエンジニアとして設計・構築をやっていた頃は「いつどんな環境で作業をしてもオペレーションが出来るよう、普段使いの端末はなるべくカスタマイズしない」というポリシーでした。これは、面倒くさがりやの性格もあるし、大学卒業時に秘伝の ~/.emacs.el を失ったことも大きかったし、大学の先輩でも前職の同僚でもあった @nizah の影響を受けたのもあります。

いずれにしても、情報系の大学にいた4年間+社会人11年半くらいは下記のようなポリシーでやってきたわけです。

  • ~/.profile~/.bashrc はほとんどカスタマイズしない
  • alias も ls--color=auto --file-type 程度
  • プロンプトも git 付属の git-prompt.sh を利用する程度
    • Ubuntu のデフォルトの ~/.bashrc で読み込まれます
  • peco とか fzf は知ってるけど使わない
  • zsh のインストールや chsh などもっての外

Immutable Infrastructure の初出は 2013 年らしいけど、当時はオンプレ中心にやっていた自分でもリモートで作業する必要性もなくなってきました。Ansible が 2.0 系になった頃かな、と。そこで 2016 年くらいに一度だけ zsh を試したのですが、当時は WSL なんて便利なものもなく、Cygwin 上の zsh は性能的に常用に堪えず、長らく bash を使い続けてきたのです。
# 正確に言うと、既に Build 2016 では発表されていて Insider Preview には来ていたかと思われるが、まさかこんなに実用的になるとは思っていなかった。

しかし、最近は WSL で実用的な性能の Linux 環境が手に入る上、転職して顧客環境における構築作業をやらなくなったこともあり、ちょうど 2020 年の夏くらいから zsh を使い始めたのです。zsh はカスタマイズ性が高すぎてプラグインマネージャーなるものを使ってカスタマイズをするのが一般的で、その中でも zinit というプラグインマネージャーがイケてるらしい。…ということで、zinit の公式のサンプルを元に、ちょっとだけ誰かの dotfiles から必要なものをコピってきたりして、比較的シンプルな ~/.zshrc をよく分からんまま使っていたわけです。

プラグインのダウンロードと有効化

…というわけで、何はともあれ、プラグインのダウンロードと有効化から。以降、基本的に Zinit Wiki / Introduction を読み進めていきます。

zinit load zdharma/history-search-multi-word
zinit light zsh-users/zsh-syntax-highlighting

上記は GitHub 上の zdharma/history-search-multi-wordzsh-users/zsh-syntax-highlighting からプラグインを読み込みます。

下記に示す通り、プラグインの読み込み方法には 2 つのモードがあります。

  • zinit load
    • トラッキング機能を有効にする。具体的には zinit report で一覧表示ができたり、zinit unload でプラグインを無効化できる、等。
  • zinit light
    • トラッキング機能が無効になる。そのかわりに高速化されている。

zinit loadzinit light の後に続くのはデフォルトでは GitHub のリポジトリ名です (他の VCS の取り扱いについては…この先は君の目で確かめてくれ!)。

自分の場合、約1年くらい zinit report は使ったことがないし、普段使いの機能は zinit unload することもないです。唯一気になったのが zinit light でも zinit delete --clean で不要なプラグインを削除する機能が使えるか。これも、自分で挙動を確認した限りは不要なプラグインが zinit light された状態ではもちろん動作しませんでした。しかし、~/.zshrc から削除して zsh を再起動してから zinit delete --clean するば不要なプラグインとして削除できるようでした。自分としてはこれで必要十分です。

そのため、下記のように使い分けることにしました。

  1. 原則、 zinit light を用いて ~/.zshrc を記載する
  2. 一時的に試用するプラグインはコマンドライン上で zinit load で読み込む
    • 試用して問題がなければ zinit light を用いて ~/.zshrc に記載する
    • 問題があれば zinit unload して zinit delete する
  3. 自動有効化/無効化(後述)を利用する場合は、例外的に zinit load を用いて ~/.zshrc に記載する

Oh My Zsh のプラグイン、Prezto のモジュールの再利用

Oh My Zsh と Prezto は zinit よりも歴史も長い zsh のプラグインマネージャーです。これらは標準的に用意されたプラグイン(Prezto の場合はモジュール)があり、自身のリポジトリ内に複数のプラグイン(モジュール)を保持しています。zinit と比較するとプラグインマネージャーとしては起動時間は遅いですが[1]、これらのプラグイン(モジュール)にも、まだまだ使えるものが結構あります。そのため、zinit ではこれらのプラグインが再利用できるようにしています。

(再利用例 1)
zinit snippet https://github.com/robbyrussell/oh-my-zsh/raw/master/plugins/git/git.plugin.zsh
zinit snippet https://github.com/sorin-ionescu/prezto/blob/master/modules/helper/init.zsh
(再利用例 2)
zinit snippet OMZ::plugins/git/git.plugin.zsh
zinit snippet PZT::modules/helper/init.zsh

前者の記法は、特定 URL のスクリプトをダウンロードして source で取り込むだけなので、Oh My Zsh プラグインや Prezto モジュール以外の zsh スクリプトにも使えます。

また、再利用例 1 と 2 は等価なので、Oh My Zsh や Prezto の標準的に用意されたプラグイン(モジュール)を再利用する場合は後者を使うと良いでしょう。このような Oh My Zsh や Prezto 向けのプレフィックスは他にもあるので[2]、~/.zshrc の簡素化や可読性の観点で積極的に使用するのが良いです。

注意点として、Oh My Zsh や Prezto といったプラグインマネージャーが動作するわけではないので、スクリプト間の依存関係は ~/.zshrc を書く人間側で解決する必要があります。例えば、再利用例に記載した git.plugin.zsh は実際のソースコード[3]を見ると分かる通り、Oh my Zsh に含まれている lib/git.sh に依存しています。そのため、実は再利用例そのままではエラーになってしまいます。これらを正常に動作させるためには、下記のように記述する必要があります。

(再利用例 3)
zinit snippet OMZ::lib/git.zsh
zinit snippet OMZ::plugins/git/git.plugin.zsh
zinit snippet PZT::modules/helper/init.zsh

さらに、前述した Oh My Zsh や Prezto 向けのプレフィックスを積極的に使用すると下記のようにも記述できます。

(再利用例 4)
zinit snippet OMZL::git.zsh
zinit snippet OMZP::git
zinit snippet PZTM::helper

依存関係の解決が面倒ではありますが、その分、Oh My Zsh や Prezto のプラグインマネージャーに依存しないので、メモリ消費量や実行時間を抑えることができます。

ちなみに、この文章を書いている時点で自分が使用しているのは OMZP::tmux です。ZSH_TMUX_AUTOSTART=true 相当の機能を自分で実装するのが面倒だっただけなのですが。たくさんあり過ぎて、どれを使っていいものか分からんのですよねえ…。既に Oh My Zsh や Prezto を使っている人は移行の難易度が下がるでしょう。

多機能コマンド zinit ice の謎

さて、zinit を「さっぱり分からん」状態にしている元凶は、私は多機能すぎる zinit ice コマンド、For 構文の 2 つにあると考えています。逆に言えば、この 2 つさえ抑えれば zinit の設定は自ずと理解できると言ってもいいでしょう。

それでは、まず zinit ice を解説していきます。このコマンドが何をやっているかというと、直後に行われる zinit loadzinit light および zinit snippet の挙動を変更しています。

※Zinit Wiki では zinit ice svn pick"init.zsh" を使った例を最初に提示していますが、この世は git に飲み込まれたので、svn については触れません。ここで行っている解説は後でやります。

書き方

では、下記の例をもとに zinit ice の書き方と挙動について説明します。

(zinit ice 使用例)
zinit ice as"program" cp"httpstat.sh -> httpstat" pick"httpstat"
zinit light b4b4r07/httpstat

zinit ice に渡されている as, cp, pick のことを modifier と呼びます。その後、ダブルクォーテーションで囲われたものは modifier に対する引数です。zinit ice はその他にも下記のような modifier と引数の書き方もサポートしていますが、引数がシンタックスハイライトのサポートを受けられないことから、上記の書き方が標準となっています。上と下を見比べてもらえれば一目瞭然でしょう。

(他の書き方の例)
zinit ice as:program cp:"httpstat.sh -> httpstat" pick:httpstat
zinit light b4b4r07/httpstat

zinit ice as=program cp="httpstat.sh -> httpstat" pick=httpstat
zinit light b4b4r07/httpstat

zinit ice --as=program --cp="httpstat.sh -> httpstat" --pick=httpstat
zinit light b4b4r07/httpstat

例における modifier の動作

zinit ice コマンドには modifier とその引数があるということを理解したところで、実際の挙動を理解するためには、まず元のリポジトリを見てみるのが一番です。では、実際にアクセスしてみましょう。

https://github.com/b4b4r07/httpstat

zsh プラグインマネージャーはプラグインを読み込む際、大雑把には git clone してきて、<プラグイン名>.plugin.zsh のようなファイルを探して source します[4]。しかし、このリポジトリはドキュメント用の README.mddemo.png、スクリプト本体である httpstat.sh だけで構成されています。zsh プラグインマネージャーが読み込む <プラグイン名>.plugin.zsh のようなファイルはありません。リポジトリの内容と README.md を見てもらえば分かる通り、このリポジトリはいわゆる zsh プラグインではなく、httpstat.sh というスクリプトを提供するためのリポジトリです。

最初の as"program" という modifier と引数は、こういった source による読み込みが不要な、つまり zsh プラグインではないリポジトリからスクリプト等を得る場合に使います。これは「この後に zinit load または zinit light するリポジトリは zsh プラグインじゃないぞ」と zinit に伝えている、と言い換えてもよいでしょう。

次に cp という modifier です。この modifier は、引数によってリポジトリ内にある httpstat.shhttpstat にコピーするように zinit に伝えています。同様に mv という modifier もありますが、後述する理由もあって cp modifier を利用します。

最後に pick という modifier です。この modifier は、引数で与えられた(先ほど httpstat.sh から cp された) httpstat に実行権限を付け、$PATH に追加するように zinit に伝えています。

実際に、2 行のコマンドを実行してみてください。zinit ice を実行した時点ではパッと見には何も起きていないように見えます。これは、zinit ice コマンドが単体では何もしないためです。その後、zinit light b4b4r07/httpstat をすると、リポジトリが git clone された上で上記の処理が実行された旨の表示がされます。

デフォルトの git clone 先は ~/.zinit/plugins です。また、リポジトリ名の /--- に置換されて、~/.zinit/plugins/b4b4r07---httpstat/ となります。ls -la ~/.zinit/plugins/b4b4r07---httpstat/ を使ってアクセスすると、上記のような動作が行われていることが分かります。さらに、echo $PATH することで $PATH に追加されていることが分かるでしょう。

付け加えるなら、.git ディレクトリがあることから、このディレクトリが引き続き git 管理下にあることが分かります。この状態のままリポジトリの upstream に更新があって git pull した場合のことを想像してください。cp であればコピーされた httpstat が untracking file になるだけで問題なく git merge できます。しかし、mv modifier を利用した場合にはコンフリクトを起こしてしまう可能性があります。だから cp modifier を利用する必要があったんですね (メガトン構文)。

ともあれ、これでユーザーは無事に httpstat コマンドが使用できるようになりました。

mv modifier と atpull modifier

では mv modifier は、必ず git でコンフリクトを起こしてしまうので、使い物にならない modifier なのでしょうか。いいえ、そんなことはありません。こういった問題を避けるために、atpull という modifier があります。

mv を使った例を試す前に、先ほど zinit light でダウンロード・有効化したプラグインを削除しておきます。

$ zinit delete b4b4r07/httpstat -y

Done (action executed, exit code: 0)

次に、下記に mv modifier を使った例を示します。

(mv と atpull の使用例)
zinit ice as"program" mv"httpstat.sh -> httpstat" pick"httpstat" \
    atpull'!git reset --hard'
zinit light b4b4r07/httpstat

cp modifier が mv modifier に置き換えられています。また、(これはzinit の機能には関係ありませんが)行末の \ による改行を挟んで、atpull modifier が追加されていることが分かります。

上記の 2 行のコマンドを実行した後、ls -la ~/.zinit/plugins/b4b4r07---httpstat/ でディレクトリにアクセスすると、httpstat.sh がありません。これは cp ではなく mv modifier を使用したためファイル名が変更されたからです。この状態でリポジトリの upstream で更新が行われると、atpull modifier が効果を発揮します。この modifier はその名前の通り、git pull が行われる際に引数で指定したコマンドが実行される modifier、いわば hook です。引数の先頭に ! を付けた場合は git pull する、付けなかった場合はに実行されます。この例では、git reset --hardgit pull するに実行されます。そのため、zinit light した際に mv された httpstat.sh および httpstat は、git reset --hard で元に戻されます。その後、コンフリクトを起こさずにリポジトリの upstream に追従した上で、再び mv modifier でファイル名が変更され、pick で実行権限を与えられます。

注意点として、インタラクティブなシェルでは !git は直近に実行された git から始まるコマンドに展開されてしまうため、modifier の引数がシングルクォーテーションで囲う必要があります。! に限らず、modifier の引数で zsh の特殊文字を利用するケースは気を付けましょう。

zinit snippetas modifier

as"program" は、zinit snippet でも同様に使うこともできます。例では使用していませんが、この方法でも引き続き atpull modifier が使えます。

(as"program" と snippet の使用例)
zinit ice mv"httpstat.sh -> httpstat" pick"httpstat" as"program"
zinit snippet https://github.com/b4b4r07/httpstat/blob/master/httpstat.sh

…とはいえ、記述量や処理のシンプルさを考えると、自分の ~/.zshrc では as"program" の場合は cp を使った書き方を利用するべきだと判断しました。

as"completion"

zinit icezinit snippet の組み合わせが有効なのは、もっぱら補完用スクリプトの読み込みです。下記に示すように as modifier には引数として "completion" を指定する記述方法で、docker コマンド用の補完を行えるようになります。

(as"completion" と snippet の使用例)
zinit ice as"completion"
zinit snippet https://github.com/docker/cli/blob/master/contrib/completion/zsh/_docker

このファイルがそうであるように、ほとんどの補完用スクリプトは 1 つのファイルで構成されています。リポジトリごと git clone する zinit loadzinit load より相性が良いでしょう。

blockf modifier

この modifier は zsh プラグインによる $fpath の変更を禁止します。

古い zsh プラグインだとプラグインマネージャーに無断で $fpath を勝手に変更して補完機能を実装しているケースがあるらしいです。そういった動作を防いで zinit 側で完全に補完機能をコントロールするための modifier とのこと。補完系のプラグインマネージャーにはとりあえず blockf を付けておけばよい印象(酷)。このあたりは、$fpathcompinit 周りの動作をキチンと理解すると分かるのでしょう。

(blockf の使用例)
zinit ice blockf
zinit light zsh-users/zsh-completions

この後、Zinit Wiki では補完機能のコントロールについて記述がありますが、今のところ必要性を感じていないので省略します。考えなしに補完系のプラグインやスニペットをガンガン入れるとパフォーマンスに響くようなので、zsh の起動や動作が遅いと感じるようになったら見直しましょう。

Turbo モード

wait modifier を使った遅延読み込みのことを Turbo モードというようです。利用するためには zsh 5.3 以降が必要です。…って、いまどき、そんな古い zsh 使ってる環境なんかないでしょう。使用例では zinit load になっていますが、zinit light でも問題なく動作します。

(wait の使用例 1)
PS1="READY > "
zinit ice wait'!0'
zinit load halfo/lambda-mod-zsh-theme

これ、使用例が分かりづらい上、既に instant prompt で romkatv/powerlevel10k を利用していたりすると思ったように挙動が確認できませんでした。動作を確認したい場合、zinit をしただけの素の状態を用意すると良いです。そのためには、下記のような ~/.zshrc にする必要があります。

.zshrc
### Added by Zinit's installer
if [[ ! -f $HOME/.zinit/bin/zinit.zsh ]]; then
    print -P "%F{33}▓▒░ %F{220}Installing %F{33}DHARMA%F{220} Initiative Plugin Manager (%F{33}zdharma/zinit%F{220})…%f"
    command mkdir -p "$HOME/.zinit" && command chmod g-rwX "$HOME/.zinit"
    command git clone https://github.com/zdharma/zinit "$HOME/.zinit/bin" && \
        print -P "%F{33}▓▒░ %F{34}Installation successful.%f%b" || \
        print -P "%F{160}▓▒░ The clone has failed.%f%b"
fi

source "$HOME/.zinit/bin/zinit.zsh"
autoload -Uz _zinit
(( ${+_comps} )) && _comps[zinit]=_zinit

# Load a few important annexes, without Turbo
# (this is currently required for annexes)
zinit light-mode for \
    zinit-zsh/z-a-rust \
    zinit-zsh/z-a-as-monitor \
    zinit-zsh/z-a-patch-dl \
    zinit-zsh/z-a-bin-gem-node

### End of Zinit's installer chunk

実際にやってみると、PS1 を変更した時点でプロンプトが PS1 で指定したものになります。その後、zinit icezinit load を投入し、そのまま何度かエンターキーを叩いてみてください。バックグラウンドで halfo/lambda-mod-zsh-theme をロードしながらもプロンプトは使える状態になっていることが分かります。wait modifier の先頭の ! は、ロードが完了した後にプロンプトを再表示する、という意味です。使用例のプラグインがプロンプトを変更するものなので、読み込み完了後にプロンプトを再表示するために ! が入っているわけですね。例えば、補完プラグインのようにこっそりと遅延読み込みをしたい場合には ! は不要です。また、数字は何秒待ってから遅延読み込みをするかです。指定しない場合は 0 になるようです。

…というわけで Prezto のプロンプトの例はすっ飛ばして、次の使用例へ。

(wait の使用例 2)
zinit ice wait lucid atload'_zsh_autosuggest_start'
zinit light zsh-users/zsh-autosuggestions

前述の通り、waitwait"0" と同義です。また新しく lucid という modifier が登場しました。これは、プラグインを読み込む際のプログレスバーの表示などを省略する modifier になります。そして atload modifier は読み込みが完了した際に実行される関数を指定します。ここで指定された _zsh_autosuggest_start という関数はまさに zsh-users/zsh-autosuggestions の中で定義されており、補完を有効にするための関数です。

また、参考リンクに記載がありますが、wait"0a" のように suffix を付けることで同じ待機時間でもプラグインの読み込み順序を明示的に指定できます。プラグイン同士に依存関係がある場合、suffix の指定をすると良いでしょう。

もう1つの難関: For 構文

zinit ice を紹介した際に、下記のように記載しました。

zinit を「さっぱり分からん」状態にしている元凶は、私は多機能すぎる zinit ice コマンド、For 構文の 2 つにあると考えています

最初に紹介した zinit ice の使用例は、zinit ice ... した後に zinit load/light/snippet する、というものでした。ここまでの説明で zinit ice の動作を理解した今、実は For 構文は簡単に理解できます。なぜなら、この構文は zinit ice した後に zinit load/light/snippet する、という一連の流れを単に 1 つのコマンドに省略した構文だからです。先ほどの zsh-users/zsh-autosuggestions の使用例を For 構文で書き直してみます。

(For-Syntax の使用例 1)
zinit wait lucid atload'_zsh_autosuggest_start' light-mode for \
    zsh-users/zsh-autosuggestions

最初に紹介した zinit ice の使用例と違い、2 つの相違点があります。

  • ice が不要になっている
  • zinit light ... を別コマンドではなく、light-mode for ... と 1 行に続けている

この構文の良い点は、下記の 3 点です。

  1. for に続けて、複数の zsh プラグインを書くことができる
  2. for よりも前に書いた modifier は for に続けて書いた複数の zsh プラグインのデフォルト値になる
  3. その上で、for に続けて、プラグイン毎に modifier を上書きできる

文章を読むと難しく感じますが、実際の例を見ると簡単です。やっていきましょう。

(For-Syntax の使用例 2)
zinit wait lucid for \
                        OMZ::lib/git.zsh \
    atload"unalias grv" OMZ::plugins/git/git.plugin.zsh

この例では、下記のように動作します。

  1. 遅延読み込み (Turbo モード)で、プログレスバーなどを出力せず下記のプラグインを読み込む
    • OMZ::lib/git.zsh
    • OMZ::plugins/git/git.plugin.zsh
  2. OMZ::plugins/git/git.plugin.zsh の読み込み完了後に unalias grv する

ね、簡単でしょう。これで、zinit を「さっぱり分からん」状態にしている元凶の 2 つを抑えたことになります。

load および unload modifier によるプラグインの自動有効化・無効化

ここから先はちょっとした応用編。load および unload modifier を利用することで、特定条件下でのみプラグインを有効化したり無効化したりできます。

# Load when in ~/tmp
zinit ice load'![[ $PWD = */tmp* ]]' unload'![[ $PWD != */tmp* ]]' \
    atload"!promptinit; prompt sprint3"
zinit load psprint/zprompts

# Load when NOT in ~/tmp
zinit ice load'![[ $PWD != */tmp* ]]' unload'![[ $PWD = */tmp* ]]'
zinit load russjohnson/angry-fly-zsh

例では For 構文は出てきていませんが、zinit ice をちゃんとマスターしていれば、なんとなくやっていることが分かるはずです。この例では、load および unload modifier に指定した条件を満たした場合に、自動的にプラグインの zinit load または zinit unload が行われます。同じ遅延読み込みでも、wait~/.zshrc 読み込み時にのみ行われますが、loadunload は常時判定が行われるという違いがあります。一方、load modifier で指定した条件を満たさない限り zinit load はされませんから、zsh の起動速度には大きく影響を与えない、という点では wait と同じです。

注意点としては、条件を満たした場合にのみ load が行われるので、プラグインの内容によっては lucid modifier を上手く組み合わせる必要があります。また、思い出してください。zinit lightzinit load の違いを説明した際に書いた通り、zinit unload するには zinit light で読み込んではいけません。

最後に

いかがだったでしょうか。この文章は Zinit Wiki の Introduction をザックリと意訳しただけです。たったそれだけの事ですが、呪文のように見えた他人の dotfiles~/.zshrc に記載された zinit コマンドの意味が、なんとなく掴めるようになったのではないでしょうか。

この後、zinit を更に使いこなすために、皆さんが理解するべき要素は大きく 2 つあります。

  1. Ice Modifiers
  2. Annexes プラグイン
    • Zinit Wiki、Zinit Extensions 章
    • 各 Annexes プラグインの README.md
      • 特に、z-a-bin-gem-node は非常によく使われているので読んでおいたほうがよい

1 については言うまでもありません。ice を制するものは zinit を制する。ここで紹介した modifier が全てではないので、より多くの ice modifier を理解するべきです。差し当たり、多くの dotfiles で用いられていて、この記事で説明できなかった下記の modifier の挙動は抑えておいたほうが良いでしょう。

  • Cloning Options
    • from
    • bpick
  • Selection of Files
    • src
    • multisrc
  • Conditional Loading
    • if
    • has
  • Command Execution After Cloning, Updating or Loading (hook)
    • atclone
    • atpull
    • atinit
    • atload
  • Others
    • as"null"
    • id-as

2 については、Annexes の名の通り、zinit に付属しているプラグイン群です (付属と言いつつ別のリポジトリですが、zinit のインストーラーが導入するか質問をしてきます)。特に、zinit-zsh/z-a-bin-gem-nodesbin, fbin といった ice modifier を実装しており、他人の dotfiles を理解する上では必要になります。また、メインの開発言語として Rust を使用している場合、exa のような Rust 製ツールを Cargo で管理できる zinit-zsh/z-a-rust にも目を通しておくと良いでしょう。

それでは、皆さんに良い zinit ライフがあらんことを。

Discussion

ログインするとコメントできます