Bashには、スクリプトへ渡された引数、直前の終了状態、プロセスIDなどを表す特殊パラメーターがあります。代表例は$#、$@、$?、$$、$!、$0です。短く書ける反面、引用を外すと空白やワイルドカードを含む引数が壊れ、直前のコマンドを一つ追加しただけで$?が変わるなど、誤用しやすい機能でもあります。本稿はBashスクリプトを想定し、それぞれの意味、引用、実運用の検証方法を整理します。
特殊パラメーターとは
シェルパラメーターは値を保持する名前や番号です。利用者が通常代入する変数に加え、Bash自身が意味を与える特殊パラメーターがあります。これらの一部は参照できても通常の代入では変更できません。位置パラメーター$1、$2などはスクリプトや関数の引数です。10番目以降は${10}のように波括弧が必要で、$10は$1の後に文字0が続く解釈になり得ます。
$0:スクリプト名またはシェル名
$0は起動方法に応じてスクリプト名やシェル名を表します。相対パス、絶対パス、シンボリックリンク経由、bash -c、source、関数では値が変わります。単純な利用方法表示には使えますが、信頼できる実ファイルパスやセキュリティ境界だと決めつけません。スクリプト自身の場所を求める処理は、シンボリックリンクやsourceをどう扱うか仕様を決めて実装します。
printf 'program: %s\n' "$0"
$0を引用せず表示すると、空白を含むパスが分割されます。エラー文へ出すときもprintfの書式と値を分けます。
$#:引数の個数
$#は位置パラメーターの個数です。必須引数の検証は、個々の値へ触る前に行います。引数が不足した状態でset -uが有効だと、$1参照がエラーになるためです。引数の個数だけでは内容の安全性を保証しないので、パス、数値、列挙値など用途ごとの検証を続けます。
if (( $# != 2 )); then
printf 'usage: %s INPUT OUTPUT\n' "$0" >&2
exit 2
fi
input=$1
output=$2
秘密値をコマンドライン引数で渡すと、プロセス一覧やシェル履歴に露出する可能性があります。パスワードやトークンは、承認された秘密管理、保護されたファイル記述子、対話入力など別の経路を使います。
$@:引数を一つずつ保ったまま扱う
ダブルクォートで囲んだ"$@"は、各位置パラメーターを別々の引数として展開します。空白を含む値や空文字も境界を保てるため、引数を別コマンドや関数へ転送する標準的な形です。引用を外した$@は単語分割とファイル名展開を受け、入力が壊れたり対象が増えたりします。
for argument in "$@"; do
printf '<%s>\n' "$argument"
done
some_function "$@"
外部入力をevalで再評価せず、配列や"$@"で値の境界を保ちます。引数にシェル記号が含まれていても、適切に引用されていれば文字列として渡せます。
$*:結合文字列としての扱い
ダブルクォート内の"$*"は、すべての位置パラメーターをIFSの先頭文字で連結した一つの単語として展開します。ログ用の要約など限定的な用途はありますが、元の引数境界を失います。引数を転送するときは"$@"を使います。未引用の$*と未引用の$@はどちらも再分割され、予測が難しいため避けます。
printf 'joined: %s\n' "$*"
引数に秘密情報が含まれ得るなら、結合してログへ出してはいけません。トラブルシューティングでは引数個数や許可された非機密オプションだけを記録します。
$?:直前の終了状態
$?は直前に実行したパイプラインの終了状態です。慣例では0が成功、0以外が失敗ですが、grepの一致なしのように非0が想定内の状態を示すコマンドもあります。対象コマンドのマニュアルを確認します。$?は次のprintfや代入以外のコマンドで上書きされるため、直後に変数へ保存します。
some_read_only_check
status=$?
printf 'status=%s\n' "$status"
if (( status != 0 )); then
exit "$status"
fi
Bashではif command; thenの形で直接分岐する方が読みやすいこともあります。パイプラインの既定の終了状態は末尾コマンドに由来するため、途中の失敗を検出するならset -o pipefailやPIPESTATUSを設計に含めます。
$$とBASHPID:現在のBashを識別する
$$はシェルのプロセスIDへ展開されます。作業ファイル名の衝突回避に使われる例がありますが、PIDは再利用され、攻撃者が予測できるため、安全な一時ファイル生成には不十分です。mktempなどOSの安全な作成機能を使い、適切な権限と終了時の後片付けを設計します。またBashでは、丸括弧で作るサブシェル内でも$$が親Bashの値を保つ場合があり、実行中BashプロセスのIDを示すBASHPIDとは異なります。
printf 'dollar-dollar=%s BASHPID=%s\n' "$$" "$BASHPID"
( printf 'subshell: dollar-dollar=%s BASHPID=%s\n' "$$" "$BASHPID" )
$!:最後にバックグラウンド起動したプロセス
$!は最後にバックグラウンドへ送ったジョブのプロセスIDです。起動直後に変数へ保存し、waitでその子プロセスの完了と終了状態を回収します。その間に別のバックグラウンド処理を起動すると$!は変わります。PIDだけを後で信頼して任意プロセスを操作せず、自分が起動した子とライフサイクルを結び付けます。
some_background_job &
job_pid=$!
printf 'started child pid=%s\n' "$job_pid"
if wait "$job_pid"; then
printf '%s\n' 'child succeeded'
else
status=$?
printf 'child failed: %s\n' "$status" >&2
fi
バックグラウンド処理がさらに子を作る、デーモン化する、パイプラインである場合、$!のPIDと実際のサービス全体は一致しないことがあります。長期サービスはsystemdなどのサービス管理へ任せます。
$-:現在のシェルオプション
$-には有効な1文字オプションが並びます。iの有無でBashが対話的かを確認できます。ただし、端末へ接続されていること、利用者が応答できること、操作を許可してよいこととは別です。対話性はプロンプトや補完の表示調整に使い、権限判定には使いません。
case $- in
*i*) printf '%s\n' 'interactive' ;;
*) printf '%s\n' 'non-interactive' ;;
esac
$_、$-、その他は用途を限定する
$_はBash起動直後やコマンド実行後など文脈により値が変わり、直前コマンドの最後の引数に関係することがあります。可読性と移植性が低いため、重要な業務値は明示変数へ保存します。$-もデバッグ情報として全体を出すより、必要な特定オプションだけを確認します。特殊パラメーターは簡便な状態参照であり、永続的な設定ストアではありません。
shiftで位置パラメーターを進める
shiftは$2を$1へ移すように位置パラメーターを左へ進め、$#を減らします。オプション解析で使えますが、残数を超えてshiftすると非0で失敗します。自作解析が複雑になるならBashのgetoptsや、利用言語の引数解析ライブラリを使います。値を取るオプションでは、次の引数が存在するか確認してからshiftします。
while (( $# > 0 )); do
case $1 in
--verbose) verbose=1; shift ;;
--) shift; break ;;
-*) printf 'unknown option: %s\n' "$1" >&2; exit 2 ;;
*) break ;;
esac
done
関数内では位置パラメーターが入れ替わる
関数を呼ぶと、関数内の$1、$#、$@は関数へ渡した引数を表します。呼び出し元スクリプトの位置パラメーターは関数終了後に戻ります。グローバルな$@を当然に参照する関数は依存が分かりにくいので、必要な値を明示的に渡します。再帰関数やsourceされたライブラリでは特に重要です。
安全なデバッグ方法
- 値そのものではなく、引数個数、終了状態、許可されたフラグなど必要最小限を記録する。
- 秘密値を含み得る
set -xを本番で無条件に有効化しない。 "$@"で引数境界を保ち、ログ表示のために一つの文字列へ結合しない。$?、$!は対象コマンド直後に説明的な名前の変数へ保存する。- PIDを所有権や認証の証拠にせず、waitとサービス管理で子プロセスを追跡する。
- Bash版、起動方法、対話/非対話、pipefailなど再現に必要な条件を記録する。
引数転送は"$@"、個数は$#、結果は$?、子プロセスは$!というように役割を分け、参照直後に明示変数へ保存すると読みやすくなります。特殊パラメーターを引用し、秘密情報とプロセス制御の境界を別途設計することが、安全なBashスクリプトの基本です。

コメント