sankantsuのブログ

技術メモ・競プロなど

bash の TAB補完 をカスタマイズする

概要

bash はコマンド名や引数の補完機能を備えており,入力中に TAB キーを押すことで候補を表示したり残りの部分を入力したりできる.

ただし,デフォルトであらゆる状況に対して適切な補完が効くわけではないので,文脈に応じて補完候補を自分でカスタマイズしたいという状況が生じる. このようなとき,bash に備わっている programmable completion という機構を使うことで好きなように補完候補を指定することができる.

本記事では,lpコマンド(プリンタに印刷命令を発行するコマンド)を題材に自作の補完を実装してみた.

補完のしくみ

bash では補完を行う際,事前に設定されたシェルスクリプト関数を呼び出す. このシェルスクリプト関数はコマンド名や入力中の引数を受け取って解析し,文脈に応じて適切な補完候補を生成する.

bash-completionというパッケージで便利な補完がかなりいろいろと定義されており,たいていはインストールされて使えるようになっているはずである.

GitHub - scop/bash-completion: Programmable completion functions for bash

もし入っていれば,/usr/share/bash-completionなどに補完用の関数の定義がある(~/.bashrc内にこれらのファイルを読み込む設定があるはずである).

現在定義されている補完はcomplete -pで確認できる.bash-completionが入っていれば次のような表示が出るはずである.

complete -F _longopt mv
complete -F _root_command gksudo
complete -F _command nice
complete -F _longopt tr
complete -F _longopt head
...

この出力はそのまま bash で実行できるような形で出力されており,complete -F <function-name> <command-name><command-name>というコマンドの引数の補完方法に<function-name>というシェルスクリプト関数を指定するという意味になる.

bash-completionはかなり便利だがそれでもパターンが網羅されているわけではない. 自分で補完候補を定義したい場合は,自分でシェルスクリプト関数を書いて補完用の関数として指定すれば良い.

自作の補完関数は,~/.bash_completionに保存しておけばbash-completionが勝手に読み込んでくれる.

簡単な例

_dummy() {
    COMPREPLY=( one two )
}
complete -F _dummy dummy

上の内容をdummyなどの名前でファイルに保存し,source dummyなどで定義内容を読み込む. ここでdummy <TAB>などと入力すると,one twoと補完候補が表示されるのがわかる.

_dummy は補完用の関数で,COMPREPLYという配列変数に補完候補を設定する. complete -F _dummy dummydummyというコマンドに対する引数の補完方法として_dummy()を使うように指定する.

補完用の関数名は _ + コマンド名 とするのが通例となっているようである.

入力引数の取得

実際の状況では,特定のオプションの直後にオプションの設定値の候補を補完するなど現在入力中の引数に依存したより複雑な補完が要求される.

このために入力中の引数の数や引数の中身を取得するための _get_comp_words_by_ref という関数がbash-completionで用意されている. これを使うには,次のように書けばよい.

local cur prev cword
_get_comp_words_by_ref -n : cur prev cword

localというのは関数のローカル変数を定義するための bash のキーワードである. _get_comp_words_by_ref -n : cur prev cwordという書き方で,それぞれ

  • 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}
}
complete -F _dummy dummy

ここでdummy foo barと入力した段階で TAB キーで補完を試みると,各変数の内容は

  • cur: "bar"
  • prev: "foo"
  • cword: "2"

のように設定される.

補完候補の絞り込み

入力途中の文字列にマッチする候補だけに絞りこむには compgen という組み込み関数が使える.

comgen <option> <word>

という形で,オプションで指定した方法で生成された補完候補のうち先頭がwordにマッチするもののリストを生成する.

例えば,-W <word list>という空白区切りの文字列を補完候補とするようなオプションを使って,

compgen -W "foo bar baz" ba

と実行すると,foo,bar,bazのうちbaから始まるbarbazが補完候補として生成される.

典型的には,COMPREPLYの値にcompgenで生成したリストを設定することで候補の絞り込みを実現する.

実例: lp コマンドでプリンタ名の補完

lp コマンドは,プリンタに印刷命令を発行するコマンドで次のように使う.

lp <filename>

-d <printer-name>というオプションでどのプリンタを使うか指定できるのだが,デフォルトでは適切に補完が効かない. しかし,プリンタの型番など人間が覚えているはずがないので,なんとか補完候補を表示してほしいものである.

そこで,実装したのが次の関数である.

_lp() {
    local cur prev cword
    _get_comp_words_by_ref -n : cur prev cword
    if [[ "$prev" == "-d" ]]; then
        # generate available printer list
        local printers=$(lpstat -e | tr '\n' ' ')
        COMPREPLY=( $(compgen -W "${printers}" -- ${cur}) )
    else
        COMPREPLY=( $(compgen -f -- ${cur}) )
    fi
}
complete -F _lp lp

_get_comp_words_by_refで引数を取得する. 直前の引数を見て,-dなら現在の補完候補としてプリンタ名のリストを生成する(lpstat -eで取得できる). tr '\n' ' 'というのは改行文字を空白文字に変換してcompgen -Wの引数として使えるようにするための処理である.

-dオプション以外のときは,compgen -fでファイル名を補完候補として使う.

たったこれだけのことだが結構快適になるものである. 困ったときはぜひ自分で補完を定義するのに挑戦してみてほしい.

参考

下の記事は内容が充実している.

blog.cybozu.io

公式な資料としてはman bashの Programmable Completion というセクションやman bash-builtinscompletecompgenの項目を見ると良い.

実装例をいろいろと見たい場合には,/usr/share/bash-completion以下にいろいろと置かれている.