Bash/Zsh対応オプションパーサzetoptを公開しました

# 経緯
シェルスクリプトでオプション解析と聞くとまず頭に浮かぶのは`getopt`や`getopts`かと思いますが、[環境によって動作が違ったり制限があったりバグがあったり](http://qiita.com/b4b4r07/items/dcd6be0bb9c9185475bb)で、特に配布スクリプトでの使用は避けたいところです。いろいろ考えると結局自前で解析してしまうのが一番自由で安全という結論になります。
しかし自前解析で`--long=arg`や`-abcd`や`-abcd=arg`のような形式までサポートしようと欲を出し始めると考慮すべきことが途端に増えて複雑なことになってきます。
このあたりをよしなにやってくれるライブラリがなんかあるだろと探してみると、まあないことはないんですが、これだ！と思えるものが見つかりませんでした。ということで自分好みのよく回る車輪が欲しくなって作ってみたのが`zetopt`です。

# zetopt
An option parser for shell scripts
[https://github.com/itmst71/zetopt](https://github.com/itmst71/zetopt)

# 概要
`Bash`関数で実装されたコマンドラインオプションパーサです。ほとんどすべての形式のオプション指定方法に加えサブコマンドもサポートしています。


# Demo
ぐだぐだ書いた後ですが、100文は1コードに如かずだと思うのでまずは以下のデモを見て下さい。
簡単な解説も書いてありますが、とりあえず雰囲気が伝われば十分です。

## Demo-1. フラグオプション
`-v`と`--version`というフラグオプションをペアで定義し、使用されていた場合はバージョン情報を表示します。

```bash
#!/usr/bin/env bash
. /path/to/zetopt.sh        # source zetopt.sh

zetopt init                 # 初期化
zetopt def ver:v:version    # -vと--versionをverというIDで定義
zetopt parse "$@"           # コマンドライン引数をパース

if zetopt isset ver; then   # issetサブコマンドで-vまたは--versionが使用されたかチェック
    echo version 1.0.0      # バージョン情報を表示する
fi
```
```console
$ cmd -v
version 1.0.0
$ cmd --version
version 1.0.0
```
どうでしょう？わりと直感的に理解できるコードではないでしょうか？

## Demo-2. 引数を取るオプション
引数を取るオプション`-r`を定義します。

```bash
#!/usr/bin/env bash
. /path/to/zetopt.sh        # source zetopt.sh

zetopt init                 # 初期化
zetopt def req:r @foo       # -r {foo} を定義。@は必須引数を表す
zetopt parse "$@"           # コマンドライン引数をパース

if zetopt isok req; then    # isokで-rが使用され引数がセットされているかチェック
    zetopt val req foo      # valで-rのパラメータ名fooの値を取得
fi
```
`@foo`の`@`は必須引数を表しています。`foo`は任意で付けることができるパラメータ名です。
`@`を`%`にするとオプショナル引数になります。

```console
$ cmd -r FOO
FOO
$ cmd -r  #オプション引数がないとエラー
zetopt: Error: Missing Required Option Argument(s): -r
```

## Demo-3. サブコマンド
`git remote add <NAME> <URL>`的なサブコマンドのデモです。
サブコマンドに対応してるとなんかかっこいいですよね。

```bash
#!/usr/bin/env bash
. /path/to/zetopt.sh                  # source zetopt.sh

zetopt init                           # 初期化
zetopt def /remote/add/ @NAME @URL    # remote add {NAME} {URL} を定義
zetopt parse "$@"                     # コマンドライン引数をパース

if zetopt isok /remote/add/; then     # remote add の引数がすべてセットされているか
    zetopt val /remote/add/ NAME      # valでパラメータ名NAMEの値を取得
    zetopt val /remote/add/ URL       # valでパラメータ名URLの値を取得
fi
```
```console
$ cmd remote add origin https://github.com/itmst71/zetopt.git
origin
https://github.com/itmst71/zetopt.git
```

`/remote/add/`のように名前空間の概念を利用してサブコマンドを表現します。
実はこれまでのデモでは省略可能な`/`という名前空間にオプションを定義していました。
[Demo-1](#demo-1-フラグオプション)と[Demo-2](#demo-2-引数を取るオプション)のオプション定義の名前空間を省略せずに書くと次のようになります。

```bash
zetopt def /ver:v:version    # -vと--versionをverというIDで定義
zetopt def /req:r @foo       # -r {foo} を定義。@は必須引数を表す
```

## Demo-4. サブコマンドオプション
`git commit -m "first commit"`的なサブコマンドオプションのデモです。

```bash
#!/usr/bin/env bash
. /path/to/zetopt.sh                  # source zetopt.sh

zetopt init                           # 初期化
zetopt def /commit/msg:m @MSG         # commit -m {MSG} を定義
zetopt parse "$@"                     # コマンドライン引数をパース

if zetopt isok /commit/msg; then      # commit -m の引数がセットされているか
    zetopt val /commit/msg MSG        # パラメータ名MSGの値を取得
fi
```
[Demo-2](#Demo-2-引数を取るオプション)の定義に名前空間`/commit/`を付けただけですね。

```console
$ cmd commit -m "first commit"
first commit
```

## Demo-5. 複数引数を取るオプションを複数回使用
複数の引数を取ることができるオプションを複数回使用し、そのすべてのデータに２次元キーを使用してアクセスするデモです。
キーは基本的に`<セッションインデックス>:<引数インデックス>`というフォーマット(e.g. `0:1`)になりますが、ネガティヴインデックス(e.g. `-1 -3`)やメタキャラクタ(e.g. `$ @ ^`)、パラメータ名(e.g. `foo`)、範囲指定(e.g. `0,3`)、またそれらの組み合わせや省略などにも対応しており、詳しく説明し出すと長くなるので詳細は[リファレンス](#リファレンス)を参照して下さい。

```bash
#!/usr/bin/env bash
. /path/to/zetopt.sh                  # source zetopt.sh

zetopt init                           # 初期化
zetopt def opt:o @foo @bar %baz       # -o {foo} {bar} [baz] を定義
zetopt parse "$@"                     # コマンドライン引数をパース

if zetopt isok opt @; then            # すべての-oで引数がセットされているかチェック
    echo "zetopt val opt       :" $(zetopt val opt)        # 最後のセッションのすべての値
    echo "zetopt val opt @     :" $(zetopt val opt @)      # 最後のセッションのすべての値
    echo "zetopt val opt foo   :" $(zetopt val opt foo)    # 最後のセッションのパラメータfooの値
    echo "zetopt val opt 0:0   :" $(zetopt val opt 0:0)    # 0番目のセッションの0番目の値
    echo "zetopt val opt 1:$   :" $(zetopt val opt 1:$)    # 1番目のセッションの最後の値
    echo "zetopt val opt 2:bar :" $(zetopt val opt 2:bar)  # 2番目のセッションのパラメータbarの値
    echo "zetopt val opt @:^   :" $(zetopt val opt @:^)    # 全セッションの最初の値
    echo "zetopt val opt @:@   :" $(zetopt val opt @:@)    # 全セッションのすべての値
fi
```
```console
$ cmd -o A B C -o D E -o F G H
zetopt val opt       : F G H
zetopt val opt @     : F G H
zetopt val opt foo   : F
zetopt val opt 0:0   : A
zetopt val opt 1:$   : E
zetopt val opt 2:bar : G
zetopt val opt @:^   : A D F
zetopt val opt @:@   : A B C D E F G H
```

## Demo-6. -で始まる値を引数とみなすオプション
パラメータ定義の`@`や`%`に`-`を付けて`-@`や`-%`とするだけで`-`で始まる値をオプション引数として受け取ることができるようになります。

```bash
#!/usr/bin/env bash
. /path/to/zetopt.sh        # source zetopt.sh

zetopt init                 # 初期化
zetopt def req:r -@foo      # -r {foo} を定義。-で始まる値もオプション引数とみなす
zetopt parse "$@"           # コマンドライン引数をパース

if zetopt isok req; then    # isokで-rが使用され引数がセットされているかチェック
    zetopt val req foo      # valで-rのパラメータ名fooの値を取得
fi
```

```console
$ cmd -r -100
-100
```

## Demo-7. 可変長引数を取るオプション
パラメータ定義`@`や`%`の末尾に`...`を付けて`@...`や`%...`とするだけで可変長引数を取ることができるようになります。パラメータ名付きの場合は`@foo...`や`%foo...`となります。

```bash
#!/usr/bin/env bash
. /path/to/zetopt.sh        # source zetopt.sh

zetopt init                 # 初期化
zetopt def nums:n %nums...  # -n [nums...] を定義
zetopt parse "$@"           # コマンドライン引数をパース

if zetopt isok nums; then               # isokで-nが正常にパースされたことを確認
    echo Length : $(zetopt len nums)    # lenでセットされた引数の数を取得
    echo Values : $(zetopt val nums @)  # valで-nの引数をすべて取得
fi
```
```console
$ cmd -n 0 1 1 2 3 5 8 13 21 34
Length : 10
Values : 0 1 1 2 3 5 8 13 21 34
$ cmd -n 1 3 7 15
Length : 4
Values : 1 3 7 15
$ cmd -n
Length : 0
Values :
```
`%nums...`を`@nums...`とすれば一つ目だけは必須引数になります。
`-%nums...`とすれば`-`から始まる値も可変長で取ることができますが、後続の意図しない値まで飲み込んでしまう可能性があるので推奨はできません。この場合以下のように`--`やロングオプションがあれば可変長引数のパースを終了させることができます。

```console
$ cmd -n -1 -2 -3 -4 -- arg1 arg2
$ cmd -n -1 -2 -3 -4 --long arg1 arg2
```
ちなみに`--%`と定義すると`--`で始まる値も取ることができます。さらに単体の`--`までも引数として扱いたい特殊な場合は`ZETOPT_CFG_ESCAPE_DOUBLE_HYPHEN=true`を設定して下さい。

## Demo-8. 位置パラメータの定義
実は[Demo-3. サブコマンド](#Demo-3-サブコマンド)の`@NAME`と`@URL`は`/remote/add/`という名前空間に定義した位置パラメータでした。今回は`/`名前空間に定義します。`/`は省略可能なのでパラメータ定義部分だけでも構いません。

```bash
#!/usr/bin/env bash
. /path/to/zetopt.sh         # source zetopt.sh

zetopt init                  # 初期化
zetopt def / @foo @bar @baz  # foo, bar, bazという名前でパラメータを定義
zetopt parse "$@"            # コマンドライン引数をパース

if zetopt isok /; then       # 引数がセットされているかチェック
    echo "foo :" $(zetopt val / foo) #パラメータfooの値を取得
    echo "bar :" $(zetopt val / bar) #パラメータbarの値を取得
    echo "baz :" $(zetopt val / baz) #パラメータbazの値を取得

    for ((i=0; i<${#ZETOPT_ARGS[@]}; i++))
    do
        echo "\${ZETOPT_ARGS[$i]} : " ${ZETOPT_ARGS[$i]}
    done
fi
```
引数は配列変数`ZETOPT_ARGS`に格納されているので、このように直接参照することもできます。

```console
$ cmd A B C D E F
foo : A
bar : B
baz : C
${ZETOPT_ARGS[0]} :  A
${ZETOPT_ARGS[1]} :  B
${ZETOPT_ARGS[2]} :  C
${ZETOPT_ARGS[3]} :  D
${ZETOPT_ARGS[4]} :  E
${ZETOPT_ARGS[5]} :  F
```

Demo 1〜5をひとつにまとめた感じの[demo.shを公開](https://github.com/itmst71/zetopt/blob/master/examples/demo.sh)しています。
デモ中に登場したサブコマンド(`def`とか`isset`とか`val`とか)については[チュートリアル](#チュートリアル)や[リファレンス](#リファレンス)を参照して下さい。

# 特徴
* `Bash`関数として実装
* `Bash 3.2+`, `Zsh 5.0+`サポート
* ショートオプション、ロングオプションをサポート (e.g. `--long`や`-s`)
* ショートオプションとロングオプションをペアで管理 (e.g. `-v`と`--version`)
* `=`によるオプション引数の指定をサポート (e.g. `--long=foo`と`-s=foo`)
* クラスタードショートオプションのサポート (e.g. `-abc`は`-a -b -c`と同じ)
* 最後のクラスタードショートオプションの引数指定をサポート (e.g. `-abc foo -def=bar`)
* `+`タイプのオプションをサポート (e.g. `+o foo` `+exuo=foo`)
* シングルハイフン`-`によるロングオプションのサポート (e.g. `-type f`)
* 実引数後のオプション指定のサポート (e.g. `cmd arg1 arg2 -o --option`)
* 必須オプション引数のサポート (e.g. `-r foo` : "foo"がないとエラー)
* オプショナルオプション引数のサポート (e.g. `-o foo` : "foo"はあってもなくても可)
* 複数オプション引数をサポート (e.g. `-m A B C`)
* オプションの使用回数のカウントをサポート (e.g. `-VVVV`)
* 複数回使用オプションの全引数の保存、取得をサポート(e.g. `-a A -a B`)
* 複数回使用オプションの全引数へアクセスするための２次元キーをサポート (e.g. `0:@`)
* パラメータ名での値取得をサポート (e.g. `zetopt val foo name1 name2`)
* 可変長引数のサポート (e.g. `--fibo 0 1 1 2 3 5 8 13 21 34`)
* `-`で始まる引数の指定をサポート (e.g. `--negative-number -1777771`)
* サブコマンドのサポート (e.g. `cmd remote add <NAME> <URL>`)
* サブコマンドオプションのサポート (e.g. `cmd commit -m "first commit"`)
* グローバルオプションとサブコマンドオプションでのオーバーライドをサポート
* 空白文字や`--`を通常引数として扱うかを環境変数で設定可能
* 内部処理で使用している外部コマンドやそのオプションはすべて`POSIX`の範囲内

# 必要環境
* `Bash 3.2+` / `Zsh 5.0+`
* いくつかの`POSIX`コマンド

# インストール
```
git clone https://github.com/itmst71/zetopt.git
```
自作スクリプトで`zetopt.sh`を`source`すれば`zetopt`コマンド(関数)が使用可能になります。

```bash
#!/usr/bin/env bash
. /path/to/zetopt.sh
```

# 使用方法
## 文法
```
zetopt [-v | --version | -h | --help]
zetopt {SUB-COMMAND} [ARGUMENTS]
```

## 処理の流れ
1. zetopt.sh を source
2. 初期化
3. オプション定義
4. コマンドライン引数のパース
5. パースデータの利用

```bash
#!/usr/bin/env bash
. /path/to/zetopt.sh        # 1. zetopt.sh を source
zetopt init                 # 2. 初期化
zetopt def "ver:v:version"  # 3. オプション定義
zetopt parse "$@"           # 4. コマンドライン引数のパース
if zetopt isset ver; then   # 5. パースデータの利用
    echo version 1.0.0
fi
```

# チュートリアル
すいません現状英語版しか用意できていません。
**[Tutorial](https://github.com/itmst71/zetopt/blob/master/docs/tutorial.md)**

# リファレンス
ごめんなさい現状英語版しか用意できていません。
**[Reference](https://github.com/itmst71/zetopt/blob/master/docs/reference.md)**

# Todo
* ドキュメント整備
* テスト
* サブコマンドの追加
* パフォーマンスの改善
* エラーメッセージの改善

```bash
zetopt def /remote/add/ @NAME @URL
```

```console
$ cmd remote add
zetopt: Error: Missing Required Argument(s): remote add 2 Argument(s) Required  # 現状
zetopt: Error: Missing Required Argument(s): remote add {NAME} {URL}            # 改善案
```

* ヘルプテキスト定義と簡易ヘルプ自動生成機能の実装

以下のようにヘルプテキストを定義できるようにして、

```bash
zetopt def help:h:help     "#Show help and exit."
zetopt def ver:v:version   "#Show version and exit."
if zetopt isset help; then
    zetopt help
    exit 0
fi
```
最終的にはこういう簡易ヘルプを自動生成したい。

```console
$ cmd -h
Name
  cmd -- The awesome command

Description
  The awesome command to show version.

Options
  -h, --help      Show help and exit.
  -v, --version   Show version and exit.
```

# ライセンス
MIT

# お願い
`Zsh`ユーザというわけではないので`Zsh`上での動作はあまり自信がありません。
こういう設定や状況だと動かない等何かあればコメントいただければ幸いです。