getoptions を使って面倒なシェルスクリプトのオプション解析コードを自動生成しよう！

## はじめに

[getoptions](https://github.com/ko1nksm/getoptions) はシェルスクリプト用のオプションパーサーです。`getopts` や `getopt` の代わりに使うことができ、`getoptions` をインストールするだけで簡単にシェルスクリプトのオプション解析を実装することができます。しかし、不特定の人に配布するシェルスクリプトの場合は `getoptions` をインストールしてもらうというのは選択肢にならないかもしれません。

でも大丈夫！ getoptions はオプションパーサーのジェネレータとして使うこともできます。ジェネレータとして使うと自分でオプションパーサーのコードを書く必要はありません。

**オプションパーサーとしての使い方はこちら**
「[シェルスクリプト(bash等)の引数解析が究極的に簡単になりました](https://qiita.com/ko1nksm/items/9ee16927b7f8899c4a9e)」

## 使い方

オプションパーサーとして使う場合は `getoptions` コマンドをインストールするだけで使えますが、ジェネレーターとして使う場合は `gengetoptions` コマンドも必要になります。もちろんコードの生成に使用するだけなのでシェルスクリプトの配布先にはどちらもインストールする必要はありません。（[ダウンロードはこちらから](https://github.com/ko1nksm/getoptions/releases)）

まずオプション解析を行いたいシェルスクリプトに、オプションの定義を書いた関数（`parser_definition`）とそれを認識するタグ（`@getoptions` ～ `@end`）、そしてオプションパーサーを埋め込むためのタグ（`@gengetoptions parser -i parser_definition -` ～ `@end`）を書きます。`@gengetoptions` の引数は `gengetoptions` の引数と同じです。

```sh
#!/bin/sh

set -eu

# @getoptions
parser_definition() {
  setup   REST help:usage -- "Usage: example.sh [options]... [arguments]..." ''
  msg -- 'Options:'
  flag    FLAG    -f --flag                 -- "takes no arguments"
  param   PARAM   -p --param                -- "takes one argument"
  option  OPTION  -o --option on:"default"  -- "takes one optional argument"
  disp    :usage  -h --help
  disp    VERSION    --version
}
# @end

# @gengetoptions parser -i parser_definition -
# @end

# ここからメイン処理
echo "FLAG: $FLAG, PARAM: $PARAM, OPTION: $OPTION"
printf '%s\n' "$@" # rest arguments
```

そして以下のコマンドを実行すると、`@gengetoptions` ～ `@end` の中にオプションパーサーが生成されます。コマンドを実行するたびに `@gengetoptions` ～ `@end` の間のコードは上書きされるので注意してください。

```sh
gengetoptions embed --overwrite example.sh
```

<details>
<summary>生成コード</summary>
<div>

```sh
#!/bin/sh

set -eu

# @getoptions
parser_definition() {
  setup   REST help:usage -- "Usage: example.sh [options]... [arguments]..." ''
  msg -- 'Options:'
  flag    FLAG    -f --flag                 -- "takes no arguments"
  param   PARAM   -p --param                -- "takes one argument"
  option  OPTION  -o --option on:"default"  -- "takes one optional argument"
  disp    :usage  -h --help
  disp    VERSION    --version
}
# @end

# @gengetoptions parser -i parser_definition -
# Generated by getoptions (BEGIN)
# URL: https://github.com/ko1nksm/getoptions (v3.3.0)
FLAG=''
PARAM=''
OPTION=''
REST=''
getoptions_parse() {
  OPTIND=$(($#+1))
  while OPTARG= && [ $# -gt 0 ]; do
    case $1 in
      --?*=*) OPTARG=$1; shift
        eval 'set -- "${OPTARG%%\=*}" "${OPTARG#*\=}"' ${1+'"$@"'}
        ;;
      --no-*|--without-*) unset OPTARG ;;
      -[po]?*) OPTARG=$1; shift
        eval 'set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}"' ${1+'"$@"'}
        ;;
      -[fh]?*) OPTARG=$1; shift
        eval 'set -- "${OPTARG%"${OPTARG#??}"}" -"${OPTARG#??}"' ${1+'"$@"'}
        OPTARG= ;;
    esac
    case $1 in
      '-f'|'--flag')
        [ "${OPTARG:-}" ] && OPTARG=${OPTARG#*\=} && set "noarg" "$1" && break
        eval '[ ${OPTARG+x} ] &&:' && OPTARG='1' || OPTARG=''
        FLAG="$OPTARG"
        ;;
      '-p'|'--param')
        [ $# -le 1 ] && set "required" "$1" && break
        OPTARG=$2
        PARAM="$OPTARG"
        shift ;;
      '-o'|'--option')
        set -- "$1" "$@"
        [ ${OPTARG+x} ] && {
          case $1 in --no-*|--without-*) set "noarg" "${1%%\=*}"; break; esac
          [ "${OPTARG:-}" ] && { shift; OPTARG=$2; } || OPTARG='default'
        } || OPTARG=''
        OPTION="$OPTARG"
        shift ;;
      '-h'|'--help')
        usage
        exit 0 ;;
      '--version')
        echo "${VERSION}"
        exit 0 ;;
      --)
        shift
        while [ $# -gt 0 ]; do
          REST="${REST} \"\${$(($OPTIND-$#))}\""
          shift
        done
        break ;;
      [-]?*) set "unknown" "$1"; break ;;
      *)
        REST="${REST} \"\${$(($OPTIND-$#))}\""
    esac
    shift
  done
  [ $# -eq 0 ] && { OPTIND=1; unset OPTARG; return 0; }
  case $1 in
    unknown) set "Unrecognized option: $2" "$@" ;;
    noarg) set "Does not allow an argument: $2" "$@" ;;
    required) set "Requires an argument: $2" "$@" ;;
    pattern:*) set "Does not match the pattern (${1#*:}): $2" "$@" ;;
    notcmd) set "Not a command: $2" "$@" ;;
    *) set "Validation error ($1): $2" "$@"
  esac
  echo "$1" >&2
  exit 1
}
usage() {
cat<<'GETOPTIONSHERE'
Usage: example.sh [options]... [arguments]...

Options:
  -f, --flag                  takes no arguments
  -p, --param PARAM           takes one argument
  -o, --option[=OPTION]       takes one optional argument
  -h, --help                  
      --version               
GETOPTIONSHERE
}
eval getoptions_parse ${1+'"$@"'}; eval set -- "${REST}"
# Generated by getoptions (END)
# @end

# ここからメイン処理
echo "FLAG: $FLAG, PARAM: $PARAM, OPTION: $OPTION"
printf '%s\n' "$@" # rest arguments
```

</div>
</details>

生成されるコードは見ての通り比較的シンプルなコードです（若干汎用的なコードだが手動で書くのと大差ないはず）。ヘルプを表示するときに `cat` コマンドを呼び出すだけで、オプションの解析に外部コマンドは一切使わないため高速です。単一のコードで bash だけではなく dash などを含め POSIX シェル全てに対応しているコードを生成します。オプションを変更するときは `parser_definition` 関数の中身を変更してもう一度生成するだけです。

個人的なスクリプト等は `getoptions` コマンドをインストールしておいて動的にオプションパーサーを生成する方がメンテナンスが楽で推奨する使い方ですが、このようにジェネレータとして使用することもできるようになっています。

## さいごに

一つヒントとして生成されたコードも getoptions 自体もライセンスは CC0 でパブリックドメイン（著作権は主張しない）相当であるため、生成されたコードを元にあたかも getoptions を使っていないかのように書き換えて配布することも、独自のライセンスで配布するのも完全に自由です。最初からそのような使い方を想定しています。生成されたコードに含まれる URL はメンテナンスする際の参考情報に過ぎず `--no-comments` オプションを `@gengetoptions` タグに追加すれば生成しないようにすることもできます。

もういいかげんシェルスクリプトのオプション解析コードを試行錯誤して自分で書くのはやめませんか？