終了コードをあらためて考察する

背景・目的

GNU/Linux を使っていると、シェルスクリプトや各種コマンド、Python などのプログラムが「終了コード(exit status)」を返している場面に頻繁に出会う。終了コードは一見すると単純で、成功なら 0、失敗なら 0 以外という “雑な” 仕組みに見える。しかし実務では、cron・CI・デプロイ・監視・バッチ処理など「自動化された連鎖」の中で、終了コードは「次の処理に進むか」「止めるか」「通知するか」を決める最小単位の合図になる。ログが膨大で読めない時でも、終了コードだけは機械が確実に読める。そのため、終了コードの意味づけを曖昧なまま放置すると、運用の信頼性が落ちる。

本稿の目的は、終了コードについて「一般的な慣習」と「規約(仕様・標準・実装依存の予約領域)」を整理し、最終的にポリシーとして何を採用すべきかを一般論として明確化することである。ここでは、個別の経緯やローカルな事情は捨て、再利用できる形の論考としてまとめ直す。


終了コードとはそもそも何なのか

終了コードは、あるプログラム(プロセス)が終了したときに、親(通常はシェルや呼び出し元プロセス)へ返す「結果の数値」である。典型的には「成功か失敗か」を示すフラグとして使われる。POSIX の世界では「成功は 0、失敗は非 0」が基本であり、具体的にどの数値がどの意味かは、プログラム側が設計として決める余地が大きい。ただし、どこまでも自由というわけではない。シェルや OS、既存ツールが既に「予約的に扱う領域」を持っており、そこに勝手な意味を割り当てると衝突が起きる。

また、プロセス終了には大きく二種類ある。プログラムが自分で exit(n) する「通常終了」と、シグナルなどで落ちる「異常終了」である。親は wait/waitpid などで子の終了状態を受け取り、終了コードを解釈する。Linux の man ページでも、子がシグナルで終わったか、通常終了したかを判別するマクロ(WIFSIGNALED / WIFEXITED など)が説明されている。つまり「終了コード」と呼んでいるものも、内部的には “終了のしかた” とセットで扱われる。[1]


何をねらいとしているのか

終了コードが狙っている本質は「人間向けの説明」ではなく「機械向けの分岐条件」である。具体的には次の用途が中心になる。

  1. 制御フローの分岐:成功なら次へ、失敗なら中断・リトライ・通知など。
  2. パイプラインの健全性判定:複数の処理を連結しているとき、どこで落ちたかを機械的に検出する。
  3. 監視・運用のシグナル:ジョブ管理や CI は “exit code” を第一級の結果として扱う。
  4. 最小情報での失敗通知:ログが読めない状況でも「失敗した事実」だけは確実に伝える。

重要なのは、終了コードは「情報量が少ない」ことが前提の仕組みだという点である。終了コードだけで原因を完全に表現することは難しい。だからこそ「ログとセットで設計する」ことが一般解であり、終了コードの細分化は必要最小限に留めるべき、という方向に自然に収束する。


一般的な慣習としてはどうなのか

0 は成功、非 0 は失敗

これは最も広く共有される前提である。POSIX シェルの仕様でも、コマンドが失敗した場合は非 0 の終了ステータスを返す、という基本が示される。[2]

1 は “General failure” として多用される

実務で最も多いのは「失敗したら 1」である。Python の公式ドキュメントですら「Unix プログラムは一般に、コマンドラインの文法エラーを 2、その他のエラーを 1 にすることが多い」と述べており、さらに sys.exit(“message”) のように文字列を渡すと stderr に表示して終了コードが 1 になる、という “一般的失敗の短絡” が言語仕様として用意されている。[3]

一方で「1 は便利だが、情報量が少ない」ことも古くから指摘されている。つまり 1 を多用する現実はあるが、1 だけでデバッグできる設計にはならない。ここが「ログ設計」「エラー出力設計」と不可分になる。

126 / 127 は衝突しやすい予約領域

Bash の公式マニュアルには、コマンドが見つからない場合は 127、見つかったが実行できない場合は 126、という挙動が明記されている。[4] これらをアプリ固有の意味として再定義すると、「コマンドが無いのか」「自分のプログラムが 127 を返したのか」が区別できなくなる。ゆえに 126 / 127 は “予約扱い” として温存するのが実務的に安全である、という結論になりやすい。

128 以上はシグナル由来として扱われやすい

広く参照される解説では「128 + n はシグナル n による終了」といった慣習が紹介されており、Ctrl + C の SIGINT(2) なら 130 という解釈が一般化している。[5] 実際、ジョブ管理などでも 130 = SIGINT(130 – 128 = 2)という形で説明されている。[6] したがって 128 以上の領域も、アプリ独自コードとして自由に使うと “シグナル死” と混同されやすい。

小項目まとめ

観点 要点 実務上の扱い
0 と非 0 成功は 0、失敗は非 0 が基本 最小ルールとして必ず尊重する
1 の多用 一般的失敗として 1 が最も多い 詳細はログに寄せ、1 をデフォルトにする
126 / 127 シェル側の予約的意味が強い アプリ独自用途で再定義しない
128 以上 シグナル由来として解釈されやすい アプリ独自の終了コードに使わない

歴史的にはどうなのか

終了コードの文化は、UNIX の「小さなプログラムを組み合わせる」という思想と強く結びついている。UNIX は、標準入力・標準出力・標準エラーと合わせて「終了ステータス」を提供し、これを組み合わせの接着剤にした。つまり “成功なら次へ” を機械が判断できるようにして、複雑な処理をパイプやスクリプトで組めるようにした。その結果、終了コードは「人間が読む診断書」ではなく「機械が読む信号」になった。

この歴史的背景は、終了コードを過剰に細分化して「終了コードだけで説明しよう」とする方向が本質から外れることを示唆する。説明はメッセージで行い、終了コードは分岐のための最小限に留める、という設計は UNIX 的に自然である。


世の中にはどのような規約があるのか

ここでは「仕様としての規約」と「事実上の規約(慣習)」を分けて整理する。

1) POSIX / シェル仕様(枠組み)

POSIX のシェル仕様は、終了ステータスの枠組み(成功/失敗、実装が許容する範囲など)を定める。シェル実装がシグナル等で終了した場合に 255 を超える値を割り当てることを許容する、という注意も含まれる。[2]

2) Bash の仕様(126 / 127)

Bash は「見つからない = 127」「実行不可 = 126」を明確に規定する。これは運用上の衝突を避けるため、アプリ側で再定義しないのが安全、という強い根拠になる。[4]

3) シグナル終了(128 + n)という慣習

POSIX レベルでは「シグナルで終わったかどうか」を wait 系の仕組みで判別するが、シェルの世界では “人間が見やすい数値” として 128 + n が広く扱われる。一般向けの解説や実務文脈でも、この規則は共通語彙になっている。[5]

4) sysexits(64–78)という体系

sysexits は、もともと BSD 系で使われてきた「プログラムの終了コードに意味を持たせるための分類」を提供する。たとえば EX_USAGE(64) は使い方の誤り、EX_SOFTWARE(70) はソフトウェア内部エラーなど、意味が整理されている。Linux の man-pages でも sysexits.h として参照できる。[7] OpenBSD の man ページにも同様の説明がある。[8] 日本語の解説も存在し、EX_CONFIG(78) など “設定不備” を表すコードが定義されている。[9]

5) 「予約コードを避けよ」という啓蒙(解説・コミュニティ)

TLDP の Advanced Bash-Scripting Guide は、特定の範囲に特別な意味があるとして注意を促している。[10] また、Unix 系の Q&A でも “reserved codes” をアプリ固有に使うことの混乱が議論されている。[11]

この種の啓蒙は「規格」ではないが、運用上の事故を減らす実践知として価値がある。

小項目まとめ

枠組み 性質 読み替えポイント
POSIX 仕様(枠組み) 成功/失敗の基本と解釈の前提を与える
Bash 126/127 実装仕様(事実上の標準) 衝突を避けるため予約として温存する
128 + n 慣習 シグナル終了と誤認されるため独自利用しない
sysexits 64–78 体系(意味付き分類) 必要時のみ採用し、ヘッダーで明示する
啓蒙記事・Q&A 実践知 事故パターンを共有しており運用設計に役立つ

それらをどう取り扱うべきなのか

ここまでの整理を踏まえると、現実的な取り扱いは次の二段構えになる。

第 1 段:最小集合を守る(衝突回避)

  • 0 は成功。
  • 1 は一般的失敗(General failure)として使う。
  • 126 / 127 はシェル予約として温存する(アプリ独自で再定義しない)。[4]
  • 128 以上はシグナル慣習と衝突しやすいので、アプリ独自用途では避ける。[5]

この段階だけでも、他ツールとの衝突が減り、運用時の誤解釈が大幅に減る。

第 2 段:本当に必要な場合だけ分類を導入する

終了コードの細分化は「便利そうに見える」が、運用とレビューを複雑化させやすい。そこで、分類が必要な場合だけ sysexits(64–78) を採用する。採用するなら、そのスクリプトのヘッダーで明示し、ログでも説明する。sysexits は意味体系が既に共有されているため、独自体系を自作するより衝突と誤解が少ない。[7]

つまり「基本は最小集合」「必要時だけ sysexits」という分離が、一般性と一貫性の両立に向く。

小項目まとめ

段階 方針 狙い
第 1 段 0 / 1 を基本に、126 / 127 / 128+ を避ける 外部の慣習・仕様と衝突しない
第 2 段 必要なときだけ sysexits(64–78) を採用する 意味のある分類を最小の追加コストで実現する

最終的にポリシーを定めるにあたり、何を気にしたのか

ポリシー策定で重視した観点は次の通りである。

  1. 一般性:GNU/Linux の広い慣習と衝突しないこと(126/127/128+ の予約領域)。
  2. 一貫性:Shell/Python/Ruby で意味がズレないこと。言語ごとに exit code 定義を重複させると、更新漏れや矛盾が発生しやすい。
  3. 運用性:終了コードに情報を詰め込みすぎず、ログ設計を主役に据えること。終了コードの “体系” がポリシー全体を支配すると、実装規律(明示的エラーハンドリング、ログ、I/O 安全性、テスト、ドキュメント)がおろそかになる。
  4. 保守性:ポリシー変更を最小箇所で反映できる構造にすること(exit code を Shell セクションに集約し、他言語は参照)。
  5. 読解可能性:一般の人が読んでも理解できるよう、抽象語だけでなく「何を避けるか」「なぜ避けるか」を書くこと。

どのようなポリシーを定めたのか

最終的に、以前のポリシーを見直し、リポジトリdoc/POLICY は「Implementation Policies」として、Shell/Python/Ruby を同一ドキュメントに統合した。設計思想・ログ形式・終了コードの意味づけを言語横断で揃え、実装規律(POSIX 準拠、明示的エラーハンドリング、ドキュメント更新、テスト構造)を規定する。

ここで、終了コードについては次のように定めた。

  • 0: Success
  • 1: General failure(基本はここに寄せる)
  • 126: exists but not executable(予約、再定義しない)[4]
  • 127: not found(予約、再定義しない)[4]
  • 128+ はシグナル慣習があるため、アプリ独自用途に使わない。[5]
  • 細分類が必要な場合のみ sysexits(64–78) を採用し、ヘッダーで明示する。[7]

結論としてどのようなポリシーができあがったのか

結論は「終了コードは最小集合で衝突回避を優先し、説明はログで行う。必要時のみ sysexits を限定採用する」という形に落ち着いた。これにより、終了コード設計が過剰に肥大化せず、一般的慣習と整合しつつ、リポジトリ内で矛盾が生まれにくい。


ポリシーには終了コード以外も含め、何が書かれているのか

終了コードはポリシー全体の一部に過ぎない。実際の doc/POLICY では、より重要な実装規律が中心になっている。要点は次の通りである。

Shell Script(実装の基準点)

  • POSIX 準拠(#!/bin/sh、bash 依存を避ける、local 不使用)。
  • 関数化と責務分離(短く単一目的)。
  • ヘッダー・ドキュメントの必須構造(Description / Requirements / Usage / Options / Test Cases / Version History の順序)。
  • ログ形式統一([INFO] / [WARN] / [ERROR]、エラーは stderr)。
  • エラーハンドリング(set -e を使わず明示的チェック、早期検証、exit は異常系のみ、通常は return で伝播)。
  • I/O 安全性(破壊的操作の回避、パス検証、既存の一時ファイル規約尊重)。
  • CLI 規律(-h は常に 0 で終了、引数不正は usage+失敗)。
  • テストと運用(cron 前提、PATH 等の明示、プラグイン構造の戻り値伝播)。

Python / Ruby(Shell と同型の思想で実装する)

  • 設計思想・ログ・終了コードは Shell と同一(exit code の再定義はしない)。
  • それぞれの言語で、エントリポイント(main)・標準ライブラリ縛り・テスト配置・互換性制約を明確化し、Shell と同じ規律(明示的エラー処理、早期検証、ログ整形)に従わせる。

参考文献

  1. Linux man-pages: wait(2)(終了状態の判定マクロ WIFSIGNALED / WIFEXITED など) https://man7.org/linux/man-pages/man2/wait.2.html
  2. The Open Group Base Specifications Issue 8: Shell Command Language(終了ステータスに関する仕様を含む) https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html
  3. Python 公式ドキュメント sys.exit(文字列を渡すと stderr 出力+終了コード 1、Unix の慣習にも言及) https://docs.python.org/3/library/sys.html
  4. Bash Reference Manual: Exit Status(126/127 の意味) https://www.gnu.org/s/bash/manual/html_node/Exit-Status.html
  5. Qiita: Bash の予約された exit code と 128+n(シグナル由来の扱い) https://qiita.com/Linda_pp/items/1104d2d9a263b60e104b
  6. IBM Docs: LSF job exit codes(130=SIGINT などの説明) https://www.ibm.com/docs/ja/spectrum-lsf/10.1.0?topic=logging-lsf-job-exit-codes
  7. Linux man-pages: sysexits.h(3head)(64–78 の体系) https://man7.org/linux/man-pages/man3/sysexits.h.3head.html
  8. OpenBSD man pages: sysexits(3)(sysexits の体系) https://man.openbsd.org/sysexits.3
  9. manpages 日本語ミラー: sysexits(3) 解説(EX_CONFIG 等の説明) https://nxmnpg.lemoda.net/ja/3/sysexits
  10. TLDP Advanced Bash-Scripting Guide: Exit Codes With Special Meanings(予約領域への注意喚起) https://tldp.org/LDP/abs/html/exitcodes.html
  11. Unix & Linux Stack Exchange: reserved codes 議論(予約領域を独自用途で使うことの混乱) https://unix.stackexchange.com/questions/242111/using-reserved-codes-for-exit-status-of-shell-scripts