TT Lab
はじめる
学ぶ 学習パス コース

失敗したのに終了コードは 0 だった

終了コード 0 は約束である

TT Labで続きを見る

一言でいうと

運用ツールは、結果を終了コードで伝え、人に向けた言葉は標準エラー出力へ、機械が読む結果は標準出力へ出します。この3つの経路を混ぜた瞬間、スクリプトはcronやパイプラインの中で、静かに嘘をつきます。

なぜ必要なのか

午前3時、バックアップディレクトリを検査するスクリプトが動きました。バックアップは届いていなかったのに、次のステップはそのまま進みました。スクリプトがprint("backup missing!")を出力して、そのまま終わったからです。Pythonは、例外なく終わったプログラムに、終了コード0を返します。cronもCIも、その0だけを見ます。画面に出力された文を、誰も読みませんでした。

このコースがPythonを扱う理由は、採用市場が最も多く求めているからです。2026-09-11に、Greenhouseの13個のボードとWe Work Remotelyを集計した結果(エンジニアリング職種959件)、Pythonは445件(46.4%)で、すべての技術キーワードのうち1位でした。韓国の6社だけを別に数えても、131件(46.1%)で1位でした。ところが、求人が求めているのは文法ではありません。「運用自動化スクリプトの作成」「社内ツールの開発」のような文の裏にあるのは、他の人が信頼して実行できるプログラムを作る規律です。その規律の1つ目が、終了コードです。

どう動くのか

sys.exit()のドキュメントは、慣例を次のように書いています。整数0は正常終了、0以外の値は異常終了で、Unixのプログラムは、コマンドラインの文法エラーに2、それ以外のエラーに1を使います。整数以外のオブジェクト(例: 文字列)を渡すと、そのオブジェクトが標準エラー出力に出力され、終了コードは1になります。そのため、このコースのツールは、3つの約束を守ります。

終了コード 意味 誰が処理するか
0 検査に合格 次のステップが進みます
1 検査に失敗(ツールは正常で、対象が問題) アラートとリトライのポリシーが判断します
2 ツールのエラー(不正な引数、存在しないパス) 人がツールを直す必要があります

この表が、grepの約束(一致は0、不一致は1、エラーは2)と同じ形をしているのは、偶然ではありません。argparseも、同じ慣例に従います。ArgumentParser.error()は、使用法のメッセージを標準エラー出力に出力して、終了コード2で終了します。引数を手でパースすると、この約束を毎回実装し直すことになり、たいてい抜け落ちます。

ログは、logging HOWTOが定めたとおりに動作します。何も設定がなければ、既定のレベルはWARNINGで、出力先は標準エラー出力(sys.stderr)で、既定の形式は심각도:로거 이름:메시지(プレースホルダーは、重大度、ロガー名、メッセージです)です。basicConfig(level=..., format=...)で、レベルと形式を変えます。ここで重要なのは、出力先が標準エラー出力だという事実です。print()は標準出力へ出力されます。そのため、診断のメッセージをprint()で出力すると、tool | jqのようなパイプラインが壊れ、結果をloggingで出力すると、結果がパイプに届きません。

import argparse, logging, sys

log = logging.getLogger("dircheck")

def main(argv=None) -> int:
    p = argparse.ArgumentParser(prog="dircheck")
    p.add_argument("path")
    p.add_argument("-v", "--verbose", action="store_true")
    args = p.parse_args(argv)                 # 잘못된 인자면 여기서 2 로 끝난다
    logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO,
                        format="%(levelname)s %(name)s: %(message)s")
    log.debug("checking %s", args.path)       # 표준 오류
    print("files=3 ok=true")                  # 표준 출력 — 기계가 읽는다
    return 0                                  # 종료 코드는 main 이 돌려준다

if __name__ == "__main__":
    sys.exit(main())

最後の2行が、このコースが強調する形です。main(argv)が整数を返し、sys.exit()は、ファイルの一番下で1回だけ呼びます。そうすれば、次のモジュールで、このツールをimportしてテストできます。モジュールを読み込んだ瞬間にプログラムが実行されてしまうと、テストはできません。

例外はどうなるのでしょうか。捕まえなかった例外は、トレースバックを標準エラー出力に出力して、終了コード1で終わります。終了コードだけを見ると、「検査の失敗」と区別がつきません。そのため、ツールのエラー(存在しないパス、権限なし)は、OSErrorを捕まえて、メッセージ1行と終了コード2に変換します。トレースバックは、ツールを作った人には情報ですが、午前3時にアラートを受け取った人にとっては、ノイズです。

現場での姿

最もよくある事故は、「成功のように終わる失敗」です。パイプラインの途中にpython3 check.py || trueを入れてある場合、検査ツールが終了コードを返さない場合、try: ... except Exception: print(e)ですべての例外を握りつぶして0で終わる場合が、すべて同じ結果を出します。2番目によくあるのは、標準出力の汚染です。ツールがJSONを出力するのに、途中に「connecting...」のような進捗メッセージが標準出力に混ざって、json.loadsが壊れます。3つ目は、-vを付けたときだけ出る情報がないために、障害のとき「もう一度実行しながらprintを追加」しなければならないツールです。ログのレベルは、最初に作るときに入れておかないと、永遠にありません。

次のラボですること

バックアップディレクトリの検査ツールdircheck.pyを、最初から作ります。--helpが動く骨格から始めて、0・1・2の終了コードの約束を入れ、ツールのエラーではトレースバックの代わりに1行のメッセージを出力し、-vでDEBUGログを標準エラー出力にだけ流し、--jsonで機械が読む結果を標準出力に出力します。最後に、main(argv)が整数を返す形に整えて、インポートしても実行されないツールにします。