Fishの補完をコマンドラインの内容に応じて変える

by
カテゴリ:

Fishで補完を定義するとき、コマンドに指定された引数によって補完候補を変えたいことがあります。たとえばメインコマンドの直後だったらサブコマンドを補完したい、--input-fileの後だったらファイル名を補完したいとかいうことありますよね。

私はtaskコマンドの--globalオプションでこのようなニーズを感じました。 taskコマンドはTaskfileというプロジェクトが提供するタスクランナーのコマンドです。

GitでMakefileを管理している場合、個人的によく使うタスクの登録先がなくて困るのですが、Taskfileはタスク管理用のYamlの名前を複数パターン持つことでこの問題を解決しているので好きです。

としておくと、taskコマンドはTaskfile.ymlを優先するので、Taskfile.dist.ymlを編集せずに個人用のタスクを定義できます。また、Taskfile.ymlで以下のようにしておくと、Taskfile.dist.ymlのタスクも継承できます。便利。

version: '3'

includes:
  dist:
    taskfile: ./Taskfile.dist.yml
    flatten: true

Taskfileのもう一つ面白いところは、--globalという引数を指定すると、~/.config/Taskfile.ymlを参照してくれるところです。よく実行するコマンドなんだけど、実行ファイルを容易するまででもないなあというケースに便利。

ところが困ったことに、Taskfileが提供するtaskコマンドの補完定義は--globalオプションを考慮していませんでした。このためtask --globalをしていても、カレントディレクトリのTaskfile.ymlのタスクを補完してしまいます。

こんなんとき、fishでは組込みのcommandlineコマンドでコマンドラインの内容を取得できるので、taskコマンドに--globalが指定されているか調べればOKです。

コマンドラインの取得範囲は引数で変更できます。

ざっくり、以下のような感じです。

echo hi; echo bye | cat --number
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -b, --current-buffer
         ^^^^^^^^^^^^^^^^^^^^^^^ -j, --current-job
                    ^^^^^^^^^^^^ -p, --current-process
                        ^^^^^^^^ -t, --current-token

直近の引数が--globalかどうか知りたいだけなら-tで十分そうです。しかし、task --global --dryのように--globalが途中にある場合は-tでは不足で、-pが妥当そうです。

間違っても-b-jをしてはいけません。以下のようなコマンドがあった場合に--globaltaskに指定されているものか検証する手間が発生します。

echo --global | task

commandline -pで取得した結果は1つの文字列なので、トークン単位に分割してから、--globalを含むか調べる必要があります。単純にスペース単位で分割すると、以下のようなコマンドから--globalを誤認識してしまいます。

task --dir "/temp/fo --global x/bar"

このような時、fishの組込みのreadコマンドが便利です。 --tokenize --listオプションを指定すると、トークン単位で分割した結果を変数に保存できます。

echo "--global 'foo --global bar'" | read --tokenize --list --local tokens
for token in $tokens
    echo "#> $token"
end
#> --global
#> foo --global bar

というわけで、commandline -pread --tokenize --listを組み合わせると以下のように、--globalの指定を検証できます。

commandline -p | read --tokenize --list --local tokens
for token in $tokens
    if test "_$token" = "_--global"
        echo "global option is specified"
    end
end

あとは条件分岐して補完候補を変えるだけです。

実際の例は以下のPRにあります。もしよければご覧ください。

https://github.com/go-task/task/pull/2134

ENJOY

余談ですが、初期実装はめちゃくちゃ危険なことをしていました。

set --local cmdline (commandline --current-buffer)
eval "set --local tokens $cmdline"

この方法では、cmdlineが”echo hi; task”のような文字列だった場合に、evalの内容が以下のようになり、変数定義に加えてtaskコマンドを実行してしまいます。

set --local tokens echo hi; task

Taskfile.ymlにデフォルトタスクが定義されている場合、taskコマンド単体で実行できる仕様なので、いったい何が起きるか分かりません。こわいですね。

幸い、デフォルトタスクが定義されていない状況で実験したので、以下のようなエラーメッセージを見るだけで済みました。

task: No tasks with description available. Try –list-all to list all tasks task: Task “default” does not exist