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!