高機能で短いシェルスクリプト用のオプション解析コード（POSIXシェル準拠・独自実装）

## はじめに

**2020-08-12追記** この記事の内容を元に「[getoptions](https://github.com/ko1nksm/getoptions)」というライブラリを作りました。→「[簡単に使えるエレガントなオプション解析ライブラリ（シェルスクリプト用）](https://qiita.com/ko1nksm/items/1bcc49bef56a5c245251)」

シェルスクリプト用のオプション解析コードのサンプルは検索するといくつも見つかるのですが、機能が不足していたり雛形とするには長くメンテナンスしづらいものばかりだったので作成しました。以下のような特徴があります。

* POSIXシェル準拠なので bash 以外でも動作可能（dash, zsh, ksh, mksh, yash 等で動作確認）
* ロングオプションにも対応（`getopt`, `getopts` 未使用）
* オプションとオプション以外の引数の順番を混在させることが可能
* 単一の `-` をオプションではなく引数として扱う
* オプション解析を打ち切る `--` に対応
* 結合されたショートオプションに対応（例 `-abc`
* 省略可能なオプション引数に対応（例 `-ovalue`, `--option=value`）
* フラグを反転させるオプションに対応（例 `+o`, `--no-option`）
* 同じオプションが複数回指定された場合の対応
* 外部コマンドを呼び出していないので高速
* 必要な機能のみ使えるように段階的に実装
* コードが短くメンテナンスしやすい
* [shellcheck](https://github.com/koalaman/shellcheck) によるチェックと [shellspec](https://github.com/shellspec/shellspec) を使ったテストコードあり

ちなみに独自実装しているのは `getopt` がロングオプション対応してるのが GNU 版だけで macOS では動かないからです。`getopt`（と `getopts`）を使用せずに、十分に置き換え可能な機能を実装しています。また本質的なコードよりもオプション解析コードの方が長いなんて事にならないよう短く仕上げています。そのため少し変わったコーディングスタイルになってますので気に入らない人は好きに書き換えてください。雛形（サンプルコード）として作成してるので自由に変更して使ってもらって構いません。

実際に動作するサンプルコードは [こちら](https://github.com/ko1nksm/optionsh) です。参考ですが `getopt`, `getopts` の詳細な解説は「[シェルスクリプト オプション解析 徹底解説 (getopt / getopts)](https://qiita.com/ko1nksm/items/cea7e7cfdc9e25432bab)」で記事にしています。

※ これまで自分が書いていたコードをベースに機能追加して新たに作成したものなのでバグがあるかもしれません。使用する場合は十分確認をお願いします。

**サンプルコード一覧**

1. オプション引数が必要ない場合
2. オプション引数に対応する
3. オプションとオプション以外の引数の順番の混在に対応する
4. -- でオプション解析処理を打ち切る
5. 結合されたショートオプションに対応する
6. 結合されたオプション引数に対応する
7. 省略可能なオプション引数に対応する
8. +, --no- でフラグを反転させる
9. 同じオプションが複数回指定された場合の対応

上から解説を加えつつ機能を実装しています。使用する際は全てを実装する必要はなく必要だと思う所までで十分です。殆どの場合 4. まで実装すれば必要十分だと思います。5. 以降を実装すると GNU 版の `getopt` と比べても遜色がない機能になりますがコードの量が増えます。解説の都合上、一度定義した関数は省略しています。例えば `unknown` 関数は 1. で定義しています。

## 1. オプション引数が必要ない場合

全ての引数がオプションまたはオプション以外の引数で、オプション引数が必要なければ `for` を使って簡潔に書くことができます。

```sh
abort() { echo "$*" >&2; exit 1; }
unknown() { abort "unrecognized option '$1'"; }

FLAG_A='' FLAG_B=''

for arg; do
  case $arg in
    -a | --flag-a) FLAG_A=1 ;;
    -b | --flag-b) FLAG_B=1 ;;
    -?*) unknown "$@" ;;
    *) set -- "$@" "$arg"
  esac
  shift
done
```

`FLAG_A` 変数は、フラグが立っているときは `1` 立っていないときは空文字になります。`0` でないのは `[ "$FLAG_A" ]` という短い書き方でフラグを判定することができるようにするためです。例では変数は空文字で初期化していますが、任意の値で初期化するとオプションを省略したときのデフォルト値となります。

このコードはオプションを環境変数に設定し、オプション以外の引数を位置パラメータ `$@` に残します。多少分かりづらいと思うので説明します。

1. まず `for arg in "$@"` により `$@` がループの要素リストとしてメモリに保持されます。
2. この状態で `for` ループの中で `shift` を行っても、メモリに保持された方の `$@` には影響がありません。<br>
   言い換えると、引数が5個ある場合、`for` ループの中で何度 `shift` を行っても5回ループします。
3. ループの中にある `shift` により、ループする前に `$@` にあった引数はすべてなくなります。
4. しかし、ループの中でオプション以外の引数を `set -- "$@" "$arg"` で `$@` に追加しています。
5. つまり、最初あった引数はなくなりますが、新たに追加した引数が `$@` に残ります。

`for` を使うのはこのパターンだけです。なぜなら例えば `--flag1 --file data.txt --flag2` という引数の場合、`data.txt` の部分はオプション引数なので位置パラメータに残したくありません。ですが `for` ループは引数を一つずつ繰り返すため `data.txt` を飛ばすことができません。また `-abc` や `--option=value` のような一つの引数が複数の意味を持つ場合の対応も難しくなります。`while` を使用すればそのような処理をシンプルに書くことができます。

## 2. オプション引数に対応する

オプション引数に対応させます。

```sh
required() { [ $# -gt 1 ] || abort "option '$1' requires an argument"; }

FLAG_A='' FLAG_B='' ARG_I='' ARG_J=''

while [ $# -gt 0 ]; do
  case $1 in
    -a | --flag-a) FLAG_A=1 ;;
    -b | --flag-b) FLAG_B=1 ;;
    -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
    -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
    -?*) unknown "$@" ;;
    *) break
  esac
  shift
done
```

<details><summary>1. との差分（関数と変数定義以外）</summary><div>

```diff
-for arg; do
-  case $arg in
+while [ $# -gt 0 ]; do
+  case $1 in
     -a | --flag-a) FLAG_A=1 ;;
     -b | --flag-b) FLAG_B=1 ;;
+    -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
+    -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
     -?*) unknown "$@" ;;
-    *) set -- "$@" "$arg"
+    *) break
   esac
   shift
 done
```
</div></details>

## 3. オプションとオプション以外の引数の順番の混在に対応する

オプションとオプション以外の引数の順番を混在できるようにします。

```sh
param() { eval "$1=\$$1\ \\\"\"\\\${$2}\"\\\""; }

FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' PARAMS=''

parse_options() {
  OPTIND=$(($# + 1))
  while [ $# -gt 0 ]; do
    case $1 in
      -a | --flag-a) FLAG_A=1 ;;
      -b | --flag-b) FLAG_B=1 ;;
      -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
      -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
      -?*) unknown "$@" ;;
      *) param PARAMS $((OPTIND - $#))
    esac
    shift
  done
}

parse_options "$@"
eval "set -- $PARAMS"
```

<details><summary>2. との差分（関数と変数定義以外）</summary><div>

```diff
+parse_options() {
+  OPTIND=$(($# + 1))
 while [ $# -gt 0 ]; do
   case $1 in
     -a | --flag-a) FLAG_A=1 ;;
@@ -12,10 +14,14 @@
     -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
     -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
     -?*) unknown "$@" ;;
-    *) break
+      *) param PARAMS $((OPTIND - $#))
   esac
   shift
 done
+}
+
+parse_options "$@"
+eval "set -- $PARAMS"
```
</div></details>

新しく増えたのは `param` 関数です。オプション以外の引数を取り出すための `PARAMS` 変数を組み立てています。変数名を変更できるようにするため `eval` を使った読みづらいコードとなっていますが、最終的に `PARAMS="$PARAMS \"\${$((OPTIND - $#))}\""` というコードを実行します。例えばコマンドライン引数が `-a foo -b bar` の場合 `PARAMS` 変数には ` "${2}" "${4}"` という文字列が入ります。そしてこの文字列を `eval "set -- $PARAMS"` で位置パラメータ `$@` に取り出しています。bash 等であれば配列に引数を入れていくだけで良いのですが、POSIX 準拠の範囲では配列が使えないのでこのような方法を使っています。

また `OPTIND` 変数を本来とは違う用途で使用しているので注意してください。本来は `getopts` コマンドによって使用されるシェル変数で、これを違う目的に使い回すのは一般に良くないことですが、シェルスクリプトには（POSIX準拠の範囲では）グローバル変数しかなく衝突を避けるためにこのようにしています。オプション解析を自前で行うので `OPTIND` 変数が本来の用途で使われることはまずないので無害でしょう。気になる方は別の名前に変更してください。

## 4. -- でオプション解析処理を打ち切る

引数の途中で `--` が出てきたときに、そこでオプション解析処理を打ち切るための対応です。

```sh
params() { [ "$2" -ge "$3" ] || params_ "$@"; }
params_() { param "$1" "$2"; params "$1" $(($2 + 1)) "$3"; }

FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' PARAMS=''

parse_options() {
  OPTIND=$(($# + 1))
  while [ $# -gt 0 ]; do
    case $1 in
      -a | --flag-a) FLAG_A=1 ;;
      -b | --flag-b) FLAG_B=1 ;;
      -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
      -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
      --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
      -?*) unknown "$@" ;;
      *) param PARAMS $((OPTIND - $#))
    esac
    shift
  done
}

parse_options "$@"
eval "set -- $PARAMS"
```

<details><summary>3. との差分（関数と変数定義以外）</summary><div>

```diff
 parse_options() {
   OPTIND=$(($# + 1))
   while [ $# -gt 0 ]; do
     case $1 in
       -a | --flag-a) FLAG_A=1 ;;
       -b | --flag-b) FLAG_B=1 ;;
       -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
       -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
+      --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
       -?*) unknown "$@" ;;
       *) param PARAMS $((OPTIND - $#))
     esac
     shift
   done
 }

 parse_options "$@"
 eval "set -- $PARAMS"
```
</div></details>

新しく増えたのは `params` 関数です。`--` が現れたらそれ以降の引数をすべて `PARAMS` 変数に追加しているだけです。

## 5. 結合されたショートオプションに対応する

`ls -al` のようにショートオプションをつなげたものへの対応です。

```sh
FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' PARAMS=''

parse_options() {
  OPTIND=$(($# + 1))
  while [ $# -gt 0 ]; do
    case $1 in
      -[!-]?*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
    esac

    case $1 in
      -a | --flag-a) FLAG_A=1 ;;
      -b | --flag-b) FLAG_B=1 ;;
      -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
      -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
      --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
      -?*) unknown "$@" ;;
      *) param PARAMS $((OPTIND - $#))
    esac
    shift
  done
}

parse_options "$@"
eval "set -- $PARAMS"
```

<details><summary>4. との差分（関数と変数定義以外）</summary><div>

```diff
 parse_options() {
   OPTIND=$(($# + 1))
   while [ $# -gt 0 ]; do
+    case $1 in
+      -[!-]?*) OPTARG=$1; shift
+        set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
+    esac
+
     case $1 in
       -a | --flag-a) FLAG_A=1 ;;
       -b | --flag-b) FLAG_B=1 ;;
       -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
       -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
       --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
       -?*) unknown "$@" ;;
       *) param PARAMS $((OPTIND - $#))
     esac
     shift
   done
 }

 parse_options "$@"
 eval "set -- $PARAMS"
```
</div></details>

例えば `-abcd` のようになってる場合`-a -bcd` と最初の一文字と残りに分解しています。この処理をループのたびに行うことで、その他のコードを修正すること無く結合されたショートオプションに対応しています。

`OPTIND` 変数と同様に `OPTARG` 変数も本来の目的とは違った用途で使用しているので注意してください。`OPTARG` は結合されたショートオプションを分解するためのワーク変数として使用しています。

## 6. 結合されたオプション引数に対応する

`-i value` を `-ivalue`、`--arg-i value` を `--arg-i=value` という指定の仕方もできるようにするための対応です。

```sh
noarg() { [ ! "$OPTARG" ] || abort "option '$1' doesn't allow an argument"; }

FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' PARAMS=''

parse_options() {
  OPTIND=$(($# + 1))
  while [ $# -gt 0 ] && OPTARG=; do
    case $1 in
      --?*=*) OPTARG=$1; shift
        set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
      -[ij]?*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
      -[!-]?*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
    esac

    case $1 in
      -a | --flag-a) noarg "$@"; FLAG_A=1 ;;
      -b | --flag-b) noarg "$@"; FLAG_B=1 ;;
      -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
      -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
      --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
      -?*) unknown "$@" ;;
      *) param PARAMS $((OPTIND - $#))
    esac
    shift
  done
}

parse_options "$@"
eval "set -- $PARAMS"
```

<details><summary>5. との差分（関数と変数定義以外）</summary><div>

```diff
 parse_options() {
   OPTIND=$(($# + 1))
-  while [ $# -gt 0 ]; do
+  while [ $# -gt 0 ] && OPTARG=; do
     case $1 in
+      --?*=*) OPTARG=$1; shift
+        set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
+      -[ij]?*) OPTARG=$1; shift
+        set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
       -[!-]?*) OPTARG=$1; shift
         set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
     esac

     case $1 in
-      -a | --flag-a) FLAG_A=1 ;;
-      -b | --flag-b) FLAG_B=1 ;;
+      -a | --flag-a) noarg "$@"; FLAG_A=1 ;;
+      -b | --flag-b) noarg "$@"; FLAG_B=1 ;;
       -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
       -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
       --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
       -?*) unknown "$@" ;;
       *) param PARAMS $((OPTIND - $#))
     esac
     shift
   done
 }

 parse_options "$@"
 eval "set -- $PARAMS"
```
</div></details>

「結合されたオプション引数」は 5. の「結合されたショートオプション」と区別する必要があるので、どれが「結合されたオプション引数」であるかを判定するコードが必要になります。上記コードの `-[ij]?*)` が該当のコードです。（`case` の部分と二重管理に定義する必要があるので少し嫌ですね。）

また `OPTARG` は「結合されたオプション引数だったか？」を判定するフラグ変数としても再利用しています。

## 7. 省略可能なオプション引数に対応する

省略可能なオプション引数とは、「`-i` または `-ivalue`」「`--arg-i` または `--arg-i=value`」という指定ができるオプションです。`-i value` や `--arg-i value` という指定の仕方はできないので注意してください。（GNU 版の `getopt` も同じ仕様です。）

```sh
optional() { [ "$OPTARG" ] && OPTARG=$2; }

FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' OPT_O='' OPT_P='' PARAMS=''

parse_options() {
  OPTIND=$(($# + 1))
  while [ $# -gt 0 ] && OPTARG=; do
    case $1 in
      --?*=*) OPTARG=$1; shift
        set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
      -[ijop]?*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
      -[!-]?*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
    esac

    case $1 in
      -a | --flag-a) noarg "$@"; FLAG_A=1 ;;
      -b | --flag-b) noarg "$@"; FLAG_B=1 ;;
      -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
      -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
      -o | --opt-o ) optional "$@" && shift; OPT_O=${OPTARG:-1} ;;
      -p | --opt-p ) optional "$@" && shift; OPT_P=${OPTARG:-default} ;;
      --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
      -?*) unknown "$@" ;;
      *) param PARAMS $((OPTIND - $#))
    esac
    shift
  done
}

parse_options "$@"
eval "set -- $PARAMS"
```

<details><summary>6. との差分（関数と変数定義以外）</summary><div>

```diff
 parse_options() {
   OPTIND=$(($# + 1))
   while [ $# -gt 0 ] && OPTARG=; do
     case $1 in
       --?*=*) OPTARG=$1; shift
         set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
-      -[ij]?*) OPTARG=$1; shift
+      -[ijop]?*) OPTARG=$1; shift
         set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
       -[!-]?*) OPTARG=$1; shift
         set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
     esac

     case $1 in
       -a | --flag-a) noarg "$@"; FLAG_A=1 ;;
       -b | --flag-b) noarg "$@"; FLAG_B=1 ;;
       -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
       -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
+      -o | --opt-o ) optional "$@" && shift; OPT_O=${OPTARG:-1} ;;
+      -p | --opt-p ) optional "$@" && shift; OPT_P=${OPTARG:-default} ;;
       --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
       -?*) unknown "$@" ;;
       *) param PARAMS $((OPTIND - $#))
     esac
     shift
   done
 }

 parse_options "$@"
 eval "set -- $PARAMS"
```
</div></details>

`OPTARG` はさらに省略可能なオプション引数の値を入れた変数としても使用しています。`${OPTARG:-1}` や `${OPTARG:-default}` の `1` や `default` は（オプションがそのものではなく）オプションの引数が省略された場合（例 `--opt-o`）のデフォルト値です。オプションの引数を空文字にした場合（例 `--opt-o=`）もオプションの引数を省略されたとみなされます。つまり`--opt-o`と`--opt-o=`を区別することはできません。対応するショートオプション（`-o`）では区別するのが不可能で、また `getopt`と同じ動作であるため、仕様とします。

## 8. +, --no- でフラグを反転させる

GNU 版の `getopt` でも対応してない機能ですが、比較的よく使われている機能なので対応しています。

```sh
FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' OPT_O='' OPT_P='' PARAMS=''

parse_options() {
  OPTIND=$(($# + 1))
  while [ $# -gt 0 ] && OPTARG=; do
    case $1 in
      --?*=*) OPTARG=$1; shift
        set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
      --no-*) unset OPTARG ;;
      -[ijop]?*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
      -[!-]?*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
      +??*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "+${OPTARG#??}" "$@"; unset OPTARG ;;
      +*) unset OPTARG ;;
    esac

    case $1 in
      [-+]a | --flag-a | --no-flag-a) noarg "$@"; FLAG_A=${OPTARG+1} ;;
      [-+]b | --flag-b | --no-flag-b) noarg "$@"; FLAG_B=${OPTARG+1} ;;
      -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
      -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
      -o | --opt-o ) optional "$@" && shift; OPT_O=${OPTARG:-1} ;;
      -p | --opt-p ) optional "$@" && shift; OPT_P=${OPTARG:-default} ;;
      --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
      [-+]?*) unknown "$@" ;;
      *) param PARAMS $((OPTIND - $#))
    esac
    shift
  done
}

parse_options "$@"
eval "set -- $PARAMS"
```

<details><summary>7. との差分（関数と変数定義以外）</summary><div>

```diff
 parse_options() {
   OPTIND=$(($# + 1))
   while [ $# -gt 0 ] && OPTARG=; do
     case $1 in
       --?*=*) OPTARG=$1; shift
         set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
+      --no-*) unset OPTARG ;;
       -[ijop]?*) OPTARG=$1; shift
         set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
       -[!-]?*) OPTARG=$1; shift
         set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
+      +??*) OPTARG=$1; shift
+        set -- "${OPTARG%"${OPTARG#??}"}" "+${OPTARG#??}" "$@"; unset OPTARG ;;
+      +*) unset OPTARG ;;
     esac

     case $1 in
-      -a | --flag-a) noarg "$@"; FLAG_A=1 ;;
-      -b | --flag-b) noarg "$@"; FLAG_B=1 ;;
+      [-+]a | --flag-a | --no-flag-a) noarg "$@"; FLAG_A=${OPTARG+1} ;;
+      [-+]b | --flag-b | --no-flag-b) noarg "$@"; FLAG_B=${OPTARG+1} ;;
       -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
       -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
       -o | --opt-o ) optional "$@" && shift; OPT_O=${OPTARG:-1} ;;
       -p | --opt-p ) optional "$@" && shift; OPT_P=${OPTARG:-default} ;;
       --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
-      -?*) unknown "$@" ;;
+      [-+]?*) unknown "$@" ;;
       *) param PARAMS $((OPTIND - $#))
     esac
     shift
   done
 }

 parse_options "$@"
 eval "set -- $PARAMS"
```
</div></details>

`OPTARG` をさらに反転させたフラグであるかを判断するために使用しています。

## 9. 同じオプションが複数回指定された場合の対応

同じオプションが複数回していされた場合の対応です。今までのコードは同じオプションが指定された場合、最後に指定されたオプションが有効になっていました。これはよくある実装の一つですが、場合によっては指定されたオプションの数だけ処理を行いたい場合もあります。一番簡単なのはオプションが指定されるたびに変数に値を追加していく方法です。例えば、`-v` が指定された場合 `VERBOSE` 変数に `v` を、もう一度指定された場合は `vv` をと値を追加していきます。`0`, `1`, `2`, ...と数値を使用しても良いと思います。オプション引数がある場合は、任意の区切り記号で区切って値を結合していきます。（例 カンマ区切りで `foo,bar,baz`）

```shell
# 参考 foo,bar,baz のような文字をループで処理する場合

VALUES="foo,bar,baz"

# ループ処理しやすくするために空文字以外の場合に,を最後につける
VALUES=$VALUES${VALUES:+,}
while [ "$VALUES" ]; do
  echo "${VALUES%%,*}"
  VALUES=${VALUES#*,}
done

# または（位置パラメータ$@に代入する）
orig_ifs=$IFS
IFS=,
set -- $VALUES
IFS=$orig_ifs

```

上記のように一つの変数に複数の値を詰め込むことができるならば、この方法を使うのが簡単ですが、例えばファイル名の指定など、どんな文字にも対応したい場合もあります。bash など配列が使えるシェルであれば配列に入れていくだけでよいのですが、POSIX 準拠する場合は配列が使用できないので代わりに文字列をエスケープして詰め込んでいます。

```shell
FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' OPT_O='' OPT_P='' PARAMS=''

push_end() { [ "${2#*\'}" = "$2" ] && eval "$1=\"\$$1 '\${3:-}\$2'\""; }
push() { push_end "$@" || push "$1" "${2#*\'}" "${2%%\'*}'\"'\"'"; }

# " # 処理的には意味がないコメント。Qiitaのシンタクスハイライトが混乱してるようなので

parse_options() {
  OPTIND=$(($# + 1))
  while [ $# -gt 0 ] && OPTARG=; do
    case $1 in
      --?*=*) OPTARG=$1; shift
        set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
      --no-*) unset OPTARG ;;
      -[ijop]?*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
      -[!-]?*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
      +??*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "+${OPTARG#??}" "$@"; unset OPTARG ;;
      +*) unset OPTARG ;;
    esac

    case $1 in
      [-+]a | --flag-a | --no-flag-a) noarg "$@"; FLAG_A=${OPTARG+1} ;;
      [-+]b | --flag-b | --no-flag-b) noarg "$@"; FLAG_B=${OPTARG+1} ;;
      -i | --arg-i ) required "$@" && shift; push ARG_I "$1" ;;
      -j | --arg-j ) required "$@" && shift; push ARG_J "$1" ;;
      -o | --opt-o ) optional "$@" && shift; push OPT_O "${OPTARG:-1}" ;;
      -p | --opt-p ) optional "$@" && shift; push OPT_P "${OPTARG:-default}" ;;
      --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
      [-+]?*) unknown "$@" ;;
      *) param PARAMS $((OPTIND - $#))
    esac
    shift
  done
}
```

（`push`, `push_end` 関数を定義し、`push` 関数を使うようにしただけです。差分は省略します。）

このコードを使って、`-i foo -i bar -i baz` のように指定すると `ARG_I` 変数に `'foo' 'bar' 'baz'` という文字列が代入されます。`-i "It's a pen"` のようにシングルクォートが含まれている場合は、`'It'"'"'s a pen'` というふうにエスケープされます。そしてこの値を使用する時は、`eval` を使って位置パラメータ `$@` に代入します。

```shell
args_i() {
  printf '%s\n' "$@"
}
eval args_i "$ARG_I"

# または（現在の$@が上書きされます）
# eval set -- "$ARG_I"
# printf '%s\n' "$@"
```

## 対応してない機能について

* 単一の `-` で始まるロングオプション (例 `-exec`)
  * GNU 版の `getopt` で使用可能だが、`find` コマンドと Java や PowerShell 関連でしか見たことがなく、シェルスクリプトで実装するものとしては需要は少ないと思われるため
* `+` で始まる結合されたオプション引数 (例 `+ovalue`)
  * `zsh`, `ksh`, `mksh` で使用可能だが、使われてる例を知らないため
* ロングオプション名を省略名で指定できる（例 `--longoptions` の代わりに 省略名 `--long` が使える）
  * コードが長くなるし、個人的にあまり好きな機能ではないので

**補足** （2020-11-03 追記） この記事を元に開発したオプション解析ライブラリ [getoptions](https://github.com/ko1nksm/getoptions) では、単一の `-` で始まるロングオプションと、ロングオプション名の省略に対応しています。

## さいごに

このコードは必要な部分をコピーして手直して組み込むことを想定したものですが、ライブラリの形にすると更に使いやすくなりそうです。気が向いたら~~作るかもしれません~~ → 作りました。（この記事の冒頭参照）