Linux getoptコマンド 長いオプションの仕様に注意

### getoptコマンドで長いオプションを指定する際に注意しなければならない事柄について説明します。

発端は私の下記記事にて作成したシェルスクリプトを実行した際に起こった**不可解(私にとって)結果**が発端になります。
[(Qiita) CSVファイルをDockerコンテナ内のPostgreSQLにインポートするシェルスクリプトを作る](https://qiita.com/pipito-yukio/items/0558f4431459f81c76dd)

### 参考サイト

[Ubuntu 公式マニュアル(日本語): getopt - コマンドのオプションを解析する (強化版)](https://manpages.ubuntu.com/manpages/impish/ja/man1/getopt.1.html)  
※2 下記 「man getopt」コンソールで出力される(英語) の日本語版に対応
```bash
$ man getopt
GETOPT(1)                                                     User Commands                                                    GETOPT(1)

NAME
       getopt - parse command options (enhanced)

# ...以下省略...
```

下記 **atmarkIT** サイトも豊富なサンプルとともに解説が有り大変参考になります。  
+ [Linux基本コマンドTips（379）【 getopt 】コマンド――オプションを解析する](https://atmarkit.itmedia.co.jp/ait/articles/2002/14/news016.html)
    
+ [Linux基本コマンドTips（380）【 getopt 】コマンド――オプションを解析する（応用編）](https://atmarkit.itmedia.co.jp/ait/articles/2002/20/news011.html)

### 長いオプション解析結果の不可解？

下記の図がシェルスクリプトのgetoptの長いオプションを指定した部分になります。

![getopt_longoption_overview.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/6864497a-10b3-fbb5-a138-e9e3efdc3e8b.jpeg)

結論から言うと、**長いオプションの解析では完全一致を含む最小一致がこのコマンドの想定した動作である**ということになります。

### 長いオプションを使う意図は？

例としてデータベースのCSVインポートで**レコード件数によってインポート処理を変えたい場合**  
+ 数十万件を超えたら **テーブルの制約を解除して**インポート実行  
※**危険な処理なので長いオプションかつ長めのオプション名が必要だ**
+ 上記以外なら**DBのチェック有り** (**テーブルの制約**) インポート実行  
※**オプション省略でいい**

#### 1. 検証用のシェルスクリプト

+ オブションが未指定なら "**not drop_constraint**" を表示
+ --drop-constraint が指定されたら "**drop_constraint**" を表示
+ --help が指定されたらコマンドのヘルプを表示
+ 長いオプションを指定して完全一致しない場合は、エラーメッセージを表示

```shell
#!/bin/bash

readonly SCRIPT_NAME=${0##*/}

print_error()
{
   cat << END 1>&2
$SCRIPT_NAME: $1
Try --help option
END
}

print_help()
{
   cat << END
Example:
  $SCRIPT_NAME
  $SCRIPT_NAME --drop-constraint
END
}

params=$(getopt -o '' -l drop-constraint -l help -n "$SCRIPT_NAME" -- "$@")

# Check command exit status
if [[ $? -ne 0 ]]; then
  # 長いオプション不一致 ※最小一致は含まない
  echo 'Try --help option for more information' 1>&2
  exit 1
fi

eval set -- "$params"

echo "eval.params: ${params}"

# init option value
drop_constraint=

# Positional parameter count: $#
while [[ $# -gt 0 ]]
do
  case "$1" in
    --"drop-constraint")
      drop_constraint=true
      shift
      ;;
    --'help')
      print_help
      exit 0
      ;;
    --)
      shift
      break
      ;;
    *)
      echo 'Internal error!' >&2
      exit 1
      ;;
  esac
done

if [[ ${drop_constraint} == true ]]; then
  # ロングオプションに一致
  echo "drop_constraint"
else
  # オプション指定なし
  echo "not drop_constraint"
fi
```

#### 1-A. 最小一致(完全一致含む)で解析OKとされたオプション入力例

これが一致するのは当然

```bash
$ # 完全一致
$ ./getopt_only_longopt.sh --drop-constraint
eval.params:  --drop-constraint --
drop_constraint: true
option is drop_constraint
$ # --help
$ ./getopt_only_longopt.sh --help
eval.params:  --help --
Example:
  getopt_only_longopt.sh
  getopt_only_longopt.sh --drop-constraint
```

**私が不可解と思った**実行結果

※ **--d, --drop, --drop-const**でOKなら、**意図的に長いオプション名にしたのに意味がない?**  
※ **--d が OKなら 短いオプション -d でも変わらなくない?**

```bash
$ # 末尾の一文字を欠ける
$ ./getopt_only_longopt.sh --drop-constrain
eval.params:  --drop-constraint --
drop_constraint: true
option is drop_constraint
$ # --dropのみ
$ ./getopt_only_longopt.sh --drop
eval.params:  --drop-constraint --
drop_constraint: true
option is drop_constraint
$ # 先頭のみ --d
$ ./getopt_only_longopt.sh --d
eval.params:  --drop-constraint --
drop_constraint: true
option is drop_constraint
$ # 最小一致
$ ./getopt_only_longopt.sh --h
eval.params:  --help --
Example:
  getopt_only_longopt.sh
  getopt_only_longopt.sh --drop-constraint
```

#### 1-B. 入力パラメータ無し解析とされた入力例
+ -- のみ
+ オプションなし ※通常処理

```bash
$ # --のみ
$ ./getopt_only_longopt.sh --
eval.params:  --
drop_constraint: 
not drop_constraint
$ # オプション未指定
$ ./getopt_only_longopt.sh
eval.params:  --
drop_constraint: 
not drop_constraint
```

#### 1-C. 入力エラーと解析とされた入力例

+ 長いオプションが指定されているが**最小一致も完全一致もない**

```bash
$ # 完全に不一致 (1) 中の文字誤り
$ ./getopt_only_longopt.sh --drop-consraints
getopt_only_longopt.sh: unrecognized option '--drop-consraints'
Try --help option for more information
$ # 完全に不一致 (2) 存在しないオプション
$ ./getopt_only_longopt.sh --no
getopt_only_longopt.sh: unrecognized option '--no'
Try --help option for more information
```

#### 1-D. 短いオプションを外したらどうなる  
※ **-o ""** の部分

```shell
params=$(getopt -l drop-constraint -l help -n "$SCRIPT_NAME" -- "$@")
```

**完全一致も不一致(エラーにもならない)もオプションなしと判定され**想定外の結果となる

```bash
$ # 完全一致: drop-constraint
$ ./getopt_only_longopt.sh --drop-constraint
eval.params:  --
drop_constraint: 
not drop_constraint
$ # 完全一致: help
$ ./getopt_only_longopt.sh --help
eval.params:  --
drop_constraint: 
not drop_constraint
$ # 不一致 ※エラーにならない
$ ./getopt_only_longopt.sh --crop
eval.params:  --
drop_constraint: 
not drop_constraint
```

なぜこのような動作になるのかは最初に紹介した公式ドキュメントに記載がある

```
ハグ
getopt(3) 関数は、引き数が任意のロングオプションが、空の任意引き数を渡された場合でも、解析
       できる (だが、ショートオプションについては、それができない)。この getopt(1) コマンドは、空
       の任意引き数を、引き数が存在しないかのように処理している。

訳注] 詳しく言うと、getopt(3)  (getopt_long(3)) 関数は、 引き数が任意のロングオプションに
              引数がない場合と、空の引き数を渡された場合とを区別している。   しかし、ショートオプ
              ションについては、その区別ができない。

              この  getopt(1) コマンドの動作について言うと、第 2、第 3 の書式では、ロングオプショ
              ン、ショートオプションを問わず、   引き数が任意のオプションに引き数が存在しない場合
              も、引き数が空文字列である場合も、  オプションの引き数として空文字列を出力する。 ま
              た、第 1 の書式では、引き数が任意のオプションに引き数が存在しない場合も、  引き数が
              空文字列の場合も、そのオプションの引き数はまったく出力されない。 「出力」セクション
              のショートオプションの説明を参照していただきたい。

              要するに、この getopt コマンドでは、引き数が任意のオプションについて、 引き数が存在
              しない場合と引き数が空文字列である場合の区別がまったくないのである。 だから、バグと
              言っても、不具合ということではなく、このコマンドと getopt(3) 関数の仕様が微妙に違う
              ことを言っているのだろうと思う。

       ショートオプションを全く使いたくない場合の  getopt  コマンドの構文は、あまり直感的ではない
       (ショートオプションズ文字列を明示的に空文字列にしなければならないのだ)。

       [訳注] すなわち、getopt -o '' --longoptions ...  のように使用しなければならない。
```

#### 公式のサンプル  

シェルスクリプトの実行環境
+ OS: Ubuntu 22.04.3 LTS  
[格納ファイル] /usr/share/doc/util-linux/examples/**getopt-example.bash**

ソースコードの内容は下記のとおりで、シェルスクリプトの標準テンプレートとして使えます。

```shell
#!/bin/bash

# A small example script for using the getopt(1) program.
# This script will only work with bash(1).
# A similar script using the tcsh(1) language can be found
# as getopt-example.tcsh.

# Example input and output (from the bash prompt):
#
# ./getopt-example.bash -a par1 'another arg' --c-long 'wow!*\?' -cmore -b " very long "
# Option a
# Option c, no argument
# Option c, argument 'more'
# Option b, argument ' very long '
# Remaining arguments:
# --> 'par1'
# --> 'another arg'
# --> 'wow!*\?'

# Note that we use "$@" to let each command-line parameter expand to a
# separate word. The quotes around "$@" are essential!
# We need TEMP as the 'eval set --' would nuke the return value of getopt.
TEMP=$(getopt -o 'ab:c::' --long 'a-long,b-long:,c-long::' -n 'example.bash' -- "$@")

if [ $? -ne 0 ]; then
	echo 'Terminating...' >&2
	exit 1
fi

# Note the quotes around "$TEMP": they are essential!
eval set -- "$TEMP"
unset TEMP

while true; do
	case "$1" in
		'-a'|'--a-long')
			echo 'Option a'
			shift
			continue
		;;
		'-b'|'--b-long')
			echo "Option b, argument '$2'"
			shift 2
			continue
		;;
		'-c'|'--c-long')
			# c has an optional argument. As we are in quoted mode,
			# an empty parameter will be generated if its optional
			# argument is not found.
			case "$2" in
				'')
					echo 'Option c, no argument'
				;;
				*)
					echo "Option c, argument '$2'"
				;;
			esac
			shift 2
			continue
		;;
		'--')
			shift
			break
		;;
		*)
			echo 'Internal error!' >&2
			exit 1
		;;
	esac
done

echo 'Remaining arguments:'
for arg; do
	echo "--> '$arg'"
done
```

### 2. 意図したオプションに変える

#### 改善したシェルスクリプト  

+ 短いオプション: **-o drop-constraint** に変更
+ 短いオプションの有無
    + 指定なし: -o オプションチェックせずに処理分岐に入る
    + 指定有り: "drop-constraint" に完全一致するかチェックする

```shell
#!/bin/bash

readonly SCRIPT_NAME=${0##*/}

VALID_OPTION="drop-constraint"

print_error()
{
   cat << END 1>&2
$SCRIPT_NAME: $1
Try --help option
END
}

print_help()
{
   cat << END
Example:
  $SCRIPT_NAME
  $SCRIPT_NAME -o drop-constraint
END
}

params=$(getopt -o 'o:' -l help -n "$SCRIPT_NAME" -- "$@")

# Check command exit status
if [[ $? -ne 0 ]]; then
  echo 'Try --help option for more information' 1>&2
  exit 1
fi

eval set -- "$params"

echo "eval.params: ${params}"

# init option value
input_option=

# Positional parameter count: $#
while [[ $# -gt 0 ]]
do
  case "$1" in
    -o)
      input_option=$2
      shift 2
      ;;
    --'help')
      print_help
      exit 0
      ;;
    --)
      shift
      break
      ;;
    *)
      echo 'Internal error!' >&2
      exit 1
      ;;
  esac
done

echo "input_option: ${input_option}"
drop_constraint=

if [ -n "$input_option" ]; then
   # 短いオプションが入力されていたら、完全一致チェック
   if [ "$VALID_OPTION" == "$input_option" ]; then
       # 有効 
       drop_constraint=true
   else
      # 入力エラー
      echo "Error: short option -o is not match 'drop-constraint'" 1>&2
      exit 1
   fi
fi   

echo "drop_constraint: ${drop_constraint}"
if [[ ${drop_constraint} == true ]]; then
  echo "option is drop_constraint"
else
  echo "not drop_constraint"
fi
```

正常ケースの実行結果

```bash
$ # オプションなし
$ ./getopt_only_longopt.sh
eval.params:  --
input_option: 
drop_constraint: 
not drop_constraint
$ # 短いオプションありで完全一致
$ ./getopt_only_longopt.sh -o drop-constraint
eval.params:  -o 'drop-constraint' --
input_option: drop-constraint
drop_constraint: true
option is drop_constraint
$ # 長いオプション help
$ ./getopt_only_longopt.sh --help
eval.params:  --help --
Example:
  getopt_only_longopt.sh
  getopt_only_longopt.sh -o drop-constraint
```

エラーケースの実行結果 
 
 + (1) 短いオプションの不一致の場合は、自前のチェックエラーで終了  
 + (2) 存在しない短いオプションの場合は、getoptコマンドのチェックエラーで終了

```bash
$ # 短いオプションありで不一致
$ ./getopt_only_longopt.sh -o drop-constrain
eval.params:  -o 'drop-constrain' --
input_option: drop-constrain
Error: short option -o is not match 'drop-constraint'
$ # 存在しない短いオプションはエラー
$ ./getopt_only_longopt.sh -l drop-constraint
getopt_only_longopt.sh: 無効なオプション -- 'l'
Try --help option for more information
```

この改修により当初の想定した動作が実現できました。


### 3. 結論

最初に紹介したサイトのドキュメントなどよくよく読むと、長いオプションで最小一致が一応作者の想定していることがなんとなく理解できました。

コマンドの設計思想を理解した上で、シェルスクリプトを作成することが重要です。

**man コマンド で出力した内容(英語)をよく読む**、英語がわからなければGoogle翻訳で訳す、あるいは対応する日本語訳のサイトもあるのでそのドキュメントをじっくり読む。

シェルコマンド(外部コマンド含む)ではmanでコンソールに使用例を出力するものも多いですし、今回のようにサンプルソースをシステムに格納している例もあります。

それでもわからなかったら**Stackoverflow**などの回答に信頼おけるサイトを探しましょう。