fishの補完設定覚え書き

fishの補完の書き方について調べたので，まとめてみました．

bash, zshでは「補完を書く」なんて考えもしませんでしたが，fishだとかなり簡単に書ける印象です．

このエントリでは[hubコマンド](https://github.com/github/hub)の補完を書きながら説明していきます．

(`hub`を選んだのはサブコマンド持ちのコマンドだからで，とくに詳しいわけではないので何か間違いがあったらご指摘ください．`hub`のfish補完には[hub公式の補完](https://github.com/github/hub/blob/master/etc/hub.fish_completion)が既にあり，そちらも参考にしています．)

### 参考にしたサイト等

日本語マニュアル: [全訳！fishシェル普及計画](http://fish.rubikitch.com)
補完についての記事: [fish shell で補完の実装 - complete](http://qiita.com/sosuke/items/4e763afb9e46f82ea9ea)
fish標準添付の補完設定: https://github.com/fish-shell/fish-shell/tree/master/share/completions

# 補完を定義するには？
`~/.config/fish/completions`ディレクトリに`コマンド名.fish`という名前のファイルを置いおくと，補完のたびに設定が読み込まれるようです．

次のような関数を定義しておくと補完設定がすぐに書けて便利かもしれません．

```bash
function ced --description 'edit completion'
	eval "$EDITOR $HOME/.config/fish/completions/$argv[1].fish"
end
```

今の場合は`~/.config/fish/completions/hub.fish`を作成し，その中に補完設定を書いていきます．

# オプションを補完するには?
補完の設定をするには`complete`コマンドを使います．
オプションの設定は`-s (--short-option)`，`-l (--long-option)`， `-o (--old-option)`でできます．

`hub`の場合はオプションに`--help`と`--version`を取るので，以下のようにします．

```bash
complete -c hub -l help -d 'show help'
complete -c hub -l version -d 'print the version'
```
# サブコマンドを補完するには?
`hub`はサブコマンドを使うコマンドなので，それを補完できるようにしてみます．

サブコマンド補完は「入力中の語が，コマンドの第一引数であるとき」の条件付き補完となります．条件付き補完をする際には，条件の判定式を`complete`の`-n (--condition)`オプションに渡します．判定式は`__fish_use_subcommand`という関数が用意されているので，これを使って次のように書けます．

```bash
complete -f -c hub -n '__fish_use_subcommand' -a alias -d "show shell instructions for wrapping git"
complete -f -c hub -n '__fish_use_subcommand' -a browse -d "browse the project on GitHub"
complete -f -c hub -n '__fish_use_subcommand' -a compare -d "lookup commit in GitHub Status API"
complete -f -c hub -n '__fish_use_subcommand' -a create -d "create new repo on GitHub for the current project"
complete -f -c hub -n '__fish_use_subcommand' -a fork -d "fork origin repo on GitHub"
complete -f -c hub -n '__fish_use_subcommand' -a pull-request -d "open a pull request on GitHub"
complete -f -c hub -n '__fish_use_subcommand' -a ci-status -d "display GitHub Status information for a commit"
```

ただ，`__fish_use_subcommand`はマニュアルに記載のない関数なので，将来使えなくなるかもしれません．
判定式を自作する場合は次のようになるかと思います．

```bash
function __fish_is_subcommand
    test 1 -eq (count (string match -v -- '-*' (commandline -poc)))
end

complete -f -c hub -n '__fish_is_subcommand' -a alias -d "show shell instructions for wrapping git"
...
```

`commandline -poc`は入力中のプロセス[^proc](-p)のカーソルが置かれた語の前までの語(-c)を単語に分割して出力(-o)するコマンドです．補完の条件判定でよく使われています．
[^proc]: プロセスはコマンドとその引数からなる文で，パイプや`;`で複数コマンドが組み合わせられる前のものを指します．

補間設定ファイル内で定義された関数は，そのファイル内でだけ有効[^scope]です．
[^scope]: `~/.config/fish/completions`以下の補完設定が自動読込された場合には，関数がグローバルに定義されることはありませんが，`source`で再読込した場合はグローバルな関数になります．

また，`complete`に`-f (--no-files)`オプションが付いているのは，補完候補としてファイル名一覧が表示されるのを抑制するためです．

<img width="522" alt="スクリーンショット 2017-03-19 12.16.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/124405/039001a3-1060-7a29-f912-97e304055570.png">

# サブコマンドのオプションを補完するには？
これも条件付き補完で，判定式は`__fish_seen_subcommand_from`というものが用意されています．`hub`の`alias`サブコマンドは`-s`オプションを取るので，次のようにします．

```bash
complete -x -c hub -n '__fish_seen_subcommand_from alias' -s s -d 'output shell script suitable for eval'
```

`complete`の`-x`オプションは`-f`と`-r`を合わせたものです．
`-r`オプションは「オプション引数を必ず取る」指定で，`hub alias -sh (= hub alias -s -h)`のようにショートオプションを続けて書くような補完がなされなくなります．

つまり`-x`オプションは「他のショートオプションを直に続けて書くことを認めず，オプション引数の補完候補としてファイル名を使わない」となります．

# 補完候補を自分で用意するには？
`hub alias`の`-s`オプションは，シェルの名前を指定すると`hub`に`git`という名前でエイリアスを貼る方法を表示してくれます．そのため`-s`のオプション引数は`bash`, `zsh`, `sh`, `ksh`, `csh`, `fish`のいずれかです．

これらの選択肢を補完候補として登録するには，先程の設定に`-a (--arguments)`オプションを加えて次のようにします．

```bash
complete -x -c hub -n '__fish_seen_subcommand_from alias' -s s -a 'bash zsh sh ksh csh fish' -d 'output shell script suitable for eval'
```

この設定も`-f`オプションがないとファイル名が補完候補に混じることになるのですが，今の場合は`-x`が`-f`を兼ねるので大丈夫です．

# グローバルオプションをサブコマンドのオプション補完候補から除くには？

ここまでで，`alias`サブコマンドに`-s`オプションが補完できるようになりました．いま`hub alias -`と打ってTabを押すと補完候補は`--help`, `-s`, `--version`となります．

`--help`オプションはサブコマンドについても有効ですが，`--version`はそうではありません．そのため先程の補完設定を次のように書き換えておきます．

```bash
complete -c hub -n '__fish_no_arguments' -l version -d 'print the version'
```

# 補完候補をコマンドで生成するには？

次に`browse`サブコマンドの補完を考えます．このサブコマンドは，リポジトリ名を指定するとそれをブラウザで開いてくれます．
で，このリポジトリ名として**[ghq](https://github.com/motemen/ghq)で管理してるリポジトリが補完されたら便利なんじゃないかなー**と思いました．

コマンド(今の場合は`ghq list`)の出力を補完候補とするには，`complete`の`-a`オプションの引数の中にコマンド置換を書いておけばよいです．

```bash
complete -f -c hub -n "__fish_seen_subcommand_from browse; and type -q ghq" -a "(ghq list | string match -r '(?<=github\.com/).+')"
```

# 何番目の引数かによって補完候補を変えるには？
`browse`サブコマンドはリポジトリ名に続けて`wiki`, `commits`, `issues`のようなキーワードを続けることで，リポジトリの該当ページを開いてくれます．

つまり，`browse`サブコマンドは第一引数としてリポジトリ名を，第二引数としてキーワードを取ります．入力中の語が何番目の引数なのか？ を調べる関数[^argnum]を定義すれば次のように補完設定ができます．

```bash
function __fish_is_arg_n --argument-names n
    test $n -eq (count (string match -v -- '-*' (commandline -poc)))
end

complete -f -c hub -n "__fish_seen_subcommand_from browse; and __fish_is_arg_n 2; and type -q ghq" -a "(ghq list | string match -r '(?<=github\.com/).+')"
complete -f -c hub -n '__fish_seen_subcommand_from browse; and __fish_is_arg_n 3' -a 'wiki commits issues tree' 
```

# 他のコマンドから補完設定を引き継ぐには？

最後に`hub`から`git`のコマンドを呼び出す場合に，それも補完できるようにします．これは`complete`コマンドのヘルプにも載っていて，以下のようにします．

```bash
complete -c hub -w git
```

# まとめ

結局，`hub`の補完設定は以下のようになります．
githubでも公開([ryotako/fish-hub-completion](https://github.com/ryotako/fish-hub-completion/blob/master/completions/hub.fish))していますが，最初に述べたように`hub`公式の補完もあるので，そちらを使ってもよいかもしれません．

条件付き補完を使いこなせば，fishでは簡単に強力な補完が書けます．
便利な判定式(`__fish_use_subcommand`とか)がマニュアルに載っていないのが，玉に瑕でしょうか...


```bash
function __fish_is_arg_n --argument-names n
    test $n -eq (count (string match -v -- '-*' (commandline -poc)))
end

# options
complete -c hub -l help -d 'show help'
complete -c hub -n '__fish_no_arguments' -l version -d 'print the version'

# subcommand
complete -f -c hub -n '__fish_use_subcommand' -a alias -d "[hub] show shell instructions for wrapping git"
complete -f -c hub -n '__fish_use_subcommand' -a browse -d "[hub] browse the project on GitHub"
complete -f -c hub -n '__fish_use_subcommand' -a compare -d "[hub] lookup commit in GitHub Status API"
complete -f -c hub -n '__fish_use_subcommand' -a create -d "[hub] create new repo on GitHub for the current project"
complete -f -c hub -n '__fish_use_subcommand' -a fork -d "[hub] fork origin repo on GitHub"
complete -f -c hub -n '__fish_use_subcommand' -a pull-request -d "[hub] open a pull request on GitHub"
complete -f -c hub -n '__fish_use_subcommand' -a ci-status -d "[hub] display GitHub Status information for a commit"

# alias
complete -x -c hub -n '__fish_seen_subcommand_from alias' -s s -a 'bash zsh sh ksh csh fish' -d 'output shell script suitable for eval'

# browse
complete -f -c hub -n '__fish_seen_subcommand_from browse' -s u -d 'print the URL instead of opening it'
complete -f -c hub -n "__fish_seen_subcommand_from browse; and __fish_is_arg_n 2; and type -q ghq" -a "(ghq list | string match -r '(?<=github\.com/).+')"
complete -f -c hub -n '__fish_seen_subcommand_from browse; and __fish_is_arg_n 3' -a 'wiki commits issues tree' 

# compare
complete -f -c hub -n '__fish_seen_subcommand_from compare' -s u -d 'print the URL instead of opening it'

# create
complete -f -c hub -n '__fish_seen_subcommand_from create' -s p -d 'create a private repository'
complete -x -c hub -n '__fish_seen_subcommand_from create' -s d -d 'repository description'
complete -x -c hub -n '__fish_seen_subcommand_from create' -s h -d 'homepage URL'

# fork
complete -f -c hub -n '__fish_seen_subcommand_from fork' -l no-remote

# pull-request
complete -c hub -n ' __fish_seen_subcommand_from pull-request' -s f -d "skip the check for unpushed commits"
complete -x -c hub -n ' __fish_seen_subcommand_from pull-request' -s m -d "use the first line of <MESSAGE> as pull request title, and the rest as pull request description"
complete -r -c hub -n ' __fish_seen_subcommand_from pull-request' -s F -d "read the pull request title and description from <FILE>"
complete -f -c hub -n ' __fish_seen_subcommand_from pull-request' -s o -d "open the new pull request in a web browser"
complete -f -c hub -n ' __fish_seen_subcommand_from pull-request' -s b -d 'the base branch in "[OWNER:]BRANCH" format'
complete -f -c hub -n ' __fish_seen_subcommand_from pull-request' -s h -d 'the head branch in "[OWNER:]BRANCH" format'

# ci-status
complete -f -c hub -n '__fish_seen_subcommand_from ci-status' -s v -d 'report all checks and their URLs'

# git
complete -c hub -w git
```

[^argnum]: 何番目の語なのか？ を調べる関数としては`__fish_is_token_n`がありまが，これはオプションもカウントしてしまします．一方，最初の引数かどうか調べる`__fish_is_first_token`はオプションを無視する実装になっていて，いまひとつ統一感がありません．