rcmdnk's blog
Last update

Completion: A book of daily devotionals, straight from the heart of the living God.

BashのTabを押した時に出る補完の自作等について。

Bash補完

complete

Bashであるコマンドに対して補完を出来るようにしてあげるには

complete -F _comp_func cmd

completeコマンドを使います。

これでcmdというコマンドに対する補完を_comp_funcという関数で与えます。 -Fで関数を指定。

この_comp_funcという関数は別で定義しないといけませんが、 この関数は通常ダイレクトに使われてほしくないもの(使っても意味無いもの) なので_を最初に付けたりして通常のコマンドを分けて置くのが普通です。 (別に_は絶対必要なものではないがあった方が良い。)

コマンドは複数一緒に指定することが可能。

complete -F _comp_func cmd1 cmd2 cmd3

また、

complete -o default -F _comp_func cmd

と、オプションでdefualtを付けると対応補完がない場合に ファイル/ディレクトリの補完をしてくれるようになります。

補完補助関数

COMPREPLY

補完を決める補助関数内では 基本的には COMPREPLYという値に配列を入れるとその中身が コマンドラインで補完に使われる様になります。

_comp_func
1
2
3
_comp_func () {
  COMPREPLY=(aaa bbb ccc)
}

としておけば

$ cmd <Tab>
aaa bbb ccc

と言った感じにTabを押した時に補完が出ます。

ただ、このままだとcmd の後に何を書いても常に上の三つが 補完候補として表示されるだけです。

$ cmd a<Tab>
aaa bbb ccc

と、一部を書いてもそれを絞り込んで絞ったものだけに補完してくれることはありません。 (まだ3つとも補完の候補として残っているため。)

compgen

途中までの入力を使って候補を絞るためには

COMPREPLY=($(compgen -W "aaa bbb ccc" -- "${cur}"))

と、compgenコマンドを使います。

-Wで指定した文字列の中から、そのあとで指定された文字から始まるものだけを ピックアップしてくれます。 (現在入力中のものが-で始まるオプションみたいのだったりする可能性があるので、 それを唯の文字列として扱うために--をはさんでおくと安全です。)

$ compgen -W "aaa abc bbb ccc" a
aaa
abc

の様に絞り込めることが確認できます。

つまり上の配列を作る所で、curに現在入力中の文字を入れれば この結果をCOMPREPLYに入れることで途中までの入力を効かせる事が出来ることになります。

これを持ってくるために次のCOMP_WORDS等を使います。

COMP_WORDS, COMP_CWORD

補完に使う関数の中では COMP_WORDSCOMP_CWORDという変数が用意されていて、 それぞれ

  • COMP_WORDS: コマンド自身が0番目、それ以降の引数が1番目以降に入った配列。 空白を空けてTabを押した場合には最後に空白が入ります。 従ってコマンド自身は必ずあり、空白おいて次へ行かないとこの補完自体が動かないので、 COMP_WORDSは必ず長さが2以上になります。
  • COMP_CWORD: 現在カーソルが居る位置。
1
2
3
4
_comp_func() {
  COMPREPLY=($COMP_CWORD ${COMP_WORDS[@]})
}
complete -F _comp_func test_cmd

みたいな物をsourceしてみると

$ test_cmd <Tab>
1 test_cmd
$ test_cmd aaa<Tab>
1 aaa test_cmd
$ test_cmd aaa <Tab>
2 aaa test_cmd
$ test_cmd aaa bbb <Tab>
3 aaa bbb test_cmd
$ test_cmd aaa <Tab> bbb
2 aaa bbb test_cmd

みたいな事を確認出来ます(<Tab>の位置にカーソルがあってそこでTabを押したとします)。

CMOP_WORDSにはカーソルの位置に依らずコマンドラインに書いた全ての 引数が入っています。

一方、カーソルを戻せばCOMP_CWORDの数字が変わっているのが分かります。

これらの値は通常

local cur=${COMP_WORDS[COMP_CWORD]}
local prev=${COMP_WORDS[COMP_CWORD-1]}

という値を使うのが定石。 上に書いたcompgenで使う様に現在の位置の引数をcurへ入れ、 一つ前の引数をprevへ入れます。 (prevは一番最初はコマンド自身になります。)

prevに関してはcmd -f <file>みたいなオプションを取る時に、 -fprevに入っていたら補完はファイル名にする、みたいな使い方が出来ます。

ここで、1つ注意としてコマンドライン上で使う関数なので、 関数内で使う変数は特別な理由がない限りは全てlocalなものにし、 作業に影響が無いようにすることを忘れないように。 (for文を回す時に使うfor i in 1..10i等は先にlocal iと宣言しておいたり きちんと処理する必要があります。)

Bash-Completion

Bash-Completion: http://bash-completion.alioth.debian.org/ はBashの補完を拡張するための便利設定が入ったパッケージです。

様々な追加の補完や、補完に関する補助関数なども使える様になります。

公式ページに有るGitのレポジトリからcloneするなり コピーするなりしてきて指示通りにファイルを配置するか、 MacならHomebrewで

$ brew install bash-completions

/usr/local/etc/bashcompletionというファイルと /usr/local/etc/bash_completion.d/という設定ファイルが 入ったディレクトリがインストールされます。

インストールしたbash_completionの中には _get_comp_words_by_refという関数があり、これに

_get_comp_words_by_ref cur prev

とすると上でCOMP_WORDSからセットしてるのと同じ様な事が出来ます。 (ただ、ちょっと癖があって上手くいかないことがあるのでCOMP_WORDSを 直接使ったほうが安全。)

他にもいくつか補助関数があるので、 複雑なことをしようとする時にはこれらの補助関数が役にたつかもしれません。

Bash-Completionを有効にするためには.bashrcなんかで

.bashrc
1
source /usr/local/etc/bash_completion

bash_completionを読み込むようにしておけばOK。 bash_completionを読み込むと、この中でbash_completion.d にあるファイルも全て(.bak.swpファイル等以外)読み込んでくれます。

Homebrewで入れる場合には

.bashrc
1
2
3
4
brew_completion=$(brew --prefix 2>/dev/null)/etc/bash_completion
if [ $? -eq 0 ] && [ -f "$brew_completion" ];then
  source $brew_completion
fi

みたいにしておくとHomebrewの設定を変更しても大丈夫になります。

MacのHomebrewでBash補完のファイルをインストールするFormulaを作る

自分で作ったコマンドをHomebrewで管理していて その中にBash補完を入れたい場合や、 また、既にあるコマンドに対してでも自分で補完関数を作って それをパッケージとして管理したい場合についてです。

一番分かりやすいやり方としては、 Bash-Completionをインストールした上で bash_completion.dの中に自分の補助関数を書いたファイルを入れることです。

まず、パッケージ内にetc/bash_completion.d/my_comp_file というファイルを作って、 これをインストールするためにFormula内に

1
2
3
4
5
6
  option "without-completions", "Disable bash/zsh completions"
  def install
    if build.with? "completions"
      bash_completion.install "etc/bash_completion.d/my_comp_file"
    end
  end

みたいにインストールを書きます。

もしインストールするものが他にもあるならinstall関数の中に一緒に書いてください。

インストールする際に名前を変更したいときは、

bash_completion.install "etc/bash_completion.d/my_comp_file" => "different_name"

みたいにするとdifferent_nameというファイル名でインストールされます。

optionwithout-completionsというものを使ってますが、 補完が必要ない時に--without-completionsとすると補完のファイルはインストールしないで済むようにしています。 これも大体補完ファイルを持ってるパッケージではやってるので 入れておいた方が良いかな、と。

実際のインストールは

bash_completion.install "etc/bash_completion.d/my_comp_file"

ですが、bash_completionprefix+”etc/bash_completion.d”になります。 (prefixは通常は/usr/local/。)

install関数の中では、このパスの変数.installの形に 与えた物をそのパスの中にインストールすることが出来ます。

つまり、上の場合はmy_comp_file/usr/local/etc/bash_completion.d の中にインストールすることになります。

元のファイルは別に何処にあっても良いので、 comp/my_comp_fileとかに起きたければ

bash_completion.install "comp/my_comp_file"

としてあげれば良いだけです。(上の例ではインストール先と同じになるようにしています。)

これでbash_completionを読み込む設定をしてあれば このmy_comp_fileも同時に読み込まれる様になります。

ここで、Furmulaのinstall関数内でのちょっと引っかかった部分として、 この関数内では同じファイルを二度使おうとするとエラーが出ます。

つまり、

bash_completion.install "etc/bash_completion.d/my_comp_file"
etc.install "etc/bash_completion.d/my_comp_file"

みたいに同じファイルを別々の場所にインストールしようとする事は出来ません。

ちょっと詰まったのが、etcの中に既に他にインストールしたいものが入っていて、

1
2
3
4
5
6
  def install
    prefix.install "etc"
    if build.with? "completions"
      bash_completion.install "etc/bash_completion.d/my_comp_file"
    end
  end

の様に、まずetcディレクトリをインストールしてしまう様に してあってそこにさらにbash_completionへのインストールを書いた時。

ディレクトリごと指定したものでも、その中身が全て使われる事になるので、 この場合もbash_completion.installの所で

Error: No such file or directory - etc/bash_completion.d/my_comp_file

というエラーが出ます。

なので、最初のetcのインストールでは prefix.installetc全体を指定せずに

`etc.install` "etc/file"

みたいに個別に指定する必要があります。

この辺ちょっと注意。

また、余り関係ないですが、この様にetcをインストールしたい場合、 prefix.installetcディレクトリごとやると、 /usr/local/etc/内にそれぞれのファイルがリンクとしてインストールされるのですが (ディレクトリがある場合はそのディレクトリを掘った上で中に ファイルのリンクが貼られる)、 etc.installはちょっと特殊でこれでインストールした場合は 実態がコピーされます。

このコピーはパッケージをuninstallしても消えません。

example-formula.rb: https://github.com/Homebrew/homebrew/blob/master/Library/Contributions/example-formula.rb を見るとetcに関しては

# Configuration stuff that will survive formula updates

と書いてあって、etcに入れるような設定ファイルはアップデート時にも残す 的な事をしたいことから一度インストールしたらインストールしっぱなし、 な状態にする様です。

こうなると後は手動で消す以外にない状態になります。

読み込まなければ良いだけなので悪さをすることはありませんが、 アンインストール時にも消したいと思ったら、

(prefix+'etc').install "etc/file"

の様に、etcを直接使わずprefix+'etc'を自分で作ってそれを 使ってinstallするようにすると、 /usr/local/etcにシンボリックリンクとしてインストール出来、 パッケージアンインストール時にも消せる様になります。

その辺最近 Brew-file でも色々アップデートしたのでBrew-fileのFormulaや補完関数も参考になるかも。

homebrew-file/brew-file.rb

homebrew-file/etc/bash_completion.d/brew-file

追記: 2015/05/15

Zshについても書きました。

Zshの補完について

追記ここまで

Sponsored Links
Sponsored Links

« sshの接続を高速化するconfig設定 Zshの補完について »

}