1. Qiita
  2. 投稿
  3. Zsh

zshの補完を自作してFabricのタスクを補完する

  • 1
    いいね
  • 0
    コメント

fabしてますか?fab

例えばfab task-name:'arg'の形で実行すると思いますが、タスク名補完したいですよね?うん、したいはず!

デモ

$ fab -l
===========
some header
===========
Available commands:

    fetch   (yyyymmdd = today)
    put
    search  (yyyymmdd, keyword)

-lでタスク名一覧と、書いてあればdocが表示されるので、これをごにょって補完出来る様にしたよ!

$ fab <TAB>
tasks
fetch                  -- (yyyymmdd = today)
search                 -- (yyyymmdd, keyword)
put

普通のコマンドと同じ様に、一意になるまで入力すれば自動で選ばれるし、Shiftで順次選択も当然出来るよ!
docもサブコマンド説明につっこんだよ!

$ fab -f no_doc.py -l
Available commands:

    fetch
    search

$ fab -f no_doc.py <TAB>
tasks
fetch   search

-fでデフォルト以外のfabfileを指定していてもちゃんと補完出来るよ!

$ fab -f <TAB>
fabric file
__init__.py   fabfile.py   no_doc.py

-fの補完はちゃんとファイルになるよ!

便利!!!!!

解説

結構勉強になったのでまじめに解説

参考にしたページを下部に記載し、細かい説明や他の方法の記載はある程度省略します
この手の資料は大量に既にあるので、詳しく知りたい場合はそちらを参照してください

コマンドに対する補完関数を作成して割り当てる

compdef _fab_tasks fab

function _fab_tasks {
    _values 'tasks' 'a' 'b' 'c'
}
  • このxxx.zshファイルをsource xxx.zshコマンドで読み込む
    • これ以降は段階を踏んで書き足していくので、再現してみたい場合は都度sourceを行うこと
  • compdef 関数名 コマンド名で任意のコマンドで<TAB>を押した時に呼び出す関数を設定できる
  • _values 'グループ名' '候補' '候補' ...で補完対象を追加できる
    • グループ名はzstyle ':completion:*' format '%B%d%b'の様にzshrc等で設定されていると表示される
$ fab <TAB>
tasks
a  b  c

補完対象の説明を追加する

    _values 'tasks' 'a[do a]' 'b[delete b]' 'c[search c]'

_valuesで追加する要素に[xxx]を結合すると、それが補完対象の説明となる

$ fab <TAB>
tasks
a  -- do a
b  -- delete b
c  -- search c

これだけ知っていれば後はshellの工夫で候補を作れば良い

オプション等の細かい挙動を決める

補完候補の追加はわかったので、今度は補完全体の挙動を設定する

compdef _fab_complete fab

function _fab_tasks {
    _values 'tasks' 'a[do a]' 'b[delete b]' 'c[search c]'
}

function _fab_complete {
    _arguments \
        '*:tasks:_fab_tasks'
}

様々な設定や条件分岐には_argumentsと言う関数を使う

これはいわばScalaやHaskellのmacth-caseの様なものだと解釈した
行毎にパターン:グループ名(略可):命令(略可)の様な形で記述する

上記のコードだと全てにおいて(*)_fab_tasksを呼び出すので、最初に掲載した挙動とかわらない
compdef_fab_completeに変わった点に注意)

-lオプションを足す

    _arguments \
        -l \
        '*:tasks:_fab_tasks'

これは上記で言うパターン:略:略の形で、この-l \の行だけでfabコマンドの補完可能なオプションに-lを増やすことが出来る
fab -<TAB>の様に-まで入力してあると(一意なので)自動で-lが補完される

ちなみに、一切設定を入れていない場合は-が入力されていてもファイル一覧を探そうとする(何故か何も見つからないけど)

$ fab -<TAB>
`file'

-fオプションを足す

流石に-l \だけじゃあ寂しいので、-fオプションも追加してみる

    _arguments \
        -l \
        -f \
        '*:tasks:_fab_tasks'
$ fab -<TAB>
option
-f  -l

optionと言うグループ名で候補が選べる様になった
ここからもう少し改良を続けてみる

オプションにも説明を足す

なんとなくお察しかもしれないが、オプションも説明は[xxx]で設定することが出来る

    _arguments \
        -l'[list]' \
        -f'[fabfile]' \
        '*:tasks:_fab_tasks'
$ fab -<TAB>
option
-f  -- fabfile
-l  -- list

--list--fileを足す

-l--listどちらでも同じ挙動をする様にしてみる

    _arguments \
        {-l,--list}'[list]' \
        {-f,--file}'[fabfile]' \
        '*:tasks:_fab_tasks'
$ fab -<TAB>
option
--file  -f  -- fabfile                                                                                                                                                                                    
--list  -l  -- list    

複数のオプションが同じ説明で並んでいるので良さそう

だが、実はちょっと残念な点がある
-l--listの両方を重複して入力できてしまうのだ

$ fab -l -<TAB>
option
--file  -f  -- fabfile                                                                                                                                                                                    
--list      -- list                                                                                                                                                                                       

$ fab -l --file -<TAB>
option
--list  -- list
-f      -- fabfile

2度は指定しないよね

排他制御をする

パターンの前に'(x, y)'の形で排他指定をすることが出来る

    _arguments \
        '(- *)'{-l,--list}'[list]' \
        '(-f --file)'{-f,--file}'[fabfile]' \
        '*:tasks:_fab_tasks'
$ fab -<TAB>
option
--file  -f  -- fabfile                                                                                                                                                                                    
--list  -l  -- list                                                                                                                                                                                       

補完候補は今まで通りだけど

$ fab -f -<TAB>
option
--list  -l  -- list                                                                                                                                                                                       

-fが入力済みの場合は--fileは候補にならない

また、(- *)の様にすることで、そのオプション以降は何も補完されなくすることも可能
今回の--list系のオプションや、ヘルプで有効だ

$ fab -l <TAB>
no more arguments

$ fab -f -l <TAB>
no more arguments

オプション毎に次に補完させたい候補を分ける

パターンに続くグループ名(略可):命令(略可)の部分を記述すると、そのオプションの次の候補を任意に設定できる
-fには_filesを設定してみよう

    _arguments \
        '(- *)'{-l,--list}'[list]' \
        '(-f --file)'{-f,--file}'[fabfile]:fabric files:_files' \
        '*:tasks:_fab_tasks'
$ fab -f <TAB>
fabric files
__init__.py  __init__.pyc  fabfile.py  fabfile.pyc  no_doc.py  no_doc.pyc

グループ名がfabric filesで候補がファイル一覧になっている
ちなみに他にも_users等もあるので、気になったら調べてみるとおもしろいかも

オプション等に関してはここまで

-f fabfileに応じて補完候補を動的に切り換える

最後に少しだけ、fabricの話に戻ります

基本はfab -lの結果をパースすれば良いんだけど、一点だけどう実装するべきかわからないことがあってBUFFERという変数を使ってみた
困った点はfab -lfab -f another_fabfile.py -lの結果が違う点で、
BUFFERという変数は現在のコマンドラインに入力されている行そのものが入っている

    fabfile=`echo $BUFFER | awk '{for(i=1; i <= NF; i++) if($i == "-f") print $(i + 1) }'`

    if [ -n "$fabfile" ]; then list=`fab -f $fabfile --list`; else list=`fab --list`; fi

こんな感じで現在入力中の-fの状態によって-lの結果を動的に得ることにした
-fの次がanother_fabfile.pyであることは、先述の「-fの次はfabfileを補完する」という設定である程度保証している)

完成形

compdef _fab_complete fab
function _fab_tasks {
    in_header=1
    tasks=()
    IFS_BK=$IFS
    IFS=$'\n'

    fabfile=`echo $BUFFER | awk '{for(i=1; i <= NF; i++) if($i == "-f") print $(i + 1) }'`

    if [ -n "$fabfile" ]; then list=`fab -f $fabfile --list`; else list=`fab --list`; fi

    while read line
    do
        if [[ $line = '' ]] ;then
            continue
        fi
        if [[ $in_header -ne 1 ]] ;then
            tasks+=(`echo $line | awk '{printf("%s", $1); $1=""; if ($0 != "") printf("[%s]", $0)}' | sed -e 's/\[ /\[/g'`)
        fi
        if [[ $line =~ 'Available commands:' ]]; then
            in_header=0
        fi
    done <<END
$list
END

    _values 'tasks' $tasks
    IFS=$IFS_BK
}

function _fab_complete {
    _arguments \
        '(- *)'{-l,--list}'[print list of possible commands and exit]' \
        '(-f --file)'{-f,--file}"[python module file to import, e.g. '../other.py']:fabric file:_files" \
        '*:tasks:_fab_tasks'
}

shellを書き慣れていないので多分イケてないコードだろうし、すごい時間かかったけど、挙動と得た知識には大変満足している

参考資料

今回参考にしたページ
読んで得た知識を抜粋して箇条書きで掲載する