初めに
ghq というコマンドがある。GitHub のリポジトリをクローンしていい感じに管理するコマンドだ。
これには zsh 補完ファイルも含まれているんだけど、補完できないオプションあったりして、いまいちだった。なのでそれを直すことにした。
何日か前に修正して Pull Request を送ったら無事取り込まれたので、どういう感じで修正したのか、その過程を紹介してみる。補完関数の書き方が分からない人でも雰囲気は伝わると思うので参考にしてみてほしい。
ちなみに、ghq 自体の詳しい使い方は作った人の紹介記事を見るのが良いと思う。
どこを直すか
まず、ghq コマンドの基本的な使い方はこんな感じ。
# GitHub の mollifier/config リポジトリをローカルに clone する
% ghq get mollifier/config
# clone したリポジトリを一覧表示する
% ghq list
github.com/mollifier/config
# clone したリポジトリの場所に移動(cd)する
% ghq look mollifier/config
# help でヘルプメッセージを表示する
% ghq help
NAME:
ghq - Manage GitHub repository clones
COMMANDS:
get Clone/sync with a remote repository
list List local repositories
...
コマンド自体の使い方が分からないと書けないので、あらかじめ確認しておく。特にヘルプメッセージが大事なのでよく読んでおく。
それで、元の補完ファイルの内容はこうだった。
#compdef ghq
function _ghq () {
_arguments -C \
'1:: :__ghq_commands' \
'2:: :->args' \
&& return 0
case $state in
(args)
case $line[1] in
(look)
_repos=( ${(f)"$(ghq list --unique)":gs/:/\\:/} )
_describe Repositories _repos
;;
esac
esac
return 1
}
__ghq_commands () {
_c=(
${(f)"$(ghq help | perl -nle 'print qq(${1}[$2]) if /^COMMANDS:/.../^\S/ and /^ (\w+)(?:, \w+)?\t+(.+)/')"}
)
_values Commands $_c
}
compdef _ghq ghq
この中で特に直したほうが良い点は以下の2つ。
1. 補完できないオプションがある
ghq は --help
や --version
というオプションを付けることができるんだけど、それに対応できていない。それに、例えば ghq list
は -p
や -e
オプションを付けれたりとかサブコマンドにさらにオプションを付けれるんだけど、それにも対応できていない。せっかくなので全部のオプションを補完できるように直すことにした。
2. 無駄なグローバル変数が使われている
__ghq_commands
関数の中で _c
って変数を使ってるんだけど、これがグローバル変数になっている。なのでこれで補完した後に echo $_c
とかすると _c
変数の内容が表示される。あと _arguments
って関数を呼び出してるんだけど、実はこいつも中でグローバル変数を使ってる。グローバル変数を使うと他のスクリプトと衝突したりするかもしれないので、これを修正する。
直す
それでは実際に修正作業に入ってみる。
まずはいろいろと直すところがあって、元のコードが残ってるとややこしいので一旦中身を全部消す。
#compdef ghq
そして、基本的な枠組みを書く。
#compdef ghq
function _ghq () {
local context curcontext=$curcontext state line
declare -A opt_args
local ret=1
_arguments -C \
'*:: :->args' \
&& ret=0
case $state in
(args)
# TODO
esac
return ret
}
_ghq "$@"
変数を local
で宣言してるけど、これは _arguments
関数が中で使ってる変数をローカル変数にするために必要になる。こうしておくと変数の影響範囲がこの関数内だけになる。
今回のようにサブコマンドを引数にとるコマンドの場合は基本的にこういう形で書くので、次に似たようなのを書くときはこれをコピペすれば早いと思う。
それでは具体的に補完の中身を書いていく。まずは全体で使用可能なオプションとサブコマンドの補完から。
function _ghq () {
local context curcontext=$curcontext state line
declare -A opt_args
local ret=1
_arguments -C \
'(-h --help)'{-h,--help}'[show help]' \
'(-v --version)'{-v,--version}'[print the version]' \
'1: :__ghq_commands' \
'*:: :->args' \
&& ret=0
case $state in
(args)
# TODO
esac
return ret
}
__ghq_commands () {
# TODO
}
サブコマンド名の補完は __ghq_commands
関数で受けることにした(修正前と同じ名前)。こんな感じで中身を書く。
__ghq_commands () {
local -a _c
_c=(
'get:Clone/sync with a remote repository'
'list:List local repositories'
'look:Look into a local repository'
'import:Import repositories from other web services'
'help:Shows a list of commands or help for one command'
)
_describe -t commands Commands _c
}
local -a _c
として、ローカル変数として宣言してる。これで _c
変数のスコープがこの関数内だけになる。
この時点で --help
とかのオプションと、ghq list
とかのサブコマンドが補完できるようになった。ここまではいい感じ。
その次にサブコマンド以降の補完を書く。
_arguments
のところに ->args
って書いてるけど、ここが大事。
_arguments -C \
# ...
'*:: :->args' \
&& ret=0
こうしておくと state
変数に args
という値を入れてた状態で次に進む、という動きになる。(args という名前は別に何でも良い)
今回は状態を1つしか指定していないけど、2つ以上指定することもできる。例えば2つ指定する場合の例は以下。
_arguments -C \
'1:: :->files' \
'*:: :->args' \
&& ret=0
case $state in
(files)
# ->files の場合
;;
(args)
# ->args の場合
;;
esac
}
今回は1つで良いのでargsだけにする。args に進んだ時は $words[1]
変数にサブコマンド名が入っているので分岐を書く。
function _ghq () {
local context curcontext=$curcontext state line
declare -A opt_args
local ret=1
_arguments -C \
'(-h --help)'{-h,--help}'[show help]' \
'(-v --version)'{-v,--version}'[print the version]' \
'1: :__ghq_commands' \
'*:: :->args' \
&& ret=0
case $state in
(args)
case $words[1] in
(get)
# TODO
;;
(list)
# TODO
;;
(look)
# TODO
;;
(import)
# TODO
;;
(help|h)
# TODO
;;
esac
;;
esac
return ret
}
それで、ここがポイントなんだけど、_arguments
関数を使っていて ->
で次の状態に進んだ時はメインのコマンド名(ghq)とかいらない部分が削られた状態になっている。つまり、ghq
のことは一旦忘れて get
とか list
という名前のコマンドを補完しているつもりで改めて _arguments
を使って補完を書ける。
例えば ghq get
以下の補完はこう書く。
function _ghq () {
case $state in
(args)
case $words[1] in
(get)
_arguments -C \
'(-u --update)'{-u,--update}'[Update local repository if cloned already]' \
'(-)*:: :->null_state' \
&& ret=0
;;
(list)
# TODO
;;
(look)
# TODO
;;
(import)
# TODO
;;
(help|h)
# TODO
;;
esac
;;
esac
}
これで ghq get
の後に -u
と --update
オプションが補完できるようになる。
それ以外のサブコマンドについても同じように書いていく。
function _ghq () {
case $state in
(args)
case $words[1] in
(get)
_arguments -C \
'(-u --update)'{-u,--update}'[Update local repository if cloned already]' \
'(-)*:: :->null_state' \
&& ret=0
;;
(list)
_arguments -C \
'(-e --exact)'{-e,--exact}'[Perform an exact match]' \
'(-p --full-path)'{-p,--full-path}'[Print full paths]' \
'--unique[Print unique subpaths]' \
'(-)*:: :->null_state' \
&& ret=0
;;
(look)
_arguments -C \
'1: :__ghq_repositories' \
&& ret=0
;;
(import)
_arguments -C \
'(-u)-u[]' \
'(- :)*: :(starred pocket)' \
&& ret=0
;;
(help|h)
__ghq_commands && ret=0
;;
esac
;;
esac
}
__ghq_repositories () {
# ghq look の後にここが呼び出される
# TODO
}
ghq help
の後はサブコマンドを補完できるんだけど、それは __ghq_commands
として関数にしてたのでそれを呼びだせば OK。(このために関数にしてた)
look
の引数としてはローカルに clone しているリポジトリ一覧を補完する。こういう外部のコマンドを呼んだりするようなまとまった処理は別の関数として定義したほうがよい。今回は __ghq_repositories
という名前で関数を作った。
__ghq_repositories () {
local -a _repos
_repos=( ${(@f)"$(_call_program repositories ghq list --unique)"} )
_describe -t repositories Repositories _repos
}
これでサブコマンドのオプションも全部補完できるようになった。最終的な出来上がりは以下の通り。
#compdef ghq
function _ghq () {
local context curcontext=$curcontext state line
declare -A opt_args
local ret=1
_arguments -C \
'(-h --help)'{-h,--help}'[show help]' \
'(-v --version)'{-v,--version}'[print the version]' \
'1: :__ghq_commands' \
'*:: :->args' \
&& ret=0
case $state in
(args)
case $words[1] in
(get)
_arguments -C \
'(-u --update)'{-u,--update}'[Update local repository if cloned already]' \
'(-)*:: :->null_state' \
&& ret=0
;;
(list)
_arguments -C \
'(-e --exact)'{-e,--exact}'[Perform an exact match]' \
'(-p --full-path)'{-p,--full-path}'[Print full paths]' \
'--unique[Print unique subpaths]' \
'(-)*:: :->null_state' \
&& ret=0
;;
(look)
_arguments -C \
'1: :__ghq_repositories' \
&& ret=0
;;
(import)
_arguments -C \
'(-u)-u[]' \
'(- :)*: :(starred pocket)' \
&& ret=0
;;
(help|h)
__ghq_commands && ret=0
;;
esac
;;
esac
return ret
}
__ghq_repositories () {
local -a _repos
_repos=( ${(@f)"$(_call_program repositories ghq list --unique)"} )
_describe -t repositories Repositories _repos
}
__ghq_commands () {
local -a _c
_c=(
'get:Clone/sync with a remote repository'
'list:List local repositories'
'look:Look into a local repository'
'import:Import repositories from other web services'
'help:Shows a list of commands or help for one command'
)
_describe -t commands Commands _c
}
_ghq "$@"
最後に
というわけで、無事修正できた。さくさく補完できていい感じ。
今回は基本的な補完関数の書き方とか _arguments
関数の使い方なんかは説明しなかったんだけど、なんとなく流れはつかめたと思う。
これを機に自分でも書いてみようという人は次の記事とかが参考になる。
「zsh の本」という本は補完についてすごく詳しく書いてあるので役に立つ。
あと、補完関数を書いて直してというのを繰り返してる時に簡単に再読み込みできるプラグインを以前作った。
これがあると確認しやすいので使ってみるといいと思う。
zsh の補完関数はいろいろ分かりにくいし決して書きやすいとは言えないんだけど、やっぱりちゃんと書くと便利になるので、興味を持った人はぜひ挑戦してみてください。