Linux: シェルスクリプトの終了ステータスを正確に取得する方法

Linuxのシェルスクリプトで終了ステータスを正確に取得する要点は、対象コマンドの直後に値を保存し、単体コマンド、パイプライン、関数、バックグラウンド処理を区別することです。Bashでは0が成功、0以外が失敗を表し、直前のコマンドの値は$?で参照できます。ただしprintfや代入以外の確認コマンドを一つ挟むだけで、$?はその新しいコマンドの状態へ置き換わります。

目次

終了ステータスの基本範囲と意味

Bashが扱う終了ステータスは0から255の範囲です。0は成功、非0は失敗という大枠は共通ですが、非0の細かな意味はコマンドごとの仕様です。Bashではコマンドが見つからない場合に127、見つかっても実行できない場合に126、致命的シグナルNで終了した場合に128+Nを使います。独自コマンドの値を根拠なく同じ意味へ当てはめません。

some_command
status=$?
printf 'status=%s\n' "$status"

この代入では右辺の$?が先に展開されるため、対象の値をstatusへ保存できます。画面に表示しただけで判断せず、コマンドの公式マニュアルで値の意味を確認します。たとえば検索結果なしを1で示すツールなど、実行エラーではない非0を返す設計もあります。成功・該当なし・処理失敗を仕様に沿って分けます。

条件分岐ではコマンドを直接評価する

成功か失敗で分岐するだけなら、[ $? -eq 0 ]を後から評価するより、ifの条件位置へ対象コマンドを直接置く方が安全です。失敗側の先頭では、条件となったコマンドのステータスを保存できます。ログ出力は保存後に行います。

if generate_report --output report.txt; then
  printf '%s\n' 'report completed'
else
  status=$?
  printf 'report failed: status=%s\n' "$status" >&2
  exit "$status"
fi

if ! generate_report; then status=$?という形は注意が必要です。!は終了ステータスを論理反転するため、失敗して分岐へ入った時点の$?は反転後の0です。元の値が必要なら、上のように反転せずelse側で保存します。&&や||でも評価対象が変わるため、どのコマンドの値かを明記します。

パイプラインは既定で最後のコマンドの状態になる

Bashの既定では、producer | filter | writer全体の終了ステータスは最後のwriterの値です。入力側が失敗しても、後段が正常終了すると全体が0に見える場合があります。set -o pipefailを有効にすると、全要素が成功なら0、失敗があれば最も右側で非0になったコマンドの値がパイプライン全体の値になります。

(
  set -o pipefail
  if produce_data | validate_data | write_report; then
    printf '%s\n' 'pipeline completed'
  else
    status=$?
    printf 'pipeline failed: status=%s\n' "$status" >&2
    exit "$status"
  fi
)

例ではpipefailの影響をサブシェル内へ閉じ込めています。pipefailは各要素の値を一覧で返す機能ではなく、全体の代表値を決める規則です。また、読み手が必要な分だけ取得して正常終了するパイプラインでは、入力側がSIGPIPEで非0になることがあります。既存処理へ一律に追加せず、正常系と各段の失敗をテストします。

各パイプ要素はPIPESTATUSを直後にコピーする

Bash固有の配列PIPESTATUSには、直前に実行したフォアグラウンドパイプラインの各コマンドの値が左から順に入ります。ただし単純コマンドを実行した後にも更新されます。したがって、パイプライン直後の最初の処理として別の配列へコピーします。

produce_data | validate_data | write_report
statuses=("${PIPESTATUS[@]}")
printf 'producer=%s validator=%s writer=%s\n' \
  "${statuses[0]}" "${statuses[1]}" "${statuses[2]}"

コピーより先にstatus=$?やprintfを別コマンドとして実行すると、PIPESTATUSはそのコマンドに対応する新しい内容へ変わります。全体の$?と配列を同時に必要とする設計は複雑になりやすいため、各段の配列から期待する判定を組み立てるか、各処理を一時ファイルや明示的な関数呼び出しへ分けてテストしやすくします。

関数の戻り値とデータ出力を分ける

シェル関数の終了ステータスは、明示したreturn n、または関数本体で最後に実行したコマンドの値です。関数の最後に診断用printfを追加すると、その表示が成功した0で本来の失敗を上書きすることがあります。失敗値を先に保存し、必要ならログ後にreturn "$status"します。

check_report() {
  validate_report "$1"
  local status=$?
  if (( status != 0 )); then
    printf 'validation failed: %s\n' "$1" >&2
  fi
  return "$status"
}

if check_report report.txt; then
  printf '%s\n' 'valid'
fi

終了ステータスは短い制御情報であり、文字列や大きな数値を返す経路ではありません。関数が生成するデータは標準出力、診断は標準エラー、成功・失敗は終了ステータスへ分離します。コマンド置換で標準出力を取得するときも、代入コマンドのステータスを直後に確認し、空文字と処理失敗を同一視しません。

exitとスクリプト最終値を明示する

Bashスクリプトは、構文エラーなどを除けば最後に実行したコマンドの終了ステータスを返します。exitへ引数を渡さない場合も直前のコマンドの値です。この暗黙動作へ依存すると、最後に追加したログ出力で値が変わるため、呼び出し元へ返す値は保存してexit "$status"と明示します。

run_batch
status=$?
printf 'batch finished: status=%s\n' "$status"
exit "$status"

EXITトラップで後片付けや記録をする場合も、ハンドラーの最初で$?を保存します。認証情報や顧客データをエラーログへ出さず、後片付けの失敗と本処理の失敗を別に記録します。終了値の割り当て表はスクリプトの利用者向け仕様として文書化し、同じ値を複数の意味へ使い回しません。

バックグラウンド処理はwaitで回収する

コマンドを&で開始した直後の$?は、バックグラウンド開始自体の状態であり、処理が最終的に成功したかを示しません。直後の$!でプロセスIDを保存し、必要な時点でwait "$pid"を実行します。waitは指定した子プロセスの終了ステータスを返します。

long_running_check &
pid=$!

if wait "$pid"; then
  printf '%s\n' 'background check completed'
else
  status=$?
  printf 'background check failed: status=%s\n' "$status" >&2
fi

複数ジョブを扱う場合、PIDと処理名の対応を配列などで保持し、どのwaitがどのジョブを回収したか記録します。引数なしのwaitはすべての実行中ジョブを待って0を返す仕様なので、個別の失敗値が必要な監視には不十分です。子ではないPIDや、すでに回収方法を失ったジョブも区別します。

set -eを終了値取得の代わりにしない

set -e、つまりerrexitは、非0なら常に同じように終了する単純な規則ではありません。ifの条件、whileやuntilの判定、最後以外のパイプ要素、&&・||リストなどに例外があります。関数が呼ばれた文脈でも動作が変わるため、重要な判定はifと保存したステータスで明示します。

errexitを使う場合は補助的な保護として局所適用し、pipefailとの組み合わせ、コマンド置換、関数、条件リストを個別にテストします。想定内の非0まで停止させないよう、コマンド仕様を先に読みます。単にset -eを追加してエラー処理が完成したとは判断しません。

実行側から確認するテスト

bash ./report-job.sh
status=$?
printf 'script status=%s\n' "$status"
  1. すべて成功する入力で0になることを確認する。
  2. 最初、中間、最後の各処理を個別に失敗させ、期待値になるか確認する。
  3. 空入力や該当なしが、成功・業務上の未検出・実行エラーのどれかを確認する。
  4. シグナル終了、タイムアウト、コマンドなし、実行権限なしをテスト環境で区別する。
  5. 関数の末尾ログや後片付けが本来の値を上書きしないか確認する。
  6. バックグラウンド処理をPID指定のwaitで回収しているか確認する。

終了ステータスの取得は$?を表示するだけではなく、「何の直後か」を固定する作業です。対象直後に保存し、パイプラインではpipefailとPIPESTATUSの役割を分け、非同期処理ではwaitで最終値を受け取ります。コマンド固有の意味を公式資料で確認し、ログや後片付けを追加しても値が変わらないテストを用意すると、呼び出し元が信頼できるスクリプトになります。

公式情報・参考資料

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次