シェルスクリプトの終了コード設計|exit codeを体系化して呼び出し元がエラー原因を判別できる仕組み

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)シェルスクリプト > シェルスクリプトの終了コード設計|exit codeを体系化して呼び出し元がエラー原因を判別できる仕組み
「スクリプトが失敗したのに、cronのアラートが上がらなかった」「パイプラインのステップが途中で止まっているのに、最終ステータスが 0 になっている」。こういったトラブルの根本を掘ると、exit code の設計が甘いスクリプトが原因であることが多い。

exit code(終了コード)は、シェルスクリプトが呼び出し元に伝える唯一のシグナルだ。cronはこの値を見てメールを飛ばすか判断し、CI/CDパイプラインはここで次のステップに進むかを決める。ここを正しく設計しておかないと、エラーを握り潰したまま処理が続いてしまう。

この記事では、シェルスクリプトのexit codeを体系的に設計する方法を解説する。Linuxの慣例コード、関数内での正しい伝播パターン、cronやCI/CDとの連携設計まで、RHEL 9 / Rocky Linux 9 / Ubuntu 24.04 LTSで動作確認済みのコード例を交えて紹介する。

この記事のポイント

・exit 0が成功、1が汎用エラー、2が引数誤りという慣例を守ると呼び出し元で正確に分岐できる
・126・127・128+nはシェル予約コードなので、スクリプト独自の番号と重複させてはいけない
・関数内ではreturnで返し、呼び出し側が$?で受け取る設計が安全な伝播パターン
・local宣言と代入を同じ行に書くとexit codeが消える落とし穴があり、必ず分離する


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

exit codeとは何か、なぜ重要なのか

シェルスクリプトが終了するとき、必ず0~255の整数値を返す。これが exit code だ。0は成功、1以上は何らかの失敗を意味する。

実行後すぐに $? を参照すると、直前のコマンドまたはスクリプトの終了コードを取得できる。

# スクリプトを実行してexit codeを確認する ./backup.sh echo "終了コード: $?" # 出力例(成功時) # 終了コード: 0 # 出力例(失敗時) # 終了コード: 1

cronはデフォルトで exit code が 0 以外のジョブに対してMAILTOにメールを送る。CI/CDパイプラインも同様に、0以外でジョブを失敗扱いにする。exit codeを正確に返すことは、自動化の信頼性を支える基盤だ。

Linuxが予約しているexit codeと慣例

スクリプト独自のexit codeを決める前に、シェルが予約している番号を把握しておく必要がある。これらと重複すると、呼び出し元が誤判断する。

1. シェルが予約するexit code

exit code 意味 発生例
0 成功 正常終了
1 汎用エラー 処理失敗全般
2 使い方の誤り 引数が足りない・不正なオプション
126 コマンドを実行できない 実行権限がない(chmod +x 未設定など)
127 コマンドが見つからない PATHに存在しないコマンドを呼び出した
128+n シグナルnで強制終了 128+2=130(Ctrl+C)、128+9=137(SIGKILL)

2. カスタムexit codeの設計方針

スクリプト独自のexit codeは 3~125 の範囲で定義するのが慣例だ。番号の意味はスクリプト冒頭に定数として定義しておくと、コードを読んだ人が迷わない。呼び出し元が if 文で分岐する際も番号を探す手間がなくなる。

#!/bin/bash # ======================================== # exit code 定義(3~125 の範囲を使う) # ======================================== readonly E_SUCCESS=0 # 正常終了 readonly E_GENERAL=1 # 汎用エラー readonly E_USAGE=2 # 引数・使い方の誤り readonly E_FILE_NOT_FOUND=3 # 必要なファイルが存在しない readonly E_PERMISSION=4 # 権限エラー readonly E_CONNECTION=5 # 接続エラー(DB・API・外部サービス) readonly E_TIMEOUT=6 # タイムアウト # 引数チェック(足りなければ usage エラーで終了) if [[ $# -lt 2 ]]; then echo "Usage: $0 " >&2 exit ${E_USAGE} fi

exit codeを定数化すると、後から番号を変更したときの影響範囲が一か所に集まる。スクリプト間で番号の意味が食い違う事故も防げる。

関数内でexit codeを正しく伝播させる設計

スクリプトが大規模になると、複数の関数を組み合わせる構成になる。このとき exit code を正しく伝播させないと、呼び出し元が失敗を検知できなくなる。

1. returnとexitの使い分け

関数内では exit ではなく return を使う。exit を関数の中で呼ぶと、スクリプト全体が即終了してしまい、後片付け処理(trapのEXITハンドラ等)が意図通りに動かなくなる場合がある。

#!/bin/bash readonly E_SUCCESS=0 readonly E_CONNECTION=5 # DB接続チェック関数 check_db_connection() { local host="$1" if ! ping -c 1 -W 2 "${host}" &>/dev/null; then echo "[ERROR] DBホスト ${host} に到達できません" >&2 return ${E_CONNECTION} # exit ではなく return で返す fi echo "[INFO] DB接続確認OK: ${host}" return ${E_SUCCESS} } # 呼び出し元で $? を受け取る check_db_connection "db.example.com" if [[ $? -ne 0 ]]; then echo "[FATAL] DB接続失敗。処理を中断します。" >&2 exit ${E_CONNECTION} fi echo "以降の処理を継続..."

2. サブシェルでexit codeが消えるlocal宣言の罠

コマンド置換($(...))を local 宣言と同じ行に書くと、exit code が正しく取得できなくなる。local コマンド自体が成功するため、$? は常に 0 になってしまうからだ。

#!/bin/bash # NG: local 宣言と代入を同時に書くと $? が local の終了コード(0)になる bad_example() { local result=$(false) # false は exit 1 を返すが... echo "終了コード: $?" # 常に 0 が出力される(local が成功しているため) } # OK: 宣言と代入を分離する good_example() { local result result=$(false) # false は exit 1 を返す local code=$? echo "終了コード: ${code}" # 正しく 1 が出力される return ${code} } bad_example good_example

現場でよく見かける罠だ。宣言と代入を必ず別の行に書く習慣を徹底するだけで、見えないエラーが大幅に減る。【要注意】 set -e を使っているスクリプトでも、この問題は set -e では検知できないため、必ず宣言と代入の分離を意識すること。

exit codeの設計を含むシェルスクリプトの設計力を体系的に身につけたい方は、シェルスクリプト実践講座(Linux Master Pro)もご覧ください。現場で使える設計パターンを体系的に学べます。

呼び出し元でexit codeを活用する設計

スクリプトが返した exit code を、上位のシステムが正しく受け取れるよう設計することも重要だ。

1. cronでexit codeを活用するパターン

cronはデフォルトで exit code が 0 以外のジョブが発生したとき、MAILTO に設定したアドレスへメールを送る。これを活かして、エラー時だけ通知が届くようにできる。

# /etc/cron.d/daily-backup MAILTO=admin@example.com PATH=/usr/local/bin:/usr/bin:/bin # スクリプトが exit 0 以外を返した場合にメールが届く 0 3 * * * root /opt/scripts/daily-backup.sh >> /var/log/daily-backup.log 2>&1

スクリプト側が正しいexit codeを返さなければ、cronはエラーをメールしない。ログを見ても exit code が 0 のままでは、cronの通知機能を活かせない。

2. 上位スクリプトから呼び出すパターン

複数のスクリプトをオーケストレーションする親スクリプトでは、各子スクリプトの exit code を確認して処理を制御する。|| exit $? のパターンを使うと、子スクリプトの exit code をそのまま親スクリプトの exit code として返せる。

#!/bin/bash # 親スクリプト: 複数の処理を順番に実行する run_step() { local step_name="$1" local script="$2" echo "[INFO] ${step_name} 開始..." "${script}" local code=$? if [[ ${code} -ne 0 ]]; then echo "[ERROR] ${step_name} が失敗しました(exit code: ${code})" >&2 return ${code} fi echo "[INFO] ${step_name} 完了" return 0 } # || exit $? でexit codeをそのまま親に返す run_step "DBバックアップ" /opt/scripts/db-backup.sh || exit $? run_step "ファイル同期" /opt/scripts/file-sync.sh || exit $? run_step "レポート生成" /opt/scripts/report.sh || exit $? echo "[INFO] 全ステップ正常完了" exit 0

この設計により、cronや上位スクリプトは「どのステップが exit code いくつで失敗したか」を正確に把握できる。ログに exit code が残るので、障害対応が格段に速くなる。

3. CI/CDパイプラインとの連携

GitHub ActionsやJenkinsなどのCI/CDでは、スクリプトの exit code がジョブの成否を決める。意味のある値を返すことで、どのステップで何が失敗したかをログから即座に特定できる。

# GitHub Actions の step 例 steps: - name: デプロイスクリプト実行 run: ./deploy.sh # exit code 3(ファイル未存在)が返ると steps.*.outcome が failure になる - name: デプロイ失敗時のロールバック if: failure() # 前ステップが失敗したときだけ実行 run: ./rollback.sh

よくある落とし穴とエラー対処法

1. set -e 環境でのexit codeの扱い

set -e(エラー時即終了)を使っているスクリプトでは、コマンドが 0 以外を返した瞬間にスクリプト全体が終了する。ただし、if 文や || の中では set -e が無効になることを知らないと混乱する。

#!/bin/bash set -e # if 文の中では set -e が効かない(安全) if ! some_command; then echo "失敗しました" >&2 exit 1 fi # || の右辺も set -e の対象外(安全) some_command || { echo "失敗しました" >&2; exit 1; } # 関数をそのまま呼ぶ場合は set -e が有効(失敗で即終了) some_command # 失敗すると次の行に進まずスクリプトが終了する

2. $?は直後に変数へ退避する

$? は最後に実行したコマンドの終了コードしか保持しない。別のコマンドを実行した瞬間に上書きされるため、必要ならすぐ変数に退避すること。

some_command exit_code=$? # 直後に変数へ退避する echo "処理を記録" # この時点で $? は 0(echo の終了コード)に上書きされる if [[ ${exit_code} -ne 0 ]]; then exit ${exit_code} fi

まとめ

設計項目 推奨パターン
exit code 定義 スクリプト冒頭で readonly E_XX=数値 として定数化(3~125の範囲)
関数内の返し方 exit ではなく return で返す
local 宣言と代入 local result を先に書き、次の行で result=$(コマンド) と分離する
$? の取得タイミング 対象コマンドの直後に exit_code=$? で変数に退避する
上位スクリプトへの伝播 cmd || exit $? パターンで exit code をそのまま返す
cron との連携 MAILTO を設定し、0 以外の exit code でメール通知を受け取る
exit code の設計は地味に見えるが、自動化スクリプトが障害を起こしたときの「最初の手がかり」になる。番号を体系化して定数定義しておくだけで、問題の切り分けに要する時間がまったく変わってくる。スクリプトが返す値に責任を持つ設計を習慣にしてほしい。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
シェルスクリプト講座を見る >>

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。