普通に書くと後で問題が発覚するオプション指定
シェルスクリプトに指定されたオプションを、そのままシェルスクリプトから呼び出すコマンドに渡すときは、次のようなコードを想像するでしょう。
example.sh のコード:
if [ "$1" == "--message" ]; then
Options_Message="$2"
fi
echo example.sh --message "${Options_Message}" #// オプションをそのまま渡します
最初のうちはこのコードで期待通り動きます。 しかし、このコードには問題があります。 それは、オプションが指定されていないときでも、オプションを渡してしまっている問題です。
実行コマンド:
$ code example.sh #// Visual Studio Code の場合
$ chmod +x example.sh
$ ./example.sh --message "Hello, world."
example.sh --message Hello, world.
$ ./example.sh
example.sh --message #// オプションを渡してしまっている
シェルスクリプト example.sh に渡された --message オプションをそのまま echo コマンドに渡しています。しかし、example.sh に --message オプションが渡されていなくても、echo コマンドに --message オプションを渡してしまっています。
渡している値は空文字列なので、実質 --message オプションを指定してないと解釈することもできますが、--message オプションを指定しないときのデフォルトの値がある場合、echo コマンドが受け取る値は、example.sh が受け取った値(デフォルト値)と異なる値(空文字列)になってしまいます。 他にも問題がありますが、それらの問題を全て無くすには工夫が必要です。
本記事では、シェルスクリプトに指定されたオプションを、そのままコマンドに渡すには、どう書いたらいいかを説明します。また、簡単に対処できる関数も提供します。 値を取らないフラグだけのオプションにも対応します。
ダウンロード
本書で紹介しているスクリプトのファイルは、GitHub からダウンロードできます。Linux のシェルを開くか、Windows の Git bash を開いて、以下のコマンドを実行してください。
$ cd ${HOME}
$ git clone https://github.com/Takakiriy/shell-script-tutorial
オプション解析部分
オプションを解析する部分のコードは、こちらの記事に掲載しています。
空白文字を含む値への対応
空白文字を含む値が指定される可能性があることに対応するためには、オプションの値が入った変数を参照するときに、ダブルクォーテーションで囲む必要があります。 たとえば、"${Options_Message}" は良いですが、${Options_Message} はダメです。 なぜなら、空白文字が値の終わりであると解釈されて、空白より後の値がコマンドの次の引数になってしまうからです。
実行コマンド:
(zsh では違う動きをします)
$ Options_Message="Hello, world."
$ ./echo-q --message "${Options_Message}" #// OK
'./echo-q' '--message' 'Hello, world.'
$ ./echo-q --message ${Options_Message} #// NG
'./echo-q' '--message' 'Hello,' 'world.' #// Hello, と world. が別れてしまいます
変数を展開して書くとこうなります。
$ ./echo-q --message "Hello, world." #// OK
'./echo-q' '--message' 'Hello, world.'
$ ./echo-q --message Hello, world. #// NG
'./echo-q' '--message' 'Hello,' 'world.' #// Hello, と world. が別れてしまいます
オプションを渡さないことへの対応
コマンドに引数を渡さないようにするには、オプションの値が入った変数を参照するときに、ダブルクォーテーションで囲まないように書く必要があります。 たとえば、${Options_Message} は良いですが、"${Options_Message}" はダメです。 なぜなら、もし、囲むと、オプションが指定されなかったときに、空文字列の引数を渡すことになってしまうからです。
実行コマンド:
(zsh では違う動きをします)
$ Options_Message=""
$ Options_Target="main"
$ TargetOption="--target ${Options_Target}"
$ ./echo-q ${Options_Message} ${TargetOption} #// OK
'./echo-q' '--target' 'main'
$ ./echo-q "${Options_Message}" "${TargetOption}" #// NG
'./echo-q' '' '--target main' #// 空文字列を渡してしまいます
変数を展開して書くとこうなります。
$ ./echo-q --target main #// OK
'./echo-q' '--target' 'main'
$ ./echo-q "" "--target main" #// NG
'./echo-q' '' '--target main' #// 空文字列を渡してしまいます
矛盾する両方の要求に対応する
以上のように、ダブルクォーテーションで囲む必要性と、ダブルクォーテーションで囲まない必要性が発生してしまいました。 どちらかの必要性を諦めなければならないのでしょうか。
別の解決方法があります。 それは、配列を使う方法です。 呼び出すコマンドに渡すときは配列に追加して、呼び出すコマンドに渡さないときは配列に追加しないようにすればいいのです。
example.sh のコード:
if [ "$1" == "--message" ]; then
Options_Message="$2"
shift 2
fi
if [ "$1" == "--target" ]; then
Options_Target="$2"
shift 2
fi
options=()
if [ "${Options_Message}" != "" ]; then
options+=("--message" "${Options_Message}")
fi
if [ "${Options_Target}" != "" ]; then
options+=("--target" "${Options_Target}")
fi
./echo-q "${options[@]}"
シェルスクリプトに渡された全ての引数を、スクリプトから呼び出すコマンドに渡すときに "$@" と書きますが、ユーザーが定義した配列でも同様のことができます。
実行コマンド:
$ code example.sh #// Visual Studio Code の場合
$ chmod +x example.sh
$ ./example.sh --message "Hello, world."
'./echo-q' '--message' 'Hello, world.'
$ ./example.sh --target main
'./echo-q' '--target' 'main'
$ ./example.sh --message "Hello, world." --target main
'./echo-q' '--message' 'Hello, world.' '--target' 'main'
$ ./example.sh
'./echo-q'
AddOptionToArray 関数を使う(フラグにも対応)
options 配列を作る処理を書きやすく読みやすくする AddOptionToArray 関数を紹介します。 オプションの値を取らない フラグ タイプ のオプションにも対応しています。
動作する完全なコードは、本記事の最初で示した場所からダウンロードできます。
書き方のサンプル
function Main() {
local options=()
AddOptionToArray options "--message" "${Options_Message}"
AddOptionToArray options "--target" "${Options_Target}" --default "local"
AddOptionToArray options "--watch" "${Options_Watch}" --flag
./echo-q "${options[@]}"
}
実行コマンド:
$ cd ~/shell-script-tutorial/6-option
$ ./3-delegate.sh --message "Hello, world."
'./echo-q' '--message' 'Hello, world.'
$ ./3-delegate.sh --target main
'./echo-q' '--target' 'main'
$ ./3-delegate.sh --message "Hello, world." --target main
'./echo-q' '--message' 'Hello, world.' '--target' 'main'
$ ./3-delegate.sh --watch
'./echo-q' '--watch'
$ ./3-delegate.sh
'./echo-q'
デフォルト値
編集しているスクリプトに指定できるオプション(例:--target)を指定しなかったときに、そのオプションの値を格納する変数(例:Options_Target)に格納する値(デフォルト値、例:local)は、以下のように設定します。
AddOptionToArray 関数に渡す --default オプション:
AddOptionToArray options "--target" "${Options_Target}" --default "local"
変数が定義されていないときに代入する値:
if ! [[ -v Options_Target ]]; then Options_Target="local" ;fi
変数が定義されていないときに代入する値については、本記事の最初に示したオプション解析をするコードを紹介する記事に詳しく書いてあります。
常に指定
呼び出すコマンドに常にオプションを指定する場合、次のように書きます。 常に指定することで、呼び出すコマンドのデフォルト値の変更に影響されなくなります。
AddOptionToArray options "--target" "${Options_Target}" --always
以下のように AddOptionToArray 関数を呼び出さなくても動きますが、呼び出した方が他のオプションを指定するコードと統一感が出て綺麗に見えます。
options+=("--target" "${Options_Target}")
フラグ
オプションの値を取らない フラグ タイプ のオプション(例:--watch)の場合、AddOptionToArray 関数に --flag オプションを指定します。
AddOptionToArray options "--watch" "${Options_Watch}" --flag
変数が定義されていないときに代入する値は false です。
if ! [[ -v Options_Watch ]]; then Options_Watch="false" ;fi
AddOptionToArray 関数の定義
function AddOptionToArray() {
#// Example:
#// local options=()
#// AddOptionToArray options "--test" "${Options_Test}"
#// AddOptionToArray options "--message" "${Options_Message}" --default "no message"
#// AddOptionToArray options "--search-path" "${Options_SearchPath}" --always
#// AddOptionToArray options "--verbose" "${Options_Verbose}" --flag
#// sub_script.sh "${options[@]}"
local -n arrayRef=$1 #// nameref (Bash 4.3+)
local optionName="$2"
local value="$3"
local thisOption="${4-""}" #// "", "--flag" or "--default". "${1-""}" means that "$1" default is "".
local default="${5-""}" #// "${1-""}" means that "$1" default is "".
if [ "${thisOption}" != "--default" ]; then
default=""
fi
if [ "${thisOption}" == "--flag" ]; then
if [ "${value}" != "" ] && [ "${value}" != "false" ] && [ "${value}" != "no" ]; then
arrayRef+=("${optionName}")
#// else no add
fi
elif [ "${thisOption}" == "--always" ]; then
arrayRef+=("${optionName}" "${value}")
else
if [ "${value}" != "${default}" ]; then
arrayRef+=("${optionName}" "${value}")
#// else no add
fi
fi
}
関数の引数を名前参照する(Call by reference)
AddOptionToArray 関数は、配列に追加する処理をするため、引数を入出力することになります。 それを実現するためには、引数に変数の値 ${options} を渡すのではなく、配列変数の名前 options を渡します。 また、関数の内部では、-n オプションを使って名前参照します。
ただし、bash 4.3以上が必要なので、mac にプリインストールされている bash では使えません。
local -n arrayRef=$1 #// nameref (Bash 4.3+)
名前参照を使うと、渡した変数の別名を定義することになります。 別名というと変な感じがしますが、bash では呼び出し元の関数で参照している変数を、呼び出し先の関数の中でも参照できるので(できてしまうので)、それに対する別名を定義するので別名なのです。
ただし、呼び出し元に指定した名前が、名前参照を使う変数名と同じ場合、circular name reference エラーになります。 そのエラーにならないよう、名前参照を使う変数名に限り、末尾に Ref をつけるというルールを決めると良いです。
function Main() {
local arrayRef=() #// ルールに違反した変数名
AddOptionToArray arrayRef "--message" "${Options_Message}"
}
function AddOptionToArray() {
local -n arrayRef=$1 #// circular name reference エラー
呼び出すことが多いコマンドのオプションはグローバル変数にする
他の シェルスクリプト ファイル を呼び出すコードを書くことが多いときは、オプションの配列を毎回作るのではなく、グローバル変数でオプションの配列を作っておくと便利です。
function Main() {
sub-script "${SubScriptOptions[@]}"
}
function Sub1() {
sub-script "${SubScriptOptions[@]}"
}
function Sub2() {
sub-script "${SubScriptOptions[@]}"
}
function ModifyGlobalVariables() {
SubScriptOptions=() #// local が書いてないのでグローバル変数です
AddOptionToArray SubScriptOptions "--message" "${Options_Message}"
AddOptionToArray SubScriptOptions "--target" "${Options_Target}" --default "local"
}