Bashのシェルスクリプトでは、変数へ値を入れることより、その値を「いつ、何語として、どの条件で展開するか」を制御することが重要です。引用を省いた変数展開は、空白による単語分割やワイルドカード展開を受け、想定外の引数数になることがあります。本稿では、"$var"、${var}、既定値演算子、配列、here-documentを使い、意図を明確にする方法をBash基準で解説します。
原則は変数展開を二重引用符で囲む
変数の値を一つの引数としてコマンドへ渡すなら、基本形は"$variable"です。引用しない$variableは、展開後にIFSによる単語分割とファイル名展開を受けます。値に空白、改行、*、?、角括弧が含まれると、引数が増えたり現在ディレクトリのファイル名へ置き換わったりする可能性があります。
report_name='monthly report [final].txt'
printf 'name=%s\n' "$report_name"
wc -c -- "$report_name"
printfでは値を書式文字列にせず、固定した%sの引数として渡します。printf "$value"のようにすると、値に含まれるパーセント指定やバックスラッシュが書式として解釈される恐れがあります。ファイル名を受け取るコマンドには、引用に加えて--を置き、先頭がハイフンの名前をオプションと誤認しないようにします。
波括弧で変数名の境界を明示する
${name}は$nameと同じ基本展開ですが、直後の文字との境界を明確にできます。"$name_backup"はnameではなくname_backupという変数を参照します。nameの値に接尾辞を付けたい場合は"${name}_backup"と書きます。10番目以降の位置パラメーターも${10}のように波括弧が必要です。
name='database'
archive_name="${name}_backup"
printf '%s\n' "$archive_name"
printf 'argument10=%s\n' "${10-}"
波括弧は見た目のためだけでなく、置換、長さ、部分文字列、既定値といったBashのパラメーター展開演算子を使う入口です。ただし複雑な一行へ演算子を重ねると読みづらくなります。運用スクリプトでは中間結果を名前付き変数へ分け、各入力が未設定、空文字、通常値のどれかをテストします。
未設定と空文字を区別して既定値を選ぶ
${var:-word}はvarが未設定または空文字ならwordを展開し、${var-word}は未設定のときだけwordを使います。コロンの有無で空文字の扱いが変わります。空文字を利用者の明示的な選択として認める設定では、この違いが重要です。どちらも展開結果を返すだけで、元の変数へ値を代入しません。
unset region
printf 'region=%s\n' "${region:-ap-northeast-1}"
region=''
printf 'colon=%s\n' "${region:-fallback}"
printf 'no_colon=%s\n' "${region-fallback}"
${var:=word}は未設定または空ならwordを代入したうえで展開し、${var:?message}は条件を満たさないと標準エラーへメッセージを出します。非対話シェルでは後者が終了を伴うため、外部入力の必須検証に使えますが、秘密値そのものをメッセージへ含めません。位置パラメーターや特殊パラメーターでは代入演算子を使えない場合があります。
config_dir=${CONFIG_DIR:-/etc/myapp}
: "${API_ENDPOINT:?API_ENDPOINT must be set and non-empty}"
printf 'config=%s\n' "$config_dir"
代入する展開と代入しない展開を選ぶ
${v:-word}: vが未設定または空ならwordを返す。vは変更しない。${v-word}: vが未設定ならwordを返す。空文字はそのまま。${v:=word}: vが未設定または空ならwordをvへ代入して返す。${v:?message}: vが未設定または空なら診断を出す。必須値の確認向け。${v:+word}: vが設定済みかつ空でなければwordを返し、そうでなければ空。
これらのword部分も必要に応じてパラメーター展開、コマンド置換、算術展開などを受けます。既定値のつもりで副作用のあるコマンド置換を入れると、条件次第で処理が実行されます。既定値は静的な文字列か、事前に検証した変数にし、外部コマンドの成功・失敗は別の文で扱うと流れが明確になります。
長さ・前後削除・置換を使う
${#var}は長さ、${var#pattern}と${var##pattern}は先頭から最短・最長一致を削除し、${var%pattern}と${var%%pattern}は末尾側を削除します。${var/pattern/string}は最初の一致を置換します。patternは正規表現ではなく、Bashのパターンマッチ規則です。
path='/srv/archive/report.csv'
base=${path##*/}
dir=${path%/*}
stem=${base%.csv}
printf 'dir=%s base=%s stem=%s length=%s\n' "$dir" "$base" "$stem" "${#base}"
この方法は文字列操作であり、パスの正規化や実在確認を行いません。末尾スラッシュ、ルート、改行、異なる拡張子、空文字を含む入力で期待値をテストします。パターンを変数から受け取る場合、引用位置によってパターン文字を有効にするか文字どおりにするかが変わるため、未検証の入力をそのまま置換式へ入れないようにします。
位置パラメーターは “$@” で保持する
スクリプトへ渡された引数を別のコマンドや関数へそのまま転送するときは"$@"を使います。二重引用符内の"$@"は、各位置パラメーターを別々の語として保持します。$*、引用なしの$@、"$*"は意味が異なり、空白を含む引数や空の引数が失われることがあります。
show_arguments () {
printf 'count=%s\n' "$#"
printf '<%s>\n' "$@"
}
show_arguments "$@"
オプション解析後に残った引数を転送する場合も、shift後の"$@"を使います。値を一つのコマンド文字列へ連結して再解釈するのではなく、コマンド名と引数を配列に保存して"${command[@]}"で実行すると、引数境界を維持できます。特に外部入力へevalを適用する設計は避けます。
複数値は文字列ではなく配列で管理する
Bash配列は、空白を含む複数の値をそれぞれ独立した要素として保持できます。全要素を個別の引数にするには"${array[@]}"を使います。引用付きの"${array[*]}"は、IFSの先頭文字で連結した一語になるため、コマンド引数の転送には通常向きません。要素数は${#array[@]}で確認できます。
files=('first report.txt' 'second report.txt' '--summary')
printf 'elements=%s\n' "${#files[@]}"
printf 'file=<%s>\n' "${files[@]}"
配列を使っても、要素を展開するときの引用を省けば単語分割とファイル名展開が起こり得ます。添字も算術式として解釈される場面があり、連想配列や間接展開には追加の注意が必要です。利用者が指定した変数名を${!name}で間接参照するより、許可したキーだけをcaseで対応付ける方が監査しやすくなります。
展開させたくない文字列は単一引用符を使う
単一引用符の中では、各文字が文字どおりに保持されます。'$HOME'はホームディレクトリへ展開されず、ドル記号を含む文字列になります。単一引用符自体を単一引用符の内側へ直接置くことはできないため、必要なら引用を一度閉じるか、用途に応じてprintfの引数を組み合わせます。
literal='$HOME and ${USER}'
printf '%s\n' "$literal"
printf '%s\n' 'price: $10'
二重引用符内では、ドル記号による展開、バッククォートによる旧式のコマンド置換、特定のバックスラッシュ処理が残ります。単一引用符と二重引用符は「安全・危険」の二択ではなく、必要な展開だけを許すための道具です。値を展開したい箇所は二重引用符、完全な固定文字列は単一引用符という意図をコードレビューで確認します。
here-documentの展開を区切り語で制御する
here-documentでは、区切り語を引用しないと本文でパラメーター展開、コマンド置換、算術展開が行われます。区切り語を<<'EOF'のように引用すると本文を展開しません。設定テンプレートや説明文へドル記号をそのまま出したい場合は、引用した区切り語を使うと意図が明確です。
cat <<'LITERAL_TEXT'
$HOME is not expanded here.
${USER} also remains literal.
LITERAL_TEXT
cat <<EXPANDED_TEXT
current user: $USER
EXPANDED_TEXT
外部入力をhere-documentへ埋め込む場合も、出力先の形式に応じたエスケープが別途必要です。シェルで展開されないことは、JSON、YAML、SQL、HTMLとして安全であることを意味しません。また、秘密値を展開するhere-documentをデバッグトレースへ出すと漏えいする可能性があります。
nounsetは補助策として段階的に使う
set -uまたはset -o nounsetは、未設定パラメーターの展開をエラーにするBashオプションです。タイプミスの発見に役立ちますが、空文字とは区別され、位置パラメーターや配列、互換性コードで意図しない終了を招くことがあります。既定値が妥当な箇所では${var:-default}、存在だけを確認する箇所では[[ -v var ]]など、要件を明記します。
シェルオプションだけで入力検証が完成するわけではありません。値の形式、許可範囲、長さ、パスの所属、秘密情報の扱いを個別に検証します。展開結果をログへ出すときは、認証情報や個人情報を伏せ、set -xが展開後の引数を記録する点にも注意します。
確認チェックリスト
- 一つの引数として渡す変数を二重引用符で囲んだ
- 変数名の直後に文字が続く箇所で
${name}を使った - 未設定と空文字のどちらを既定値対象にするか決めた
- 引数転送は
"$@"、複数値は引用した配列展開を使った - 外部入力をコマンド文字列へ連結して再解釈していない
- here-documentで展開の要否を区切り語の引用により明示した
- 異常系として空、空白、改行、ワイルドカード、先頭ハイフンをテストした
変数展開の制御は、シェルスクリプトの予測可能性を左右します。通常の値は"$var"、境界や条件付き展開は"${var...}"、複数引数は"$@"や"${array[@]}"を基本にすると、値とシェル構文の境界を保てます。便利な演算子を使うほど、未設定・空・特殊文字を含むテストをセットで用意しましょう。

コメント