bash-completion で自作コマンドのタブ補完に挑戦
動機
以前にgetpass
というコマンドを用意して、パスワード管理を行える簡単なコマンドラインツールを動かせるようにしました。
$ getpass github id ↵ Success: クリップボードにコピーされました。 $ getpass github pw ↵ Success: クリップボードにコピーされました。
上記が例。これで github のIDやパスワードが得られます。2つの引数はaddpass
という別のコマンドで任意に指定しています。とても手に馴染んでお気に入りなのですが、使い込むうちに不便も感じるようになってきました。キーボードを打つのが面倒くさいのです。
個人のアカウントならば問題ないのですが、プロジェクトなどで用いると問題が浮上しました。
$ getpass project_name backlog_id $ getpass project_name vpn $ getpass project_name gitlab_pw
使用するサービス, ID, PW がたくさんある場合がほとんど、それらを毎度覚えないとならないのです。これは面倒です。学生時代の先輩が「ターミナルでは1文字打つごとにタブを押す!」と言っていたことを思い出し、タブ補完に挑戦することにしました。
タブ補完ことはじめ
以下のサイトが大いに参考になります。何かの間違いで先に私のサイトを訪れてしまった方は、今すぐ参考サイトへどうぞ。
- Bash の補完機能を活用する – いますぐ実践! Linuxシステム管理 / Vol.235
- Bashタブ補完自作入門 – Cybozu Inside Out | サイボウズエンジニアのブログ
- bash なんて書いたことない人が補完関数をとりあえず自作する – Qiita
中心的役割を担うのはcomplete
というコマンド。 bash のビルトインコマンドのようです。
_my_command_completion(){ # do something } complete -F _my_command_completion my_command
タブ補完をさせたいコマンドmy_command
に対して、任意の関数_my_command_completion
を作成して作用させる仕組み。早速触っていきましょう。
Step 1 – とにかくなにか出す
my_command() { echo $@ } _my_command_completion(){ COMPREPLY=(tonikaku nanika); } complete -F _my_command_completion my_command
しょうもないコマンドmy_command
を用意しました。これを使って進めることにします。補完用の関数では、変数COMPREPLY
へ補完したい単語を配列で渡してあげる。これが一番基本の形。
$ my_command <tabを押す> nanika tonikaku
もうこれだけで候補が表示されます。候補はアルファベット順に並べ替えがされるみたいですね。
Step 2 – 補完させる
ここまででは候補を表示するだけで、補完して入力まではしてくれません。そこで次の3つを使います。
compgen |
コマンド。入力内容から絞り込みを行なってくれる。 |
$COMP_WORDS |
変数。補完関数の中で使える。 ターミナルに入力された文字の配列。 |
$COMP_CWORD |
変数。補完関数の中で使える。 今カーソルをあてている単語の順番。 |
まずはcompgen
を単体で使ってみます。completion を generate する、という意味でしょうか。
$ compgen -W "tonikaku nanika" -- t ↵ tonikaku $ compgen -W "tonikaku nanika" -- ton ↵ tonikaku $ compgen -W "tonikaku nanika" -- tonakai ↵ <なにも表示されない>
日本語で言い換えるならば「"tonikaku nanika"
の中から t で始まるものを探して」といったところかな。タブ補完にグッと近づいた気がしてきました。これを補完関数の中に放り込み、かつ現在入力されている文字を受け取れりたいところですが、そこで$COMP_WORDS
と$COMP_CWORD
の出番。
my_command() { echo $@ } _my_command_completion(){ COMPREPLY=( `compgen -W "tonikaku nanika" -- ${COMP_WORDS[COMP_CWORD]}` ); } complete -F _my_command_completion my_command
${COMP_WORDS[COMP_CWORD]}
で「現在入力されている文字」を受け取れます。私はここで少し戸惑い、腑に落ちるまで時間がかかりました。
Step 3 – 候補リストを動的に取得する
続けて、"tonikaku nanika"
の部分を可変にします。
my_command() { echo $@ } _my_command_completion(){ local args=`ls *.html` COMPREPLY=( `compgen -W "$args" -- ${COMP_WORDS[COMP_CWORD]}` ); } complete -F _my_command_completion my_command
別段難しいことはなく、変数にしてあげれば良いですね。タブ補完とは本来的には関係のない話ですらありますね。html ファイルだけを候補にする例を思い浮かびましたが、シェルで出来ることならなんでも良いと思います。
むしろここで私が学んだことは、bashの変数はデフォルトでグルーバル変数だということです。なのでビルトインコマンドのlocal
を使ってあげましょう、ということ。
Step 4 – 引数の順番にあわせて候補リストを変える
今回は1つ目と2つ目の引数で補完させる内容を変えたいです。その時は$COMP_CWORD
で条件分岐をさせればいけそうです。
my_command() { echo $@ } _my_completion_function(){ local args case $COMP_CWORD in 1 ) args=`ls *.html`;; 2 ) args=`ls *.php`;; esac COMPREPLY=( `compgen -W "$args" -- ${COMP_WORDS[COMP_CWORD]}` ); } complete -F _my_completion_function my_command
まったく実用性のない例になってしまいましたがご容赦。
完成させる
一般的なお話はここまで。今回の動機に則したgetpass
用の処理をしていきます。もう完全にタブ補完と関係がありません。json から希望のデータをどうやって得るかという話。
jq の存在は知っていましたが、使わずに済ませたい気分だったので奮闘しました。その結果は次の通り。
{ "project_name_1": { "key_1_1": "value_1_1", "key_1_2": "value_1_2", "key_1_3": "value_1_3" }, "project_name_2": { "key_2_1": "value_2_1", "key_2_2": "value_2_2", "key_3_3": "value_3_3" }, "project_name_3": { "key_3_1": "value_3_1", "key_3_2": "value_3_2", "key_3_3": "value_3_3" } }
_getpass_completion(){ local filepath=/path/to/json.json local args case $COMP_CWORD in 1) args=`grep "{" $filepath|sed -e 's/[{|:|"|]//g'`;; 2) args=`sed -n "/\"${COMP_WORDS[1]}\"\:/,/}/p" $filepath|sed -e '1d' -e '$d' -e 's/:.*$//' -e 's/[ |"]//g'`;; esac COMPREPLY=( `compgen -W "$args" -- ${COMP_WORDS[COMP_CWORD]}` ) } complete -F _getpass_completion getpass
やりたいのは次のこと。
- 第一引数のときは、project_name_1, project_name_2, project_name_3 が欲しい
- 第二引数のときは、第一引数に応じた key_* が欲しい
前者はさらりと済みました。{
を含む行だけを抜き出しました。後者はズブズブと沼にはまり時間がかかりました。awk
を試したりhead
とtail
を組み合わせたり暗中模索。sed
もawk
も、覚えた端から忘れてしまいいつまで経っても身につかないのはなぜでしょう。
最終的に、次のようにすれば、行をまたぎ最短一致で取得できることを知りました。便利。
$ sed -n "/ここから/,/ここまで/p"
終わりに
今回はまるで始めての試みでした。コピペで済ませず、ステップバイステップで進めたことで原理をしっかりと理解できました。おかげで、タブ補完の自作そのものはとても簡単シンプルだとわかりました。どんどん使っていこうと思います。
では素敵なコマンドライフを。