退出码 0 是一种承诺
一句话总结
运维工具用退出码表达结果,对人说的话走标准错误,供机器读取的结果走标准输出。一旦把这三条通道混在一起,脚本在 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) 返回整数的形态,让它成为即使被导入也不会执行的工具。