ドーモ、SREチームの湯谷(@yutannihilation)です。最近気になるモジュラーシンセはIntellijelです。
上司がいい感じのコマンドをつくるという記事を書いていましたが、いい感じのコマンドにはいい感じのタブ補完を付けたくなります。この記事ではBashのタブ補完を自作する方法を紹介します。
タブ補完の仕組み
Bashのタブ補完自体はBashに組み込まれている仕組みです(参考:Bash Reference Manual - 8.6 Programmable Completion)。completeというBashの組み込み関数によって補完方法(compspec(completion specification)と言うらしいです)が規定されていて、これがタブなどによって起動されます。
タブ補完は、lsならファイル名、cdならディレクトリ名、というようにコマンドに応じたものが設定されています。最近のLinuxディストリビューションでは多くのコマンドで快適にタブ補完ができますが、それはbash-completionというパッケージのおかげです。
このパッケージが用意してくれている数々の補完関数は、/usr/share/bash-completion/completions以下に配置されていることが多いです(このパスはディストリビューションやbash-completionのバージョンによっても異なります)。たとえば、manの補完方法は/usr/share/bash-completion/completions/manに定義されています。見てみましょう。
$ cat /usr/share/bash-completion/completions/man # man(1) completion -*- shell-script -*- [[ $OSTYPE == *@(darwin|freebsd|solaris|cygwin|openbsd)* ]] || _userland GNU \ || return 1 _man() { local cur prev words cword split _init_completion -s -n : || return ...略... __ltrim_colon_completions "$cur" return 0 } && complete -F _man man apropos whatis # ex: ts=4 sw=4 et filetype=sh
_man()というシェル関数が定義され、それに続いてcompleteコマンドが実行されています。-Fは、補完するための関数を指定するオプションです。これによって、manコマンドとaproposコマンドとwhatisコマンドは_man()という関数によって補完が行われるように設定されています。
ユーザがタブを押すと、_man()にCOMP_CWORDとかCOMP_WORDSといった変数(後述)で今のコマンドラインに入力されている文字が渡されます。_man()はそこから候補を絞り込んだ結果を、COMPREPLYという変数で返します。それがタブ補完の候補として表示される、という流れです。
はじめの一歩
まず、候補としてoneとtwoを表示するシンプルな関数_dummy()をつくってみます。候補のリストをCOMPREPLYという変数に入れると、補完候補として表示されるようになります。
$ _dummy() { COMPREPLY=(one two); }
これをmanコマンドの補完方法に指定します。
$ complete -F _dummy man
これでmanコマンドは_dummy関数でタブ補完されるようになりました。ここで、
$ man <TAB>
と打つと、以下のように候補が表示されるはずです。
$ man one two
でも上の_dummy()だと、コマンドラインの状態はお構いなしに同じ候補しか表示しません。
$ man o<TAB> one two $ man one<TAB> one two
oまで打ったら候補はoneだけになってほしいですよね。そのために_dummy()は、今のコマンドラインの状態を受け取って、候補の絞り込みをする必要があります。
補完候補の絞り込み
これには、COMP_CWORD・COMP_WORDSという変数とcompgenというコマンドを使います。
COMP_CWORD・COMP_WORDS
この変数には、それぞれ以下の値が入っています。
COMP_CWORD: 今カーソルがあるワードは何語目かCOMP_WORDS: コマンドラインのワードのリスト
たとえば、以下のようにCOMP_CWORDとCOMP_WORDSをechoする関数を設定してみます。
$ _dummy() { echo echo COMP_CWORD: ${COMP_CWORD} echo COMP_WORDS: ${COMP_WORDS[@]} }
これでタブを押すと、以下のような結果になります。COMP_CWORDとCOMP_WORDSにどのような変数が渡っているかイメージが付くでしょうか。カーソルの直前がワードのときと空白のときで結果が異なる点には注意が必要です。
$ man <TAB> COMP_CWORD: 1 COMP_WORDS: man
$ man test test2<TAB> COMP_CWORD: 2 COMP_WORDS: man test test2
$ man test test2 <TAB> COMP_CWORD: 3 COMP_WORDS: man test test2
_get_comp_words_by_ref()
現在カーソルがあるワードは${COMP_WORDS[${COMP_CWORD}]}、そのひとつ前は${COMP_WORDS[${COMP_CWORD}-1]}といったかたちでアクセスすることができます。でも、ちょっと長くて見づらいです。_get_comp_words_by_ref()という便利関数を使うと、現在カーソルがあるワードは$cur、ひとつ前のワードは$prev、ワード数は$cwordという変数に入ります。
こんな感じです。
$ _dummy() { local cur prev cword _get_comp_words_by_ref -n : cur prev cword echo echo cur: ${cur} echo prev: ${prev} echo cword: ${cword} } $ man arg1 arg2<TAB> cur: arg2 prev: arg1 cword: 2
こうした便利関数はやはりbash-completionパッケージによって提供されているもので、/usr/share/bash-completion/bash_completionに定義されています。定義が気になる場合はこのファイルを覗いてみましょう。(例えば_get_comp_words_by_refの定義はこのあたりです)
compgen
compgenは、オプションによって出てくる補完候補から、引数とマッチ(基本的に前方一致)するものだけを絞り込むコマンドです。各オプションはman 7 bash-builtinsに詳しく書かれています。ここでは主要なオプションだけ紹介します。
-W
-Wは、変換候補となる文字列のリストを指定するオプションです。以下のように、指定した文字列リストから、引数と前方一致するものだけに絞り込んでリストアップしてくれます。
$ compgen -W "one two once twice" -- one two once twice $ compgen -W "one two once twice" -- o one once $ compgen -W "one two once twice" -- onc once
-c/-f/-d
自前で文字列のリストを与えなくても、コマンドのリストとか、ファイルのリストとかを補完候補にすることができるオプションです。-cはコマンド(と実行ファイル、ディレクトリ)、-fはファイル、-dはディレクトリを補完するようになります。たとえば、以下のように-cオプションにmanという引数を与えると、manから始まるコマンドがリストアップされます。
$ compgen -c -- man mandb manpage-alert man manpath
補完してみる
compgenを使うと、dummy()は現在のコマンドラインの文字を使って候補を絞り込んだ補完ができるようになります。
$ _dummy() { local cur prev opts _get_comp_words_by_ref -n : cur prev opts="one two once twice" COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) }
こんな感じの動きです。
$ man <TAB> once one twice two $ man on<TAB> once one
ようやく補完できました。めでたしめでたし!(たったこれだけしか補完できない_dummy()に飽きたら、complete -r manでデフォルトの補完に戻すことができます)
例:go-apt-cacher の補完
具体例として、APT専用のキャッシュプロキシgo-apt-cacherのタブ補完をつくってみましょう。
go-apt-cacherには以下のオプションがあります(バージョン1.2.2時点)。オプションによって補完すべきものが違います。-fと-logfileはファイル名をとり、-logformatと-loglevelはそれぞれ固有の選択肢をとります。
$ go-apt-cacher -h Usage of go-apt-cacher: -f string configuration file name (default "/etc/go-apt-cacher.toml") -logfile string Log filename -logformat string Log format [plain,logfmt,json] -loglevel string Log level [critical,error,warning,info,debug]
まず、go-apt-cacher -まで入力してタブを押すとオプションが補完されるようにしてみましょう。オプションはコマンドの直後、つまり$cwordが1の位置にきます。このときはオプションのリストをcompgenの-Wに指定して補完するようにします。
$ _go_apt_cacher(){ local cur prev cword _get_comp_words_by_ref -n : cur prev cword if [ "${cword}" -eq 1 ]; then COMPREPLY=( $(compgen -W "-f -logfile -logformat -loglevel" -- "${cur}") ) fi } && complete -F _go_apt_cacher go-apt-cacher
次に、オプションが-logformatの場合はplain、logfmt、jsonのいずれかが、-loglevelの場合はcritical、error、warning、info、debugが補完されるようにしてみましょう。カーソルがオプションの次に来ているときは、$prevにオプションが入るのでこれを条件分岐に使うといいでしょう。
$ _go_apt_cacher(){ local cur prev cword _get_comp_words_by_ref -n : cur prev cword if [ "${cword}" -eq 1 ]; then COMPREPLY=( $(compgen -W "-f -logfile -logformat -loglevel" -- "${cur}") ) elif [ "${cword}" -eq 2 ]; then if [ "${prev}" = "-logformat" ]; then COMPREPLY=( $(compgen -W "plain logfmt json" -- "${cur}") ) elif [ "${prev}" = "-loglevel" ]; then COMPREPLY=( $(compgen -W "critical error warning info debug" -- "${cur}") ) fi fi } && complete -F _go_apt_cacher go-apt-cacher
さらに、オプションが-logfileや-fのときにはファイルへのパスが補完されるようにしてみます。ファイルの補完にはcompgenの-fオプションを使います。compopt -o filenamesは、ディレクトリ名には/を付けてくれるようにするおまじないです(詳しいことが気になる方はman 7 bash-builtinsを読んでください)。
$ _go_apt_cacher(){ local cur prev cword _get_comp_words_by_ref -n : cur prev cword if [ "${cword}" -eq 1 ]; then COMPREPLY=( $(compgen -W "-f -logfile -logformat -loglevel" -- "${cur}") ) elif [ "${cword}" -eq 2 ]; then if [ "${prev}" = "-logformat" ]; then COMPREPLY=( $(compgen -W "plain logfmt json" -- "${cur}") ) elif [ "${prev}" = "-loglevel" ]; then COMPREPLY=( $(compgen -W "critical error warning info debug" -- "${cur}") ) elif [ "${prev}" = "-logfile" -o "${prev}" = "-f" ]; then compopt -o filenames COMPREPLY=( $(compgen -f -- "${cur}") ) fi fi } && complete -F _go_apt_cacher go-apt-cacher
これで完成です。
この例はcompgenの-Wと-fしか使いませんでしたが、最終的にCOMPREPLYに候補のリストを渡せば何をしてもかまいません。やり方は様々です。/usr/share/bash-completion/completions/下に良い例がたくさんあるので、探求心の強い方は参考にしてみてください。
補完関数の置き場所
個人的に使うものであれば~/.bash_completionというファイルをつくって書いておけば、ログイン時に自動で読み込まれます。
パッケージなどに含める場合であれば、bash-completionパッケージが使っている/usr/share/bash-completion/completionsなどに置くのが良いでしょう。このパスは、pkg-config --variable=completionsdir bash-completionというコマンドで調べることができます。詳しくはbash-completionのFAQを参照してください。
まとめ
みなさまの快適なBash生活の一助となれば幸いです。
サイボウズではBashやビアバッシュが好きなエンジニアを募集しています。SREの募集要項はこちらです。