シェルスクリプトの簡素な引数解析（POSIX 準拠）

こんにちは。
シェルスクリプトの自力による引数処理（解析）を作りました[^1] [^2]（```parse.sh```）。

## 特徴
* POSIX 準拠シェルで動きます[^3]。
* ショートオプション名、ロングオプション名、複合フラグ[^4]、引数付きオプション（複数の引数[^5]も可）へ対応します。
* 制約条件は、オプション（およびその引数）は前半へ、主引数（＝位置引数）は後半へ固まっている必要があります[^6]。
   * また `-` で始まる文字列は主引数、また引数付きオプションの引数とはみなせないです。
* ソース内で、オプション引数解析部の簡素化に努めました[^11]。

[^1]: 参考：「[bash によるオプション解析](https://qiita.com/b4b4r07/items/dcd6be0bb9c9185475bb)」（自前で解析）
[^2]: 場合によっては自力ではなく、getopt, getopts, [docopts](https://github.com/docopt/docopts) の利用も良いでしょう。
[^3]: したがって bash などに限らず動きます。
[^4]: 例えば ```ls -l -t``` を ```ls -lt``` と表すものです。
[^5]: カンマ区切りの引数は複数へ分解して取り出します（例： ```--array x,y,z```）。
[^6]: したがって後半の主引数の解析は残りの ```"$@"``` を単に ```for``` ループで回して取り出します（内部関数 ```print_array_f```）。
[^11]: 内部関数```param_f```の利用などにより、```while``` ループ部の簡素化に努めました（メンテナンスの容易性を重視）。

## 動作例（DEBUG モード）
[dash](https://ja.wikipedia.org/wiki/Debian_Almquist_shell)（POSIX 準拠シェル）で動かした例は[^9]、
* 引数付きオプションに対応します（空白区切り、もしくは区切り無し）

[^9]: DEBUGモードで解析結果を出力しています。

```console
$ dash parse.sh -v -mdog -v -aa,b,c 1 2 3
$ dash parse.sh -v -m dog -v -a a,b,c 1 2 3
optional arguments:
 -v: '--verbose'
 -m: dog
 -a: [ a b c ]
main arguments: [ 1 2 3 ]
```

##### ロングオプション名で動かした例は、
* 引数付きオプションに対応します（空白区切り、もしくは```=```区切り）

```console
$ dash parse.sh --verbose --mammal cat --verbose --array x,y,z I II III
$ dash parse.sh --verbose --mammal=cat --verbose --array=x,y,z I II III
optional arguments:
 -v: '--verbose'
 -m: cat
 -a: [ x y z ]
main arguments: [ I II III ]
```

### help オプションでの動作例

```console
$ dash parse.sh --invalid_option_flag
Warning: Type "parse.sh -h|--help" for usage instructions.
$ dash parse.sh --help

    Name: parse.sh - parse arguments

    Usage: parse.sh [options] [main_argument [...]]

    Options:
      -h|--help
      -v|--verbose
      -m|--mammal mammal_name
      -a|--array array_string    Each item should be delimited by "," in array_string.
```

## Source code
```parse.sh
#!/bin/sh

## default values and constants
DEBUG=1
SEP_SPLIT=','
PROGNAME=$(basename "$0")
LF=$(printf '\n_');LF=${LF%_}
SHIFT_INCR_ERR=2

## functions
exit_f() { echo "$2"; exit $1; }
split_f() { echo "$1" | tr $SEP_SPLIT ' '; }

param_f() {
  arg_param="$2"; shift_incr=0
  for flag in $(split_f "$1 ="); do arg_param=${arg_param#"$flag"}; done
  if [ -z "$arg_param" ]; then
    arg_param="$3"; shift_incr=1
    case "$arg_param" in -*|"") shift_incr=$SHIFT_INCR_ERR;; esac
  fi
  split_f "$arg_param"; return $shift_incr
}

decompose_short_flag_f() {
  arg1="$(echo "$1" | cut -c1-2)"
  arg2="$(echo "$1" | cut -c3-)"
  shift; echo "DUMMY_FOR_SHIFT" "$arg1" "-$arg2" "$@"
}

## messages
WARNING="Warning: Type \"$PROGNAME -h|--help\" for usage instructions."

USAGE=$(cat <<- EOS
    Name: $PROGNAME - parse arguments$LF
    Usage: $PROGNAME [options] [main_argument [...]]$LF
    Options:
      -h|--help
      -v|--verbose
      -m|--mammal mammal_name
      -a|--array array_string    Each item should be delimited by "$SEP_SPLIT" in array_string.
	EOS
)  # Note that the above "EOS" follows a tab code.

## parsing optional arguments
# shellcheck disable=SC2046
while [ $# -gt 0 ]; do
  case "$1" in
    -m*|--mammal*) MAMMAL=$(param_f -m,--mammal "$@");;
    -a*|--array*)  ARRAY=$(param_f -a,--array "$@");;
    -v|--verbose)  verbose="--verbose";;
    -h|--help)     exit_f 0 "$USAGE";;
    -[!-][!-]*)    set -- $(decompose_short_flag_f "$@");;
    -*)            exit_f 1 "$WARNING";;
    *)             break;;
  esac
  shift_incr=$?; [ $shift_incr -lt $SHIFT_INCR_ERR ] || exit_f 1 "$WARNING"
  shift $((shift_incr+1))  # = 1 or 2
done

## main
if [ -n "$DEBUG" ]; then
  print_array_f() { printf "[ "; for e in "$@"; do printf "%s " "$e"; done; echo "]"; }
  printf "%s\n" "optional arguments:"
  echo ' -v:' \'"$verbose"\'
  echo ' -m:' "$MAMMAL"
  printf -- " -a: "; print_array_f "$ARRAY"
  printf "main arguments: "
  print_array_f "$@"
fi
```


## 説明

* ```while [ $# -gt 0 ]; do``` ループ内： 位置パラメータ（先頭順）からオプション引数を取り出します（```shift``` コマンド利用）。

### 内部関数
* ```param_f```： 引数つきオプション（＋引数）から、その引数部分を取り出します（動作例では ```-m|--mammal``` などの直後の値）。
   * また上記の```while```ループで使うための ```shift``` 数を求めます（正常ならば 1 もしくは 2。これから 1 を引いた ```shift_incr``` を ```return status``` 値として出力します）。

* ```decompose_short_flag_f```： 複合フラグ処理を行います。
   * しかし、```shellcheck``` コマンドで検査すると、この中で ```set -- $var``` の箇所に対して警告が出ることは仕方ないようです[^10]。

[^10]: 参考："[SC2086: Double quote while using set](https://github.com/koalaman/shellcheck/issues/1284)" (koalaman/shellcheck, GitHub)

```console
In parse.sh line 154:
    -[!-][!-]*)     set -- $(decompose_flag_f "$@");;
                           ^----------------------^ SC2046: Quote this to prevent word splitting.
```

## 最小機能限定版
最小機能に限定したものも作りました。引数付きオプションには対応しません。

```console
$ ./parse_minimum.sh -v 1 2 3
 -v: '--verbose'
1 2 3
```

```parse_minimum.sh
#!/bin/sh
PROGNAME=$(basename "$0")

# function
exit_f() { echo "$2"; exit $1; }

# messages
WARNING="Warning: Type \"$PROGNAME -h|--help\" for usage instructions."

USAGE=$(cat <<- EOS
    Name: $PROGNAME - parse arguments
    Usage: $PROGNAME [options] [main_argument [...]]
    Options:
      -h|--help
      -v|--verbose
	EOS
)  # Note that the above "EOS" follows a tab code.

# parsing optional arguments
while [ $# -gt 0 ]; do
  case "$1" in
    -v|--verbose)  verbose="--verbose";;
    -h|--help)     exit_f 0 "$USAGE";;
    -*)            exit_f 1 "$WARNING";;
    *)             break;;
  esac
  shift 1
done

# main
echo ' -v:' \'$verbose\'
for e in "$@"; do printf "%s " "$e"; done; echo
```