perlbrew-completionを書いた / またはcompletionスクリプトの開発とデバッグの話
きょうびmakeやgitでも補完が効くなか、perlbrewでも補完が効いてほしいですよね。
たとえば私はマシンによってperlbrewで入れたperlはけっこう違っているのですが、どのマシンにどのバージョンのperlを入れたか正確には覚えていません。なのでperlbrew use [tab]
で利用可能なperlの一覧が出るなどしてほしいところです。
そこでperlbrew-completionを書きました。pull-req済みなのできっと次のバージョンあたりから使えることでしょう。
$ perlbrew [tab] alias install off available install-cpanm self-upgrade clean install-patchperl switch compgen install-perlbrew switch-off display-bashrc lib symlink-executables display-cshrc lib-create uninstall env lib-delete use exec lib-list version help list init mirror $ perlbrew use [tab][tab] # use/switchはinstalled perlsで補完される perl-5.14.2 perl-5.8.9 $ perlbrew use 14[tab] # use/switchは部分マッチで補完される $ perlbrew use perl-5.14.2 # 上記コマンド実行後はこうなる
completionスクリプトの書き方は簡単で、bash関数を定義して内部で$COMPREPLY配列に補完リストを代入するだけです。コマンドの状態は${COMP_WORDS[*]}で引数のリストを、$COMP_CWORDで現在のカーソル位置を引数リストのインデクスとして得られるので、これを使って処理するだけです。completeスクリプトはシェル関数+compgenで補完リストを作るのが普通のようですが、後述する理由により外部スクリプトとして書いたほうが圧倒的に開発が楽なのでperlbrewのサブコマンドとして実装しました。
さて本題ですが、completionスクリプトのデバッグは大変です。シェル関数として実装した場合、該当のスクリプトをいちいちsourceで読み込むのは面倒だし、UIに関するテストなのでユニットテストも難しいです。
そこで、まずcompletionスクリプトを外部スクリプトとして書くことでsourceでの読み込みをしなくていいようにします。今回はperlbrewの内部APIを使いたかったのでperlbrewのサブコマンドとして実装しました。
このようにするとcompletionスクリプトは以下のようにただperlbrew compgen
実行するだけ。これはsouceで最初に読み込んでおきます。
# $ source complete.sh export PERLBREW="command perlbrew" _perlbrew_compgen() { COMPREPLY=( $($PERLBREW compgen $COMP_CWORD ${COMP_WORDS[*]}) ) } complete -F _perlbrew_compgen perlbrew
また開発中いちいちインストールしなくても済むように、perlbrewディストリビューションのディレクトリで環境変数を設定し、lib/App/perlbrew.pmが使われるようにします。
export PERLBREW="perl -I$PWD/lib $PWD/bin/perlbrew"
これでlib/App/perlbrew.pmを編集するだけでcompletionの挙動が変わるようになりました。
しかしまだ問題があります。completionスクリプトは[tab]を押すごとに実行されるので、デバッグログをSTDERRに出すとターミナルが汚れて開発どころではなくなるのです。
そこで、以下のようなログ出力をスクリプトに仕込んだあと別ターミナル$ tail -f bashcomp.log
でログを観察するといいでしょう。もちろん開発用のターミナルでexport PERLBREW_DEBUG_COMPLETION=1
するのも忘れずに。
sub _compgen { my($self, $cur, @args) = @_; if($ENV{PERLBREW_DEBUG_COMPLETION}) { open my $log, '>>', 'bashcomp.log'; print $log "[$$] $cur of [@args]\n"; } ...; }
この状態で補完しようとすると以下のようにログが出ます。
[51931] 1 of [perlbrew] # $ perlbrew [tab] [51932] 1 of [perlbrew u] # $ perlbrew u[tab] [51933] 1 of [perlbrew use] # $ perlbrew use[tab] [51934] 2 of [perlbrew use] # $ perlbrew use [tab]
ちゃんとperlbrew use[tab]
(useのあとにすぐ[tab])とperlbrew use [tab]
(useの後にスペースがあって[tab])がインデクスによって区別できてますね。
completinスクリプトをシェルスクリプトだけで実装しようとするとかなり大変で心が折れそうになりますが、上で紹介した方法だとだいぶ楽に開発できました。
複雑なコマンドにはcompletionスクリプトを添付するとユーザーが楽をできますね。
Enjoy completion!