ドーモ、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の募集要項はこちらです。