rcmdnk's blog

赤ちゃんのための補完食入門

以前、 Zshの補完について ちょっと書きましたが、 足りない所とか理解しきれてなかった所があるので追記。

Zshの補完について

取り敢えず前回のはこちら。

補完を有効にするために

autoload -U compinit
compinit

.zshrc等で設定しておくことやcompdef/compadd等の 補完を作成、登録するコマンドなどについてまとめています。

上のautoloadcompinitを有効にさせるためのコマンド。

compinitは指定されたパスにある補完ファイルから 補完をアップデートして、~/.zcompdumpというファイルに コマンドとそれに使う補間関数の定義の一覧を作成します。

パスはfpathまたはFPATHで設定されていて、 何方も同じパスが入っていますが、 fpathの方は配列、FPATHの方は通常のPATHの様に:で区切られた文字列として 登録されています。

何方かをアップデートするともう片方も自動的にアップデートされるので 何方かだけをいじればOK。

$ echo $fpath
/usr/local/share/zsh-completions /usr/local/share/zsh/site-functions /usr/local/Cellar/zsh/5.2/share/zsh/functions
$ echo $FPATH
/usr/local/share/zsh-completions:/usr/local/share/zsh/site-functions:/usr/local/Cellar/zsh/5.2/share/zsh/functions
$ fpath=(/new/path "${fpath[@]}")
$ echo $fpath
/new/path /usr/local/share/zsh-completions /usr/local/share/zsh/site-functions /usr/local/Cellar/zsh/5.2/share/zsh/functions
$ echo $FPATH
/new/path:/usr/local/share/zsh-completions:/usr/local/share/zsh/site-functions:/usr/local/Cellar/zsh/5.2/share/zsh/functions

.zcompdumpには以下の様にコマンドと関数が登録されています。

.zcompdump
1
2
3
4
5
6
7
8
...
_comps=(
...
'brew' '_brew'
'brew-file' '_brew-file'
...
)
...

コマンドに対して補間関数が定義されていれば、 こんな感じで

'<command>' '<completion (initialization) function>'

と言った感じに書かれているはずです。 注意なのは下に書くように、この2番めは実際に補完を行う関数だけが書かれたファイルか、 または、補完の定義を行う関数(ファイル)でもOK、と言う点。

補完設定ファイルの作成

Homebrewの補助コマンドの Brew-file ではbrew fileコマンドに対する補完や、 さらにbrewコマンド自体にも補完に関する強化を行っています。

で、普段Bashばかり使っているのであれでしたが、 Zshの方で実はきちんと補完が動いてませんでした。 なのでその辺のアップデートがてらの補完に関するメモです。

まず、補完の作り方は Zshの補完について で大体書いた感じですが、 そのファイルの記述で一つ忘れていたのが、 ファイルの先頭にShebangの様な形で、

#compdef brew-file

と書いておく必要があります。 #!/bin/zsh等普通のShebangを書いてはダメ 1

ここでcompdefの後ろに書いてある物が対象のコマンドになります。

また、使われる関数名は記述されているファイル名になります。 このbrew-fileコマンドの場合には

/usr/local/share/zsh/site-functions/_brew-file

というファイルがfpathの中に入っています。

この様なファイルがfpathに入った状態でcompinitすると 補完の定義がアップデートされ.zcompdumpが更新されます。

上にも書いたように、この_brew-filebrew-fileコマンドを 最初に呼んだ時に呼ばれ、補完の方法をロードするために使われます。

実際に、_brew-fileの内容は以下のようになっています2

homebrew-file/brew-file

この中では_brew_fileという関数を定義して、

compdef _brew_file brew-file

brew-fileの補完を定義しています。

なので、実際に補完に使われる関数は_brew_file(真ん中アンダーバー)になり、 _brew-file(真ん中ハイフン)はこのファイルが 初回だけ補完の定義用に呼び出される、と言う形になっています。

ファイルの中にcompdef等がない補完ファイルもあり、 この場合はそのファイルが補完関数として登録されます。

また、gitの補完ファイルなどは

git/git-completion.zshlink
1
2
3
4
5
6
7
8
#compdef git gitk
...
_git ()
{
...
}

_git

みたいな定義になっていて、無限再帰しそうな感じもあるのですが、 この場合は関数の_gitが良しなにgitへの補完関数として登録されています。

なので書き方として、#compdef <command>を書いた上で、

  • 中でもcompdefを使い指定のコマンドに特定の関数を補間関数として与える。
  • 補完関数の内容を直接ファイルに書き込み、そのファイル自体を補間関数だとして使う。
  • ファイル名と同様の関数を書き、それを最後に呼び出すような形で置いておいてその関数を補間関数として使う。

と言った書き方が出来ます。

最後の形式のものだと、最後に関数を呼ぶ際、

_git "$@"

みたいな形で引数を渡す形のものもありましたが、 そうで無いものも動いているので、この辺は古い書き方?なのかもしれません。 (昔はファイル内で定義された関数、みたいなのを上手くロードできずに ファイルの中身そのものを補完関数として渡していたために直接引数を渡す必要があった?)

man zshcompsysとかを読んでみたもののこの辺イマイチはっきりしなかったんですが、 取り敢えず zsh-users/zsh-completions とかにある補完を参考にしていけばやりたいことは出来る様になるかと。

補間関数がロードされるタイミング

一つ大きく勘違いしてたのがこれらの補間関数がロードされるタイミング。

Bashの場合、補完をロードするには単に定義が書いてあるスクリプトをロードするだけです。 Bash-Completion では

source /usr/local/bash_completion

のようにbash_completionという親ファイルを読み込み、 この中から/usr/local/bash_completion.d内にある定義ファイルを 片っ端から読み込んでいく、と言う形です。

この際、関数などは勿論読み込まれ、completeコマンドによって 各コマンドと結び付けられます。

一方、Zshの場合は上に書いたようにcompinitを呼ぶのが 通常の補完の有効化です。

この際、起こることはコマンドと各補完初期化ファイル/関数との結びつけだけです。 実際にファイルが読み込まれたりはしません。

従って、compinitした後だからといって該当の関数を直接使おうとすると エラーが起こります。

但し、fpathに含まれるファイルは、その名前の関数として、

1
2
3
4
_brew-file () {
        # undefined
        builtin autoload -XUz
}

みたいな感じで定義されています。 (where _brew-fileまたはautoloadとすると全てのこの様な読み込み前関数を見れます。)

ここで、autoload -Xは、autoloadが実行されるその関数名と同じ名前のファイルをfpath内から探して その内容を関数の内容と置き換える、と言った操作をします 3

つまり、compinit時には本当に結びつけだけして、 この関数を一度呼んで初めて中身が実装されます。

他の-Uオプションはこの関数内でエイリアスを展開しないようにするオプション、 -zはZsh形式で読み込む、と言うオプション。 通常はこれですが、-kというksh形式で読み込むオプションもあります。

builtinautoloadが下手にエイリアスとかされてても元のビルトインコマンドをそのまま使うためのコマンドです。

ここで困ったのが homebrew-file/brew-wrap というbrewコマンドを拡張するファイルの中で、 この補完関数を使っている所。

compinitを下あとで、**brew-wrap**を読みこむようにしておいても この関数がまだキチンと定義されてないためおかしな挙動をします。

これを回避するために 以下の用に.zcompdumpの中の定義を見て、 ここにファイルが登録されていれば 関数をロードして実行(補完を定義する)、ということをする様にしました。

brew-wrap
1
2
3
4
5
6
7
8
9
10
11
# Wrap the brew command completion, to use the completion on `brew file`.
if ! type -a _brew >& /dev/null;then
  return
elif [ "$ZSH_VERSION" = "" ] && ! type -a _brew_file >& /dev/null;then
  return
elif [ "$ZSH_VERSION" != "" ];then
  if ! type -a _brew-file >& /dev/null;then
    return
  fi
  _brew-file
fi

homebrew-file/brew-wrap

_brew-fileとファイル名が関数になったものが見つかればその関数を実行します。 これでBashの時の様に中で定義されてる_brew_fileという関数が 実際に読み込まれている状態になります。

Sponsored Links
  1. Bashと共用のものにしていますが、 Bashの場合は単にsourceで読み込むだけなのでこの部分は Shebangでもどんな#で始まる行であってもただのコメント業として無視されるので なんでも良い。

  2. ここではファイル名がbrew-fileですが、実際にZshで使う際には _brew-fileと言う名前で使うために違うディレクトリにリンクが貼ってあります。

    homebrew-file/_brew-file

    BashとZshで同じファイルを使ったほうが楽なのでそのための処置です。

  3. autoload

    autoload -U _brew-file
    

    の様に直接使うと fpathの中からファイルを探して来てそれをロードします。 (compinitfpathの中から#compdefが先頭にあるファイルを探してきてロードする。)

    この際はcompinitで行われるのと同様、中身にbuiltin autoloadが入っただけの関数が定義されるだけです。

    もし、

    autoload +X _brew-file
    

    と、+Xオプションを使うと定義づくりと実行を同時に出来るのですが、 なぜか+X-Uオプションは同時に使えない、と言う制限があるので もしファイルをロードして実行したい、と言う場合、 安全にエイリアスを無視したい場合は一度-Uだけで呼んで、 その後その関数を直接呼ぶ必要があります。

    autoload -U _brew-file
    _brew-file
    

Sponsored Links

« apt-cygのGitHubレポジトリが閉鎖されてる Macでのトラックパッドでのドラッグの無効化/有効化 »

}