簡単に使えるエレガントなオプション解析ライブラリ（シェルスクリプト用）

続編 → [シェルスクリプト（bash等）の引数解析が究極的に簡単になりました](https://qiita.com/ko1nksm/items/9ee16927b7f8899c4a9e)

以前書いた「[高機能で短いシェルスクリプト用のオプション解析コード（POSIXシェル準拠・独自実装）](https://qiita.com/ko1nksm/items/7d37852b9fc581b1266e)」を元に機能強化しライブラリにしたオプションパーサー「[getoptions](https://github.com/ko1nksm/getoptions)」を作成しました。宣言的なパーサー定義関数を書くだけでオプション解析が行なえます。ループをぐるぐる回してオプションをチェックしていくコードはもう必要ありません。getopt → getopts → gnugetopt → getoptions と名前が長くなるほど高機能になっていると言えば覚えやすいと思います。（gnu-getopt は無理やりですが 笑）


bash 依存なしなので dash を含む全ての POSIX シェルに対応しており、外部コマンドもほぼ使ってない（`cat` のみ、オプション機能であるヘルプの自動生成で使用）ので軽快に動作します。オプションの指定方法はロングオプションを含む一般的によく使われているもの（例 `-abc`, `-vvv`, `-ovalue`, `--long`, `--option=value`, `--` 等）であれば対応しています。（おそらく [POSIX](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html) および [GNU](https://www.gnu.org/prep/standards/html_node/Command_002dLine-Interfaces.html) のオプションの仕様を満たせてるのではないかと。）またエラーメッセージを置き換えたりバリデーションやヘルプの自動生成機能もあります。**2020-11-14追記** ロングオプション名を省略して指定できる機能と、サブコマンドへの対応を追加しました。

細かい使い方は [README.md](https://github.com/ko1nksm/getoptions/blob/master/README.md) 参照ですが、以下の例から直感的にわかるのではないかと思います。

```sh
#!/bin/sh

VERSION=1.0

. ./getoptions.sh # もしくは中身を本体のスクリプトに貼り付け

# オプションパーサーの定義関数
parser_definition() {
  setup   RESTARGS plus:true error:error \
      -- "Usage: ${2##*/} [options] [arguments...]" '' 'getoptions sample' ''
  msg -- 'Options:'
  flag    FLAG_A  -a                                   -- "message a"
  flag    FLAG_B  -b                                   -- "message b"
  flag    FLAG_F  -f +f --{no-}flag                    -- "expands to --flag and --no-flag"
  flag    VERBOSE -v    --verbose counter:true init:=0 -- "e.g. -vvv is verbose level 3"
  param   PARAM   -p    --param                        -- "accepts --param value / --param=value"
  param   NUMBER  -n    --number  validate:number      -- "accepts only a number value"
  option  OPTION  -o    --option  default:"default"    -- "accepts -ovalue / --option=value"
  disp    :usage  -h    --help
  disp    VERSION       --version
}

number() { case $OPTARG in (*[!0-9]*) return 1; esac; }
error() {
  case $2 in
    number) echo "option '$1' is not a number" ;;
    *) return 1 ;; # その他はデフォルトのエラーを表示
  esac
}

# parser_definition 関数を元に オプションパーサーである parse 関数を定義
eval "$(getoptions parser_definition parse "$0")"
# parser_definition 関数を元に ヘルプ表示用の usage 関数を定義（省略可）
eval "$(getoptions_help parser_definition usage "$0")"

parse "$@"               # オプション解析
eval "set -- $RESTARGS"  # オプション以外の引数を取得するために $@ を再設定

echo "$FLAG_A"
printf '%s\n' "$@" # オプション以外の残りの引数
```

上記の例は以下のようなオプションに対応させるためのコードです。

```sh
./sample/basic.sh -ab -f +f --flag --no-flag -vvv -p value -ovalue --option=value 1 2 -- 3 -f
```
## 細かい話

`getoptions` 関数は正確にはオプションパーサーのジェネレータです。`getoptions` 関数で生成したコードを `eval` することでオプションパーサー関数（上記の例では `parse` 関数）を定義します。`eval` せずに `getoptions` 関数単体で実行するとオプションパーサーのコードが出力されます。この生成されたコードのみを使用することも可能で、この場合は` getoptions.sh` は不要となります。

`getoptions.sh` がグローバルに定義する関数は `getoptions` 関数と `getoptions_help` 関数のみです。その他の関数（`setup` 関数や `flag` 関数等）はコマンド置換（`$(...)`）のサブシェル内に閉じ込められるためグローバルを汚しません。また オプションパーサーで使用する変数は `OPTARG` と `OPTIND` のみです。この２つの変数は本来 `getopts` シェルビルトインコマンドで使われる変数ですが `getoptions` を使用するなら `getopts` は不要となるはずなので再利用しています。

`getoptions_help` 関数も同様に `usage` 関数を定義します。このヘルプの自動生成機能はオプション機能です。使い捨てのスクリプト程度であれば十分だと思いますが、出力が気に入らない場合はその他の方法で出力するなり改造するなりしてください。`getoptions_help` 関数は `getoptions` 関数と完全に独立しているので不要な場合は問題なく削除できます。（余談ですがヘルプの見やすさにこだわりたい場合は [docopts](https://github.com/docopt/docopts) のような仕組みを追加したほうが良いと思います。）

## おまけ： ksh93u のバグ

ksh93u で以下のコードがエラーになりハマった・・・（ksh93u 以前及び ksh2020 では問題なし）

```sh
#!/bin/sh
set -eu 1 2
VAR='var'
while [ $# -gt 0 ]; do
  [ $# -eq 1 ] && unset VAR # 最後のループで unset する
  if [ "${VAR+x}" ]; then   # VAR 変数が定義（set）されていたら？
    echo "$VAR"             # 定義されてないのに実行される => エラー「VAR: parameter not set」
  fi
  shift
done
```

`[ "${VAR+x}" ]` で `VAR` 変数が定義されているときだけ `echo` で変数を参照するはずなのですが `unset` しても定義されていると誤判定します。もちろん実際には定義されてないためエラーになります。ワークアラウンドは `eval '[ "${VAR+x}" ]'` とすれば OK です。`eval` することで内部の何かがリセットされるのでしょう。

## さいごに

似たようなツールはすでにあるのではないかと作る前にリサーチしましたが残念ながら見つかりませんでした。見つかったとしても bash 専用だったりしますし。bash 専用に目をつぶったとしても使い方や機能に満足がいくものはありませんでした。

ライセンスは「[Creative Commons Zero v1.0 Universal](https://github.com/ko1nksm/getoptions/blob/master/LICENSE)」にしており（可能な限り）権利を放棄しているので、ライブラリとして使用したりスクリプト内にコピーしたり不要な部分を削除したり改造したりとライセンスのことを一切気にせず自由に使うことができます。

ということで私も自作ツールのオプション解析処理をこれに置き換えていこうと思います。