zsh補完関数の書き方(訳)

zsh補完関数の書き方

by John Beppu
translated into japanese by Jun Mukai

このコラムを読んで数ヶ月もしないうちに、あなたは zsh の途方もないタブ補完システムについて学んでいるだろう。自分の $HOME/.zshrc ファイルに次の2行を足すだけで、タブキーでファイル名を拾うだけでなくて、コマンドラインオプションも持ってくることができる。

autoload -U compinit
compinit

ある Linux コマンドに対するコマンドラインオプションのリストを提供するために、 zsh はそのコマンドに対応した補完関数を実行する(つまり、 ls とタイプしてタブキーを押したら、 zsh は ls の補完関数を実行するのだ)。個々の補完関数はコマンドのオプションをリストアップし、どのオプションが引数を要求するか提示してくれる。補完関数はコンテキスト依存にもなる。たとえば、 man 1 とタイプしたあとでタブキーを押すとする。 man ページの名前が出てくることを確認してほしい。次に man 3 のあとにタブキーを押してみよう。リストは異なっている。 man の補完関数はセクションを認識する。

zsh は多くの Linux コマンドに対する補完関数を含むが、全てではない。ちょっとまってほしい。悩むことはない。自分で自分の補完関数を書くことで、 zsh の可能性を拡張できるのだ。

警告

補完関数は実際、とても簡単に書けるが、 zsh の文書を読んでそれを修得することはない。現在、書き方を手早く教えてくれる単一の情報源というのはない。

zshcompsys と zshcompwid の man ページはほとんど完全な情報源だが、最初から最後まで補完関数をどう書くのかは説明してくれない。これらはリファレンスとして使うには最適だ。

同じように、 Zsh User's Guide にも注釈付きの例がたくさんあるが、最初から最後まで通しのチュートリアルにはなっていない。そこにあるのも、背景なしの非常に発展的な素材である。

最も実用的な情報源は zsh と一緒に配布されている補完関数群だ(補完関数は、 $fpath 変数に列挙されているディレクトリにある)。しかし、ソースコードでさえ不十分だ。コメントは充実してないし、教育目的で書かれていないからだ。配布されている補完関数は、若き関数プログラマが作りたいと思っているであろう達人の作といった類いのものだ。

補完関数の書き方を学ぶのはほとんど絶望であるように思えたかもしれないが、そうでもない。本文書の情報が他の文書の理解を助けてくれるだろう。

はじめる

補完関数の世界に飛び込む準備ができたら、次の設定を自分の $HOME/.zshrc ファイルに加えてほしい。この設定は、補完関数の表示をできるだけ過剰にするものだ。補完関数を書かない場合であっても、この設定でそのパワーを体験できる。

図1: .zshrc へ加える設定
zstyle ':completion:*' verbose yes
zstyle ':completion:*:descriptions' format '%B%d%b'
zstyle ':completion:*:messages' format '%d'
zstyle ':completion:*:warnings' format 'No matches for: %d'
zstyle ':completion:*' group-name ''

補完システムの基本的な仕組み

補完関数の実際のソースを($fpath のディレクトリを探って)見た人なら、 _arguments() 関数を頻繁に使っていることに気付いたかもしれない。これは、補完関数の著者が使う最も重要な関数なので、あらゆるところにある。

_arguments() 関数は実際には組み込み関数 compadd() をラップしている。最終的に情報を取得し、それを補完システムのコアに与えるのはこの compadd() だ。他の重要な関数として気付くのは compdef() で、これは補完関数とコマンドを結びつける。

figlet のための補完関数

次のリストは、figlet コマンド(http://ianchai.50megs.com/figlet.html)のための補完関数である。これはテキストを受け取って、アスキーアートフォントでそれをレンダリングする。

リスト1: _figlet 補完関数
 1   #compdef figlet
 2
 3   typeset -A opt_args
 4   local context state line
 5   local fontdir
 6   fontdir=$(_call_program path figlet -I2 2>/dev/null)
 7
 8   _arguments -s -S \
 9     "(-l -c -r)-x[use default justification of font]" \
10     "(-x -c -r)-l[left justify]" \
11     "(-x -l -r)-c[center justify]" \
12     "(-x -l -c)-r[right justify]" \
13     "(-S -s -o -W -m)-k[use kerning]" \
14     "(-k -s -o -W -m)-S[smush letters together or else!]" \
15     "(-k -S -o -W -m)-s[smushed spacng]" \
16     "(-k -S -s -W -m)-o[let letters overlap]" \
17     "(-k -S -s -o -m)-W[wide spacing]" \
18     "(-p)-n[normal mode]" \
19     "(-n)-p[paragraph mode]" \
20     "(-E)-D[use Deutsch character set]" \
21     "(-D)-E[use English character set]" \
22     "(-X -R)-L[left-to-right]" \
23     "(-L -X)-R[right-to-left]" \
24     "(-L -R)-X[use default writing direction of font]" \
25     "(-w)-t[use terminal width]" \
26     "(-t)-w+[specify output width]:output width (in columns):" \
27     "(-k -S -s -o -W)-m+[specify layout mode]:layout mode:" \
28     "(-I)-v[version]" \
29     "(-v)-I+[display info]:info code:(-1 0 1 2 3 4)" \
30     "-d+[specify font directory]:font directory:_files -/" \
31     "-f+[specify font]:font:->fonts" \
32     "(-N)-C+[specify control file]:control file:->controls" \
33     "(-C)-N[clear controlfile list]" \
34     && return 0
35
36   (( $+opt_args[-d] )) && fontdir=$opt_args[-d]
37
38   case $state in
39   (fonts)
40     _files -W $fontdir -g '*flf*(:r)' && return 0
41     ;;
42   (controls)
43     _files -W $fontdir -g '*flc*(:r)' && return 0
44     ;;
45   esac
46
47   return 1

この関数を使うには、これを _figlet というファイルに保存して、それを自分の $fpath のどこかに置けばいい。 $fpath ディレクトリに追加する権限がなければ、自分のホームディレクトリに何かディレクトリ(たとえば fun)を作成して、 $HOME/.zshrc に次のコマンドを加えることで $fpath リストに追加すればよい。

fpath=(~/fun $fpath)
autoload -U ~/fun/*(:t)

次に zsh をスタートしたときには、補完システムは _figlet 補完関数を発見できるはずである(fun にある他の補完関数も)。

最初のアンダースコアは必須である。というのは、 compinit() (補完システムを初期化する組み込み関数)は $fpath のディレクトリから、アンダースコアで開始するファイルを捜すからである。その上で、ファイルの最初の行をもとに compinit() は多様なアクションを取る。

どんな補完関数も、最初の行は #compdef で開始する。そのあとには、その関数が補完する対象のコマンドが続く。リスト1でもそうなっている。

3行目と4行目は変数 $opt_args、$context、$state、$lineの初期化だ。 $opt_args は連想配列で、-d とか -f とかいったコマンドラインオプションをキーとして、(あれば)そのオプションの実際のパラメタを値として持つ。 $state はスカラ変数で、 _arguments() 関数の状態メカニズムで使われる(詳しくは後述)。他の2つの変数、 $context と $line はこの関数では直接には使われないが、 _arguments() はこれを裏で使っている。これらの変数が _figlet 関数のローカルとして宣言されている理由は、名前空間の汚染を防ぐためである(zshは変数に関してレキシカルではなく動的なスコープを持つ)。

他の変数、5行目の $fontdir は _figlet 関数限定のものだ。この目的は figlet のフォントが保存されているディレクトリを保持することである。このディレクトリが何なのかは、 figlet -I2 を6行目で実行してとりだしているが、プログラムを直接に実行していない点に注意してほしい。

そうではなく、 _call_program() 関数を読んで figlet -I2 を実行してもらい、標準エラー出力を /dev/null にリダイレクトしている。外部コマンドの出力をセーブするには、 _call_program() を使う。これで、達人ユーザは呼ばれる実際のプログラムを上書きできる。 figlet ではわざわざこれをやってるが、補完関数を書くときに注意してほしい点のひとつである。

_arguments 関数を呼ぶ

初期化が終われば、いよいよ _arguments() を呼ぶ準備ができた。この関数は、補完関数を書いて人間の読める文書を受け取ってそれを機械の読む何かに変換する作業を軽減してくれる。 man figlet をしたら、コードの9行目から34行目は man ページとまったく同じであることがわかるだろう。

オプションの記述について9行目から12行目を見るところから始めよう。個々の行は3つのパートに分割される。

排他リストの目的は、「左寄せとセンタリングを同時に指定する意味があるのか?」という自分自身への問いかけを明確にする。これがなければ、 zsh に明示的に指示しなければならない。たとえば、 12 行目の指定は、 -r がコマンド行にあれば、 -x -l -c といったオプションは補完されないことを意味する。

ここには単純な一文字オプションしかない。注意深く見るとわかるが、9行目から25行目は figlet の一文字オプションを扱っている。次はもっと複雑なタイプの引数に移らなければならない。

26行目では、オプションが1つの引数を取る。 -w オプションは出力が取るべき幅を figlet に与えるオプションだが、引数として整数が必要だ。26行目には異なるものが2つある。最初はオプションパートに + が付随していることである。これは、このオプションが同じ単語か次の単語で1つの引数を取ることを意味している。たとえば、 -w 64 や -w72 が有効だ。二番目の違いはヘルプ文のあとの文字列で、これはユーザがタブキーを押したときのヒントとして使われる(output width (in columns))。ただし、このテキストは verbose モード(図1で示した設定)の時にしか表示されない。最後のコラムはヒントとアクションの間の区切だが、 -w には何のアクションもない。

29行目では、このオプションは何らかのアクションを行う。ここのオプション(-I)はある引数を取る。 -I では、可能な値が6つしかない。ヒントのあとで、この6つの引数をカッコに括って表示している。

30行目に他の例がある。 -d オプションは引数としてディレクトリを要求する。ここでは _files() で呼ばれる zsh の内部関数を使ってそのリストを取得している。引数として -/ を与えているので、ディレクトリしか補完リストの対象して現れない。

31行目が _figlet の補完関数にある最後のバリエーションだ。時として、あるオプションの引数に対する補完リストの構築が簡単でないので、カスタムコードとして実現しなければならない場合がある。その場合は状態メカニズムを使う。 "->fonts" が何を意味するかというと、「$state 変数を 'fonts' にセットして私に制御を渡しなさい」という意味である。

この時点で、 _arguments() は仕事のほとんどを終え、制御の流れは34行目の後を必要としない。しかし状態メカニズムを使うときは、36行目まで下りてきて、 $fontsdir 変数を -d オプションの引数で置き換えている。それから38行目に至り、 case 文に遭遇する。

$state 変数の値をベースに、フォントのリストや制御リストを構築しようとする。どちらの場合も _files() 関数をまた使っている(40行目と43行目)。 -W オプションで $fontdir の含むディレクトリに対して補完を行い、 -g オプションにグロブパターンを与えることで補完リストに列挙されるファイルに制限をかけている。

これで終わり。 0 を返すのは何の問題もないことを意味していて、何らかの問題(できるならばあるべきでない)があったときには1(0以外)を返す。これが補完関数である。

次に何をするか?

補完関数を書くことは、この例よりもずっと複雑怪奇になるかもしれない。 figlet のための関数はあまりにも簡単だと思うなら、 tar や cvs や ssh の補完関数を見てみるとよい。これらは多くのものを学ばせてくれるが、コード読み技能が十分に高くないといけない。

それでも、ここで学んだことでいろんなことができる。ドキュメントを眺めていると知らないことがみつかるだろう。そしてその時は、他の補完関数のソースが実際の例として最良のものである(それはちゃんとコメントがついていないけれど)。

最後に、 zsh-workers のメーリングリスト(http://zsh.sunsite.dk/Arc/mlist.html)が助けになると思うので、どんどん subscribe してほしい。そして、自分のやったことをコミュニティに還元してくれるなら、あなたは皆の zsh 体験をもっと良くする手伝いをしているということを覚えておいてほしい。


補完関数のリロード

補完関数を書いているときには、デバッグ目的で何度も関数をリロードしたくなると思う。以下は、 ~/fun 中のものをやみくもに unfunction して autoload する関数である。
r() {
  local f
  f=(~/fun/*(.))
  unfunction $f:t 2> /dev/null
  autoload -U $f:t
}