TT Lab
开始
学习 学习路径 课程

失败了,退出码却是 0

退出码 0 是一种承诺

在 TT Lab 中继续学习

一句话总结

运维工具用退出码表达结果,对人说的话走标准错误,供机器读取的结果走标准输出。一旦把这三条通道混在一起,脚本在 cron 和流水线中就会悄悄地说谎。

为什么需要它

凌晨三点,检查备份目录的脚本运行了。备份并没有到达,下一个步骤却照常进行了。因为脚本只打印了一句 print("backup missing!") 就结束了。Python 会给没有抛出异常而结束的程序一个退出码 0。cron 和 CI 看的都只是这个 0。屏幕上打印的句子,没有任何人读过。

本课程之所以讲 Python,是因为招聘市场对它的要求最多。2026-09-11 对 13 个 Greenhouse 招聘看板和 We Work Remotely 做的统计结果(工程类职位 959 条)显示,Python 出现了 445 条(46.4%),在所有技术关键词中排名第 1;单独统计 6 家韩国公司,也是 131 条(46.1%),排名第 1。然而职位需求想要的并不是语法。“编写运维自动化脚本”“开发内部工具”这类句子背后,是构建别人可以放心运行的程序所需要的纪律。这种纪律的第一条,就是退出码。

工作原理

sys.exit() 的文档是这样记述惯例的:整数 0 表示正常退出,非 0 的值表示异常退出,而 Unix 程序对命令行语法错误使用 2,对其他错误使用 1。如果传入的不是整数的对象(例如字符串),那个对象会被打印到标准错误,退出码为 1。所以本课程的工具遵守三条约定。

退出码 含义 由谁处理
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())

最后两行就是本课程所强调的形态。main(argv) 返回整数,sys.exit() 只在文件的最底部调用一次。这样在下一个模块中才能 import 这个工具来测试——如果在加载模块的那一刻程序就被执行,就无法测试。

异常会怎样?没有捕获的异常会把 traceback 打印到标准错误,并以退出码 1 结束。光看退出码,它与“检查失败”无法区分。所以对工具错误(路径不存在、没有权限),要捕获 OSError,把它变成一行信息加退出码 2。traceback 对创建工具的人来说是信息,但对凌晨收到告警的人来说是噪音。

在现场相遇的样子

最常见的事故是“看似成功地结束的失败”。在流水线中间放着 python3 check.py || true 的情形,检查工具没有返回退出码的情形,用 try: ... except Exception: print(e) 吞掉所有异常并以 0 结束的情形,全都会产生同样的结果。第二常见的是标准输出被污染。工具输出的是 JSON,中间却混进了“connecting...”之类的进度信息,导致 json.loads 出错。第三种是这样的工具:没有只在加上 -v 时才出现的信息,所以出故障时必须“重新运行并加上 print”。日志级别如果不在最初创建时就加进去,就永远不会有。

下一项实验要做什么

从头构建备份目录检查工具 dircheck.py。从支持 --help 的骨架出发,加入 0、1、2 的退出码约定,在工具错误时输出一行信息而不是 traceback,用 -v 只把 DEBUG 日志输出到标准错误,用 --json 把供机器读取的结果输出到标准输出。最后整理成 main(argv) 返回整数的形态,让它成为即使被导入也不会执行的工具。