この記事の狙い
Bash スクリプトの失敗の定義を明確化し、終了コード(exit status)を一貫して扱えるようにします。
0/非0 の基本から、予約コード、関数・パイプライン・サブシェルでの伝播、set -e/pipefail/trap ERR の連携、CLI のコード設計指針までを、コピペで使える最小実装とともに示します。
前提と対象
- Bash 4+(
set -Eeuo pipefail推奨) - 終了コードは 0–255(8bit)。0=成功、非0=失敗(種類を表現)
TL;DR(最小実装・コピペ可)
#!/usr/bin/env bash
set -Eeuo pipefail
set -o pipefail # パイプ途中の失敗も検知
trap 'code=$?; echo "ERROR ($code) at ${BASH_SOURCE[0]}:${LINENO}" >&2' ERR
# 終了コードの規約(例)
E_USAGE=2 # 使い方エラー
E_IO=3 # I/O/外部依存
E_TMP=4 # 一時/ロック
E_DATA=5 # 入力データ不正
usage(){ echo "Usage: $(basename "$0") <file>" >&2; }
main(){
local f="${1-}" || return $E_USAGE
[[ -n "$f" ]] || { usage; return $E_USAGE; }
[[ -r "$f" ]] || { echo "cannot read: $f" >&2; return $E_IO; }
mapfile -t lines <"$f" || return $E_IO
(( ${#lines[@]} )) || { echo "empty input" >&2; return $E_DATA; }
printf '%s\n' "${#lines[@]}" # 正常系は stdout
}
main "$@" || exit $? # 戻り値を最終 exit に
設計の要点(原則)
- 0=成功 / 非0=失敗を徹底。正常系で非0を返さない
- 失敗の分類をコードにマッピング(定数で可読化)
- stdout=結果 / stderr=メッセージ(ログは stderr)
- 伝播の仕組みを固定:
|| return/exit、set -o pipefail、必要に応じてtrap ERR - 関数は return、スクリプトは exit。
exitを関数から直接呼ばない(ライブラリ可用性)
予約・慣例の終了コード
0成功1一般的な失敗(未分類)2使い方/引数エラー(多くの CLI 慣例)126実行権なし/実行不可127コマンド未発見128+nシグナルnにより終了(130=SIGINT、137=SIGKILL 等)255範囲外(exit 256は 0 に巻き戻らず 0–255 に丸め)
予約・慣例は上書きしない。自前の分類は 3–125 あたりで定義するのが無難。
失敗の伝播パターン
単独コマンド
cp -- "$src" "$dst" || { echo "copy failed" >&2; return $E_IO; }
- 「失敗したら即 return/exit」を明示。
set -e任せにしない
関数チェーン
step1 || return $?
step2 || return $?
- 直前の失敗コードをそのまま返すなら
return $? - 分類を付け替えるなら定数で返す
パイプライン
set -o pipefail
grep -E '^\d+$' "$f" | awk '{s+=$1} END{print s}' || return $E_DATA
pipefailがないと最後のコマンドのコードだけが残る- 個々のコードが必要なら
${PIPESTATUS[@]}を参照
cmd1 | cmd2 | cmd3
st=("${PIPESTATUS[@]}") # 例: 0 2 0
(( st[0]==0 && st[1]==0 && st[2]==0 )) || return $E_DATA
サブシェル / コマンド置換
sum="$(awk '{s+=$1} END{print s}' <"$f")" || return $E_DATA
( cd "$dir" && make ) || return $E_IO
$(...)内の失敗は外側の$?で検知( ... )は子プロセス。副作用(cd等)が親に影響しない
バックグラウンドと wait
taskA & p1=$!
taskB & p2=$!
ok=0
wait "$p1" || ok=1
wait "$p2" || ok=1
(( ok==0 )) || exit $E_IO
waitの終了コードで個別の失敗を拾う
trap ERR と -E
set -E # ERR を関数/置換/サブシェルにも伝播
trap 'echo "ERR ($?) at $BASH_SOURCE:$LINENO" >&2' ERR
- ログには便利。ただし制御は明示の
|| return/exitが基本 trap ERRは成功/失敗の分岐を置き換えるものではない
CLI の終了コード設計(実務例)
例:小さな CLI の分類
E_OK=0
E_USAGE=2
E_NOTFOUND=3 # 入力対象が無い
E_INVALID=4 # フォーマット不正
E_IO=5
- README/–help に明記(「Exit codes: 0/2/3/4/5 …」)
- 使い方エラーは常に
2(grep など多くの CLI に倣う) - 外部依存(ネットワーク/権限/ロック)は
E_IOに集約し、ログで詳細
サブコマンド別けとの相性
- サブコマンドごとに内部分類を持っても、最終的に共通分類へ畳み込む
- 機械連携(make/systemd/cron/CI)では非0だけで十分な場合が多い。詳細はログへ
ログと出力の分離(再掲)
- **結果(成功時の値)**は stdout。人間向け説明/エラーは stderr
- 上流から下流へ結果をパイプする設計で、stderr 混入は致命傷
result="$(do_thing)" || exit $E_IO
printf '%s\n' "$result" # ← 結果
“コピペ可”テストブロック(最小)
#!/usr/bin/env bash
set -Eeuo pipefail
set -o pipefail
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
E_USAGE=2; E_IO=3; E_DATA=5
sut='
set -Eeuo pipefail; set -o pipefail
E_USAGE=2; E_IO=3; E_DATA=5
main(){ local f="${1-}"||return 2; [[ -n "$f" ]]||return 2; [[ -r "$f" ]]||return 3; mapfile -t L <"$f"||return 3; (( ${#L[@]} ))||return 5; printf "%s\n" "${#L[@]}"; }
main "$@" || exit $?
'
# 1) 使い方エラー
bash -c "$sut" >/dev/null 2>&1; st=$?; [[ $st -eq $E_USAGE ]] || { echo "usage:$st"; exit 1; }
# 2) I/O エラー
bash -c "$sut" _no_such_ >/dev/null 2>&1; st=$?; [[ $st -eq $E_IO ]] || { echo "io:$st"; exit 1; }
# 3) データ不正
: >"$TMP/empty"
bash -c "$sut" "$TMP/empty" >/dev/null 2>&1; st=$?; [[ $st -eq $E_DATA ]] || { echo "data:$st"; exit 1; }
# 4) 正常(行数を返す)
printf 'a\nb\n' >"$TMP/in"
out="$(bash -c "$sut" "$TMP/in")"; st=$?
[[ $st -eq 0 && "$out" = "2" ]] || { echo "ok:$st/$out"; exit 1; }
echo "PASS"
失敗しやすい点(アンチパターン)
set -e依存:条件式・パイプ・|| trueなどで落ち方が不定
→ 重要箇所は 明示の|| return/exit+pipefail- 関数から
exit:ライブラリ再利用性を殺す
→returnで上位へ委ね、エントリポイントだけexit $? - stdout にエラー文:下流が壊れる
→ stderr へ(>&2) - 予約コードの再定義:
127などを自前分類に使う
→ 3–125 を割当
運用メモ(実務)
- CI/cron/systemd で非0検知→ログ参照の運用を前提にする
- 並列・分散ジョブは「部分失敗をどう扱うか」を決め、最終集約コードを設計(例: 1件でも失敗→非0)
trap '...' EXITで一時資源を必ず掃除しつつ、失敗時のみ詳細ログを出す
互換性と移植性
- Bash 以外(dash 等)は
PIPESTATUSやERR伝播が異なる - POSIX sh 互換を狙う場合は、パイプラインの各段で明示判定し、中間ファイルも検討
セキュリティと安全設計
- 失敗時のメッセージに機密情報を含めない
- 外部コマンドの終了コードをそのまま上流へ(隠蔽しない)
trapで異常終了時の後始末(ロック解除・一時ファイル削除)を徹底
