【最終完全版】 bash/zsh 用オプション解析テンプレート (getopts→shift)

### Qiita/Zenn 用テンプレート
# 【最終完全版】 bash/zsh 用オプション解析テンプレート (getopts→shift)
### getopts bash zsh シェルスクリプト オプション解析

[//]: # (図、スクショ（★最後に書く）)


（2023-03-28 コーディング スタイル を変更）

## オプション解析に使う getopts と shift

bash/zsh 用オプション解析テンプレートとは、シェルスクリプトにどのオプションが指定されたのかを判定しやすくするためのスクリプトのテンプレートです。オプションとは下記の --version のようなハイフンから始まる指定です。

    my-shell-script  --version

シェルに入力するコマンドに指定するオプションを解析する専用のコマンド **getopts** や **getopt** を使えば、その内部で様々なオプションを解析してくれることを期待してしまうのですが、実際は制限事項がかなり複雑にあり実用に耐えられません。詳しくは getopts や getopt をマスターするために他の方が解説された記事を参照してください。たとえば、getopts は ロング オプション に対応していません。また getopt は互換性に不安があります。

しかし、オプションの解析は **ループと shift を使ったスクリプトのテンプレート** を使えば簡単に対応できます。しかも、次の章で示す様々なオプション指定方法のすべてに対応できます（だからgetoptsが進化しなくなったのかもしれません）。shift を使った多くのスクリプトに見られるような一部のオプションの指定方法しか使えないといったこともありません。また、宣言的なスクリプトなのでオプションの種類が増えても可読性は損なわれません。

shift はシェルスクリプトを起動するときに指定され引数の値が入った変数 `$2, $3, $4, ...` の値を `$1, $2, $3, ...` にシフトして代入します。もし、git status のように2つのワードからなるコマンドの場合はオプション解析をする前にシフトします。

なお、ここで紹介しているスクリプトは、bash, zsh のどちらでも動作します。本記事は 3200以上もいいねされた https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash の内容をベースに整理・追記した記事です。


## 様々なオプションの指定方法

シェルにコマンドのオプションを入力するときは、**ハイフンから始まるオプション** （たとえば下記の -f）を入力します。

    command  -f

また、ハイフン2つから始まる **ロング オプション** にも対応すべきです。ロング オプション とは2文字以上のアルファベットとハイフンで指定するオプションです。

    command  --flag

ハイフン1つと1文字のアルファベットで指定するショート オプション だけにしか対応していないと、マニュアルに書くコマンドのサンプルやユーザーが開発するシェルスクリプトの可読性が落ちてしまいます。なので、開発するシェルスクリプトには、ショート オプション と同じ機能の ロング オプション も用意します。Linux 標準コマンド以外の有名なコマンドのほとんどは両方のオプションを用意しています。

しかし、**ショート オプション** にもメリットはあります。それは、複数のオプションを連続して指定することができることです。

    command -fh

上記は下記と同じオプションを指定したことになります。

    command -f -h

ロング オプション なら下記のようになります。

    command --flag --help

パラメーター（下記 ~/conf）を持つオプションもあるでしょう。

    command -s ~/conf

-- より右は、ハイフンから始まってもオプションではない引数として扱います。

    command -- -s

他にも標準的なオプションの指定方法がありますが、それらにすべて対応するシェルスクリプトを簡単に開発するスクリプトのテンプレートを紹介します。


## パラメーター付きオプション解析スクリプト

ハイフンから始まるオプション名の右に **パラメーター** を指定するタイプのオプションを解析するスクリプトのテンプレートを紹介します。

ロング オプション

    _try_option.sh  --search-path ./conf

ショート オプション

    _try_option.sh  -s ./conf

オプション名とパラメーターの間にイコール

    _try_option.sh  --search-path=./conf

空白を含むパラメーター

    _try_option.sh  --search-path="./conf 2"

任意の位置のオプション

    _try_option.sh  arg1  --search-path ./conf  arg2

以上のようなオプションを解析をするスクリプトのテンプレートは下記のようになります。編集する箇所は4行目だけです。

    PositionalArgs=()
    while [[ $# -gt 0 ]]; do
        case $1 in
            #//// ここにオプションを追加していきます ////
            --) shift;  PositionalArgs+=("$@"); set --;;
            -*) echo "ERROR: Unknown option $1"; exit 1;;
            *)  PositionalArgs+=("$1"); shift;;
        esac
    done
    set -- "${PositionalArgs[@]}"  #// set $1, $2, ...
    unset PositionalArgs

    #//// ここに スクリプト ファイル の主な処理を書きます ////

-s, --search-path オプションを解析する場合、スクリプトは下記のようになります。下記のスクリプトはそのままコピペして実行できます。オプション名とパラメーターの間が空白の場合とイコールの場合で処理が異なるため **1種類のオプションごとに2行** のスクリプトが必要になります。

~/bin/_try_option.sh

    PositionalArgs=()
    while [[ $# -gt 0 ]]; do
        case $1 in
            -s|--search-path)  Options_SearchPath="$2"; shift; shift;;
            -s=*|--search-path=*) Arg="$1";  Options_SearchPath="${Arg#*=}"; unset Arg; shift;;
            --) shift;  PositionalArgs+=("$@"); set --;;
            -*) echo "ERROR: Unknown option $1"; exit 1;;
            *)  PositionalArgs+=("$1"); shift;;
        esac
    done
    set -- "${PositionalArgs[@]}"  #// set $1, $2, ...
    unset PositionalArgs

    echo "\$Options_SearchPath = \"${Options_SearchPath}\""
    echo "\$1 = \"$1\""
    echo "\$2 = \"$2\""

実行できるようにします（新規作成時のみ）

    chmod +x ~/bin/_try_option.sh

実行します。ただし、~/bin に PATH が通っているものとします。

    _try_option.sh  --search-path ./conf

実行結果

    $Options_SearchPath = "./conf"
    $1 = ""
    $2 = ""

ショート オプション を動作確認してみます。

    _try_option.sh  -s ./conf

実行結果

    $Options_SearchPath = "./conf"
    $1 = ""
    $2 = ""

オプションとそのパラメーターの間にイコールを指定する場合の動作確認をしてみます。

    _try_option.sh  --search-path=./conf

実行結果

    $Options_SearchPath = "./conf"
    $1 = ""
    $2 = ""

空白を含むパラメーターを動作確認してみます。

    _try_option.sh  -s="./conf 2"

実行結果

    $Options_SearchPath = "./conf 2"
    $1 = ""
    $2 = ""

オプションを指定しない場合を動作確認してみます。

    _try_option.sh  arg1

実行結果

    $Options_SearchPath = ""
    $1 = "arg1"
    $2 = ""

オプションと引数の両方を指定した場合を動作確認してみます。

    _try_option.sh  arg1  --search-path ./conf  arg2

実行結果

    $Options_SearchPath = "./conf"
    $1 = "arg1"
    $2 = "arg2"

ハイフンから始まるパラメーターを -- の右に指定した場合を動作確認してみます。

    _try_option.sh  --  -s  arg1

実行結果

    $Options_SearchPath = ""
    $1 = "-s"
    $2 = "arg1"

オプション名が間違っているときにエラーになることを動作確認してみます。

    _try_option.sh  --foo

実行結果

    ERROR: Unknown option --foo

エラーが発生したことが終了コードに入っているか確認します。（0＝エラーなし、それ以外の数値＝エラー）

    echo $?

実行結果

    1


### 複数

上記のテンプレートを使って同じ種類のオプションを複数指定したときは、**後**で（右に）指定したオプションのパラメーターだけが渡されます。下記の場合 ./conf_B だけが渡されます。

    _try_option.sh  -s ./conf_A  -s ./conf_B

同じ種類のオプションのすべてのパラメーターを **配列に格納する** こともできます。たとえば、複数の -s, --search-path オプションのパラメーターを解析して配列に格納する場合、スクリプトは下記のようになります。下記のスクリプトはそのままコピペして実行できます。

~/bin/_try_option.sh

    PositionalArgs=()
    Options_SearchPath=()
    while [[ $# -gt 0 ]]; do
        case $1 in
            -s|--search-path)  Options_SearchPath+=("$2"); shift; shift;;
            -s=*|--search-path=*) Arg="$1";  Options_SearchPath+=("${Arg#*=}"); unset Arg; shift;;
            --) shift;  PositionalArgs+=("$@"); set --;;
            -*) echo "ERROR: Unknown option $1"; exit 1;;
            *)  PositionalArgs+=("$1"); shift;;
        esac
    done
    set -- "${PositionalArgs[@]}"  #// set $1, $2, ...
    unset PositionalArgs

    for elem in "${Options_SearchPath[@]}" ;do
        echo "\$Options_SearchPath = \"${elem}\""
    done

配列用に変更した箇所は次の a〜c です。

a. while ループの前に空の配列で初期化します。

    Options_SearchPath=()

b. オプションを解析するスクリプトの処理を、配列に追加するように変更します。

    -s|--search-path)  Options_SearchPath+=("$2"); shift; shift;;
    -s=*|--search-path=*) Arg="$1";  Options_SearchPath+=("${Arg#*=}"); unset Arg; shift;;

c. 参照するスクリプトを、配列として参照するスクリプトに変更します。

    for elem in "${Options_SearchPath[@]}" ;do
        echo "\$Options_SearchPath = \"${elem}\""
    done

実行できるようにします（新規作成時のみ）

    chmod +x ~/bin/_try_option.sh

実行します。ただし、~/bin に PATH が通っているものとします。

    _try_option.sh  -s ./conf_A  -s ./conf_B

実行結果

    $Options_SearchPath = "./conf_A"
    $Options_SearchPath = "./conf_B"


### 必須

必須オプションが指定されなかったときにエラーにしたいときは、オプション解析を行うスクリプトの直後に下記のようなスクリプトを書きます。

必須の -s, --search-path オプションのパラメーターを `Options_SearchPath` 変数に渡しているときの必須チェックは、

    if [[ "${Options_SearchPath+set}" == "" ]]; then
        echo "ERROR: Not set -s or --search-path option"; exit 1
    fi

`${Options_SearchPath+set}` は、変数 `Options_SearchPath` が設定されていたら `"set"`、設定されていないか unset されていたら `""` を返します。

必須の -s, --search-path オプションの全てのパラメーターを `Options_SearchPath` 配列変数に渡しているときの必須チェックは、オプション解析を行うスクリプトの直後に下記のようなスクリプトを書きます。

    if [[ ${#Options_SearchPath[@]} == 0 ]]; then
        echo "ERROR: Not set -s or --search-path option"; exit 1
    fi

`${#Options_SearchPath[@]}` は、配列 `Options_SearchPath` の要素数です。


## フラグ オプション 解析スクリプト

指定するか指定しないかを選ぶタイプのオプションを解析するスクリプトのテンプレートを紹介します。

ロング オプション

    _try_option.sh  --flag

ショート オプション

    _try_option.sh  -f

複数の ショート オプション

    _try_option.sh  -fh

引数の右のオプション

    _try_option.sh  arg1  --flag

以上のようなオプションを解析をするスクリプトのテンプレートは下記のようになります。編集する箇所は4行目と11行目です。

    PositionalArgs=()
    while [[ $# -gt 0 ]]; do
        case $1 in
            #//// ここにオプションを追加していきます ////
            --) shift;  PositionalArgs+=("$@"); set --;;
            --*) echo "ERROR: Unknown option $1"; exit 1;;
            -*) #// Multiple short name options. e.g.-fh
                Options=$1
                for (( i=1; i<${#Options}; i++ )); do
                    case "-${Options:$i:1}" in
                        #//// ここにフラグの ショート オプション を追加していきます ////
                        *) echo "ERROR: Unknown option -${Options:$i:1}"; exit 1;;
                    esac
                done
                unset Options; shift;;
            *) PositionalArgs+=("$1"); shift;;
        esac
    done
    set -- "${PositionalArgs[@]}"  #// set $1, $2, ...
    unset PositionalArgs

    #//// ここに スクリプト ファイル の主な処理を書きます ////

-f, --flag, -h, --help オプションを解析する場合、スクリプトは下記のようになります。下記のスクリプトはそのままコピペして実行できます。1種類のオプションにつき、**ショート オプション と ロング オプション それぞれ 1行ずつ** 追加します。追加する場所が離れていることに注意してください。それ以外の部分のスクリプトを編集する必要はありません。

~/bin/_try_option.sh

    PositionalArgs=()
    while [[ $# -gt 0 ]]; do
        case $1 in
            --flag)  Options_Flag="yes"; shift;;  #// Without value
            --help)  Options_Help="yes"; shift;;  #// Without value
            --) shift;  PositionalArgs+=("$@"); set --;;
            --*) echo "ERROR: Unknown option $1"; exit 1;;
            -*) #// Multiple short name options. e.g.-fh
                Options=$1
                for (( i=1; i<${#Options}; i++ )); do
                    case "-${Options:$i:1}" in
                        -f)  Options_Flag="yes";;
                        -h)  Options_Help="yes";;
                        *) echo "ERROR: Unknown option -${Options:$i:1}"; exit 1;;
                    esac
                done
                unset Options; shift;;
            *) PositionalArgs+=("$1"); shift;;
        esac
    done
    set -- "${PositionalArgs[@]}"  #// set $1, $2, ...
    unset PositionalArgs

    echo "\$Options_Flag = \"${Options_Flag}\""
    echo "\$Options_Help = \"${Options_Help}\""
    echo "\$1 = \"$1\""
    echo "\$2 = \"$2\""

（メモ）フラグ オプション とパラメーター付きオプションの両方に対応する場合、while ループの中にそれぞれの解析スクリプトを混在させます。

    while [[ $# -gt 0 ]]; do
        case $1 in
            -s|--search-path)  Options_SearchPath="$2"; shift; shift;;
            -s=*|--search-path=*) Arg="$1";  Options_SearchPath="${Arg#*=}"; unset Arg; shift;;
            --flag)  Options_Flag="yes"; shift;;  #// Without value
                :

実行できるようにします（新規作成時のみ）

    chmod +x ~/bin/_try_option.sh

実行します。ただし、~/bin に PATH が通っているものとします。

    _try_option.sh  --flag

実行結果

    $Options_Flag = "yes"
    $Options_Help = ""
    $1 = ""
    $2 = ""

ショート オプション を動作確認してみます。

    _try_option.sh  -f

実行結果

    $Options_Flag = "yes"
    $Options_Help = ""
    $1 = ""
    $2 = ""

複数の ショート オプション を動作確認してみます。

    _try_option.sh  -fh

実行結果

    $Options_Flag = "yes"
    $Options_Help = "yes"
    $1 = ""
    $2 = ""

オプションを指定しない場合を動作確認してみます。

    _try_option.sh  arg1

実行結果

    $Options_Flag = ""
    $Options_Help = ""
    $1 = "arg1"
    $2 = ""

オプションと引数の両方を指定した場合を動作確認してみます。

    _try_option.sh  arg1  --flag  arg2

実行結果

    $Options_Flag = "yes"
    $Options_Help = ""
    $1 = "arg1"
    $2 = "arg2"

ハイフンから始まるパラメーターを -- の右に指定した場合を動作確認してみます。

    _try_option.sh  --flag  --  --help  "arg 1"

実行結果

    $Options_Flag = "yes"
    $Options_Help = ""
    $1 = "--help"
    $2 = "arg 1"

オプション名が間違っているときにエラーになることを動作確認してみます。

    _try_option.sh  --foo

実行結果

    ERROR: Unknown option --foo

エラーが発生したことが終了コードに入っているか確認します。
（0＝エラーなし、それ以外の数値＝エラー）

    echo $?

実行結果

    1