fishでのオプション解析の決定版！argparseコマンドの紹介

## 要約
fish 2.7.0でargparseという非常に便利な関数が入ったので，オプション解析にはこれを使おう！

## fishでのこれまでのオプション解析

fish shellの関数でオプション解析するには，これまでは以下のような方法がありました．

- OSが提供するgetoptコマンド (GNU実装 or BSD実装)
- [fisherman/getopts](https://github.com/fisherman/getopts)
- [oh-my-fish/plugin-getopts](https://github.com/fisherman/getopts)
- [oh-my-fish/plugin-argu](https://github.com/oh-my-fish/plugin-argu)
- 自前で解析する

getoptについては以下の記事でも触れられていますが，GNU実装とBSD実装に互換性がない問題があります．

[bash によるオプション解析](https://qiita.com/b4b4r07/items/dcd6be0bb9c9185475bb)

この問題はfishプラグインでも同様で，fisherman/getoptsとoh-my-fish/plugin-getoptsはともに`getopts`というコマンドを提供しますが，コマンド名が被っているのに使い方に互換性がありませんでした（つまり，作った関数を他人に渡すと上手く動かない可能性がある）．

その点，oh-my-fish/plugin-arguは名前被りもなく，使い方も分かりやすくて機能も申し分なかったのですが，ちょっとしたバグがあり特定のオプション[^echo]が解析できませんでした．

[^echo]: 具体的には，echoコマンドのオプションである-E, -e, -n, -sが解析されない．

つまりどの方法にも難点があり，オプション解析の決定的な方法はありませんでした．

しかし，先月(2017/11/27)リリースされたfish2.7.0でオプション解析用の関数`argparse`がビルトインとして入りました．manを見ると「このコマンドはfishのスクリプトや関数が，fishのビルトイン関数が引数を扱うのと100%同じ方法で引数を取り扱うのを簡単にします．」とのこと．

『ビルトイン関数が引数を扱うのと100%同じ方法』で！　いいですね．これからのオプション解析には`argparse`を使うのがよさそうです．

## argparseの使い方

詳細な使い方については，fishにmanが付属しています．[自分用にmanを日本語訳したもの](https://gist.github.com/ryotako/de204fe4564cb3a76b48b247e6c68d42)をGistにあげているので，よければそちらも見てみてください．

ここでは，具体的な例を示しながら`argparse`の使い方を紹介します．

### オプションを指定する

なにかすごいことをするコマンド`sugoi`を定義するとします．とりあえず`-v(--version)`でバージョン情報を表示するには，以下のようにします．

```fish:sugoi.fish
function sugoi

    argparse -n sugoi 'v/version' -- $argv
    or return 1
    
    if set -lq _flag_version
        echo "sugoi, version 0.0"
        return
    end

    # ここで何かすごいことをする
end
```

`argparse`は不正なオプションを受け取った際のエラーメッセージ表示も自動でやってくれるので，エラーメッセージに表示されるコマンド名を`-n(--name)`オプションで指定します．

```fish
$ sugoi -z
sugoi: Unknown option '-z'
~/.config/fish/functions/sugoi.fish (line 3):
in function 'sugoi'
	called on standard input
	with parameter list '-z'
```

`'v/version'`はオプションの仕様で，`-v`および`--version`が有効になります．`sugoi`コマンドに`-v`あるいは`--version`オプションが指定されると`_flag_v`および`_flag_version`というローカル変数が定義されます[^local]．ここでは`set -lq _flag_version`で変数が定義されたか確認してバージョン情報を表示しています[^setl]．

[^local]: 環境にローカル変数を定義するこの挙動はビルトインコマンドならではですね．
[^setl]: `set -q`でよさそうですが，それだとたまたまグローバルスコープに`_flag_version`が存在する場合（どんな場合だ）にも反応してしまうので，ローカル変数にだけ反応するように`set -lq`にしています．

今は`'v/version'`オプションだけですが，オプション仕様はもちろん複数指定できます．オプション仕様を列挙したあと，`--`に続けて解析されるべき`$argv`を置きます．
`--`がオプション仕様と，解析される引数との区切りの役目をします．

`argparse`は解析に失敗すると非ゼロの終了ステータスを返すので，`argparse`文の後に`or return 1`を続けて関数を終了します．

### ロングオプションだけを使う

先程は`-v`あるいは`--version`でバージョン情報を表示しましたが，ロングオプションの`--version`だけを使いたい場合は`v/version`の代わりに`v-version`とします．一応ショートオプションの`v`も書いておく不思議な書式です．

### 引数を取るオプションを使う

`-o(--output)`オプションで，`sugoi`コマンドの出力を指定したファイルに出力できるようにします．オプションの仕様に`=`を続けて`'o/output='`とするとオプションが引数を取るものと扱われます．

```fish:sugoi.fish
function sugoi

    argparse -n sugoi 'v/version' 'o/output=' -- $argv
    or return 1
    
    if set -lq _flag_version
        echo "sugoi, version 0.0"
        return
    end

    set -lq _flag_output
    or set -l _flag_output '/dev/stdout' # デフォルトでは標準出力に吐く

    begin
        echo "何かすごいこと"
    end > $_flag_output

end
```

以下のように動きます．

```fish
$ sugoi -o file1.txt
$ cat file1.txt
何かすごいこと
```

### オプションの値を複数保持する

先程定義した`sugoi`コマンドで，`sugoi -o file1.txt -o file2.txt`のように複数回`-o`オプションを使用すると最後に指定された値`file2.txt`だけが`_flag_output`にセットされます．

これはこれで適切な振る舞いな気がしますが，ここでは景気良く`-o`で指定された全てのファイルに出力するようにしてみます．同じオプションで指定された値を全て保持するには，`o/output=`のかわりに`o/output=+`とします．

```fish:sugoi.fish
function sugoi

    argparse -n sugoi 'v/version' 'o/output=+' -- $argv
    or return 1
    
    if set -lq _flag_version
        echo "sugoi, version 0.0"
    end

    set -lq _flag_output
    or set -l _flag_output '/dev/stdout' # デフォルトでは標準出力に吐く

    for output in $_flag_output
        begin
            echo "何かすごいこと"
        end > $output
    end

end
```

### 同時に指定不可能なオプションを設定する

たとえば，出力をカンマ区切りにする`-c`オプション，タブ区切りにする`-t`オプション，空白区切りにする`-w`オプションを作るとします．これらのオプションはどれか一つしか設定できないはずです．`argparse`では`-x(--exclusive)`オプションで，このようなオプション同士の排他的な関係を指定することができます．

```fish:sugoi.fish
function sugoi

    argparse -n sugoi -x 'c,t,w' \
        'v/version' 'o/output=+' 'c' 't' 'w'  -- $argv
    or return 1

    # 以下省略
```

この状態で`-c`と`-t`を同時に指定するとエラーになります．

```fish
$ sugoi -ct
sugoi: Mutually exclusive flags 'c' and `t` seen
```

## おわりに

`argparse`では他にも色々なことができて，[オプション引数が整数値でなければエラーにする](https://gist.github.com/ryotako/de204fe4564cb3a76b48b247e6c68d42#%E3%83%95%E3%83%A9%E3%82%B0%E3%81%AE%E5%80%A4%E6%A4%9C%E6%9F%BB)，といったこともできるのですが，長くなるので詳しくはmanを御覧ください．

オプション解析までビルトイン関数でできる！ fishは本当にuser-friendlyなシェルですね！