🐙

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

に公開

本記事はこちらの記事の改訂版です。set -u 対応を追加。 unset PositionalArgs までのコードは変わっていません

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

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

my-shell-script  --version
my-shell-script  -f
my-shell-script  --search-path ./conf

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

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

shift コマンド は、シェルスクリプトを起動するときに指定された 2つ目以降の引数の値が入った変数 $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

ロング オプション なら下記のようにハイフンが 2つになります。

command --flag --help

パラメーター(下記 ~/conf)を持つオプションもあります。 パラメーターを持つか持たないかは、オプションの仕様によって決まります。 ハイフンが 2つだから ロング オプション といったように、書いてある文字からパラメーターを持つか持たないかを判断することはできません。

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

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

#//// 以下のコードは、下のほう(Main 関数を呼び出す直前)に書くと Main 関数の定義をすぐ読めるようになります。 ////
#// Set default values. "! -v" means that variable is not defined.
#// if ! [[ -v __Variable__ ]]; then  __Variable__=""  ;fi
#// または
#// Set default values.
#// "${__Variable__-"__Default__"}" returns "__Default__" if __Variable__ is NOT defined; otherwise, it returns "${__Variable__}".
#//// ここにオプションを追加していきます ////

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

実際に動くサンプルコードを示します。

~/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

#//// 以下のコードは、下のほう(Main 関数を呼び出す直前)に書くと Main 関数の定義をすぐ読めるようになります。 ////

set -u
    #// set -u is optional.
#// Set default values. "! -v" means that variable is not defined.
#// if ! [[ -v __Variable__ ]]; then  __Variable__=""  ;fi
if ! [[ -v Options_SearchPath ]]; then  Options_SearchPath=""  ;fi
#// または
#// Set default values.
#// "${__Variable__-"__Default__"}" returns "__Default__" if __Variable__ is NOT defined; otherwise, it returns "${__Variable__}".
Options_SearchPath="${Options_SearchPath-""}"

#// Example
echo "\$Options_SearchPath = \"${Options_SearchPath}\""
NumOfArguments="$#"
if (( "${NumOfArguments}" >= 1 )); then
    echo "\$1 = \"$1\""
else
    echo "\$1 is not defined."
fi
if (( "${NumOfArguments}" >= 2 )); then
    echo "\$2 = \"$2\""
else
    echo "\$2 is not defined."
fi

💡 コメント Set default values. とそれ以下のコードについては、set -u に関する説明で解説します。

スクリプトを作ります

mkdir -p  ~/bin
code  ~/bin/_try_option.sh

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

chmod +x ~/bin/_try_option.sh

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

_try_option.sh  --search-path ./conf

実行結果

$Options_SearchPath = "./conf"
$1 is not defined.
$2 is not defined.

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

_try_option.sh  -s ./conf

実行結果

$Options_SearchPath = "./conf"
$1 is not defined.
$2 is not defined.

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

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

実行結果

$Options_SearchPath = "./conf"
$1 is not defined.
$2 is not defined.

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

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

実行結果

$Options_SearchPath = "./conf 2"
$1 is not defined.
$2 is not defined.

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

_try_option.sh  arg1

実行結果

$Options_SearchPath = ""
$1 = "arg1"
$2 is not defined.

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

_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

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

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

ロング オプション

_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

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

#//// 以下のコードは、下のほう(Main 関数を呼び出す直前)に書くと Main 関数の定義をすぐ読めるようになります。 ////
#// Set default values. "! -v" means that variable is not defined.
#// if ! [[ -v __Variable__ ]]; then  __Variable__=""  ;fi
#// または
#// Set default values.
#// "${__Variable__-"__Default__"}" returns "__Default__" if __Variable__ is NOT defined; otherwise, it returns "${__Variable__}".
#//// ここにオプションを追加していきます ////

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

~/bin/_try_option.sh

PositionalArgs=()
while [[ $# -gt 0 ]]; do
    case $1 in
        --flag)  Options_Flag="true"; shift;;
        --help)  Options_Help="true"; shift;;
        --) 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="true";;
                    -h)  Options_Help="true";;
                    *) 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

#//// 以下のコードは、下のほう(Main 関数を呼び出す直前)に書くと Main 関数の定義をすぐ読めるようになります。 ////

set -u
    #// set -u is optional.
#// Set default values. "! -v" means that variable is not defined.
#// if ! [[ -v __Variable__ ]]; then  __Variable__=""  ;fi
if ! [[ -v Options_Flag ]]; then  Options_Flag="false"  ;fi
if ! [[ -v Options_Help ]]; then  Options_Help="false"  ;fi
#// または
#// Set default values.
#// "${__Variable__-"__Default__"}" returns "__Default__" if __Variable__ is NOT defined; otherwise, it returns "${__Variable__}".
Options_Flag="${Options_Flag-"false"}"
Options_Help="${Options_Help-"false"}"

#// Example
NumOfArguments="$#"
echo "\$Options_Flag = \"${Options_Flag}\""
echo "\$Options_Help = \"${Options_Help}\""
if (( "${NumOfArguments}" >= 1 )); then
    echo "\$1 = \"$1\""
else
    echo "\$1 is not defined."
fi
if (( "${NumOfArguments}" >= 2 )); then
    echo "\$2 = \"$2\""
else
    echo "\$2 is not defined."
fi

(メモ)フラグ オプション とパラメーター付きオプションの両方に対応する場合、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="true"; shift;;
            :

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

chmod +x ~/bin/_try_option.sh

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

_try_option.sh  --flag

実行結果

$Options_Flag = "true"
$Options_Help = "false"
$1 is not defined.
$2 is not defined.

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

_try_option.sh  -f

実行結果

$Options_Flag = "true"
$Options_Help = "false"
$1 is not defined.
$2 is not defined.

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

_try_option.sh  -fh

実行結果

$Options_Flag = "true"
$Options_Help = "true"
$1 is not defined.
$2 is not defined.

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

_try_option.sh

実行結果

$Options_Flag = "false"
$Options_Help = "false"
$1 is not defined.
$2 is not defined.

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

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

実行結果

$Options_Flag = "true"
$Options_Help = "false"
$1 = "--help"
$2 = "arg 1"

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

_try_option.sh  --foo

実行結果

ERROR: Unknown option --foo

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

echo $?

実行結果

1

デフォルト値と必須オプション

デフォルト値、未定義エラーの set -u への対応

ユーザーがオプションを指定しなかったときは、スクリプト側でデフォルト値を設定します。

また、もし、set -u が実行されていたら、未定義の環境変数を参照したときにエラーメッセージが表示されます。 さらに set -e を実行していたら、エラーメッセージを表示するだけでなく実行中のスクリプトを終了します。 これらのモードで実行するスクリプトを作る場合、オプションが指定されなかったときの未定義エラーを回避しなければなりません。

set -u
echo "${unknown}"

_try_option.sh: line 2: unknown: unbound variable

デフォルト値

set -u を実行しなかった場合、未定義の環境変数を参照すると空文字列が返ります。 デフォルト値が空文字列ならコードを書く必要はありません。

デフォルト値が空文字列以外なら以下のようにコードを書きます。 ユーザーが フラグ オプション を指定しなかったときは、スクリプト側で "false" を設定します。

#// Set default values.
if [ "${Options_ConfigPath}" == "" ]; then  Options_ConfigPath="${HOME}/.my_script"  ;fi
if [ "${Options_Help}" == "" ]; then  Options_Help="false"  ;fi

set -u 対応デフォルト値

set -u を実行するスクリプトの場合、以下のように -v を使って、未定義エラーを回避して、デフォルト値を設定します。

set -u
    #// set -u is optional.

#// Set default values. "! -v" means that variable is not defined.
if ! [[ -v Options_SearchPath ]]; then  Options_SearchPath=""  ;fi
if ! [[ -v Options_ConfigPath ]]; then  Options_ConfigPath="${HOME}/.my_script"  ;fi

未定義の判定が必要になるケース(未定義を許容するケース)は、基本的にはオプション解析のケースでしか発生しないので、-v の意味をコメントで書いてすぐ読めるようにしておくことをお勧めします。 通常、未定義エラーが発生したら、未定義かどうかを判定するコードを書くのではなく、定義するコードを書きます。

mac にプリインストールされた bash 3.x で動作するスクリプトで set -u を実行するスクリプトを作る場合、-v が使えないので以下のように特殊な書き方で、未定義エラーを回避して、デフォルト値を設定します。 すでに環境変数が定義済みであれば、値は変更されないという動作は -v を使ったコードと同じです。

#// Set default values.
#// "${__Variable__-"__Default__"}" returns "__Default__" if __Variable__ is NOT defined; otherwise, it returns "${__Variable__}".
Options_SearchPath="${Options_SearchPath-""}"
Options_ConfigPath="${Options_ConfigPath-"${HOME}/.my_script"}"

一見すると常に環境変数の値を設定しているように見え、上書きされてしまったのではないかと考えてしまうため、コーディング ルール 的には非推奨です。

もちろん、HomeBrew を使って最新の bash を使うことを想定したスクリプトであれば、-v を使ったコードを使うことができます。

必須オプション

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

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

オプションに空文字列を指定することがない場合

必須チェックの前に、未定義ならデフォルト値として空文字列を代入し、空文字だったらエラーにします。

通常(set -u を呼び出さない場合):

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

set -u を呼び出した場合:

set -u
    #// set -u is optional.
#// Set default values. "! -v" means that variable is not defined.
if ! [[ -v Options_SearchPath ]]; then  Options_SearchPath=""  ;fi

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

mac の bash 3.x で set -u を呼び出した場合:

set -u
    #// set -u is optional.
#// Set default values.
#// "${__Variable__-"__Default__"}" returns "__Default__" if __Variable__ is NOT defined; otherwise, it returns "${__Variable__}".
Options_SearchPath="${Options_SearchPath-""}"

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

オプションに空文字列を指定できる場合

set -u を呼び出しても呼び出さなくても:

set -u
    #// set -u is optional.

#// Set default values. "! -v" means that variable is not defined.
if ! [[ -v Options_SearchPath ]]; then
    echo "ERROR: Not set -s or --search-path option"; exit 1
fi

mac の bash 3.x で set -u を呼び出した場合:

set -u
    #// set -u is optional.

#// "${__Variable__+"_is_defined"}" returns "_is_defined" if __Variable__ is defined even if "${__Variable__}" == ""; otherwise, it returns "".
if [ "${Options_SearchPath+"_is_defined"}" != "_is_defined" ]; then
    echo "ERROR: Not set -s or --search-path option"; exit 1
fi

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

複数の同じオプション(配列)の場合

配列の場合(後述)、必ず変数が定義されているので、未定義に対応する必要はありません。 要素数が 0 ならエラーにします。

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

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

複数の同じオプション(配列)

上記のテンプレートを使って同じ種類のオプションを複数指定したときは、で(右に)指定したオプションのパラメーターだけが渡されます。下記の場合 ./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

set -u
    #// set -u is optional.

#//// Options_SearchPath が未定義の場合はないため、それに対応するコードは不要です

#// Example
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"

getoptsgetopts

Discussion