写 CLI 时强制 --help 与干净退出码(成功 0/失败非 0);stdout/stderr 分开,上层用 $? 接住。

要点
- 交付清单四件套:
--help/-h列出用法与退出码约定;成功路径sys.exit(0)/exit 0;可预期失败(缺参、校验失败、业务否决)用非 0;人读日志走 stderr,机器要解析的主结果走 stdout。证据强度:高(CLI 惯例;可跟做)。 - 禁止“失败了还 exit 0”:上层 CI/Makefile/父脚本只看进程码时,会把翻车当成绿。证据强度:高(运维常识)。
- 动机旁及:
codex exec在 CI 里进程 exit 0 往往只表示“这一回合跑完了”,不等于内部命令或项目自测已通过;社区 issue/笔记多次复现“内层失败、外层仍 0”(GitHub #15536、Kogen 笔记)。证据强度:中高(公开 issue/探针;官方 non-interactive 页未给完整 exit 表;是否已在你用的 CLI 版本修好,以发行说明与本机复现为准)。 - 官方 non-interactive:
codex exec进度走 stderr、最终 agent 消息走 stdout,方便管道;自动化仍要你自己的测试/schema/门禁收口(Non-interactive mode)。证据强度:高(官方)。 - 入门做法:把四件套写进给 Codex 的任务提示+验收命令;用最小 Python
argparse或 bash 示例当对照。证据强度:高(可跟做)。
正文
场景
你让 Codex 写了个 tools/sync_fixtures.py。本地手跑“像是好了”。丢进 CI:python tools/sync_fixtures.py 失败时仍返回 0,下一步 deploy 照样启动;或者帮助信息只在报错堆栈里,新人不知道有哪些子命令。
Agent 优化的是“把活干完”。CI 要的是“进程码说话”。这两件事不会自动对齐,得你在提示里写死。
可跟做步骤/可抄工作流
证据强度:四件套=惯例高;codex exec 出口语义=官方管道约定高+社区失败传播问题中高;示例代码=演示,非某仓库复刻。
1. 任务提示里写死交付物
可抄进 Codex/Agent 首条消息:
写/改 scripts/xxx 这个 CLI,必须同时交付:
1) --help 与 -h 能打印用法;写出子命令(如有)和退出码约定
2) 成功:进程退出码 0
3) 可预期失败(缺参、文件不存在、校验失败):非 0;不要 catch 后静默成功
4) 给人看的进度/警告走 stderr;给管道解析的主结果走 stdout
5) 自测:跑 --help;再各跑一条成功与一条失败路径,贴 exit code
先给计划,等我说 go 再改文件。
2. 最小 Python 示例(标明示例)
下面是演示示例,勿当某仓库原样拷贝。重点看 argparse 怎么带退出码,以及 stderr 怎么用。
#!/usr/bin/env python3
"""示例:带 --help 与退出码约定的最小 CLI。"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="sync_fixtures",
description="Sync local fixtures. Exit 0 on success; 2 on usage/validation errors.",
)
p.add_argument("source", type=Path, help="Source fixtures directory")
p.add_argument("dest", type=Path, help="Destination directory")
p.add_argument("--dry-run", action="store_true", help="Print plan only")
return p
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
if not args.source.is_dir():
print(f"error: source not a directory: {args.source}", file=sys.stderr)
return 2
if args.dry_run:
print(f"would sync {args.source} -> {args.dest}") # stdout:可被管道接住
return 0
args.dest.mkdir(parents=True, exist_ok=True)
# ... 真实拷贝逻辑 ...
print(f"synced {args.source} -> {args.dest}")
return 0
if __name__ == "__main__":
sys.exit(main())
argparse 自带 -h/--help。约定写进 description,失败时 return 2 再 sys.exit。
3. 最小 bash 示例(标明示例)
#!/usr/bin/env bash
# 示例:--help + 退出码
set -euo pipefail
usage() {
cat <<'USAGE'
usage: sync_fixtures.sh SRC DEST
--help show this help
Exit codes: 0 success; 2 usage/validation error
USAGE
}
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
usage
exit 0
fi
if [[ $# -lt 2 ]]; then
usage >&2
exit 2
fi
echo "sync $1 -> $2"
exit 0
4. 验收清单(交给 Agent 跑完贴结果)
python scripts/sync_fixtures.py --help
python scripts/sync_fixtures.py /no/such /tmp/out; echo "exit=$?"
# 期望:缺源目录时 exit 非 0,错误在 stderr
python scripts/sync_fixtures.py ./fixtures /tmp/out --dry-run; echo "exit=$?"
# 期望:exit=0,计划在 stdout
CI 里同理:
- run: python scripts/sync_fixtures.py ./fixtures ./out
# 下一步 job 依赖本步成功:此处必须靠非 0 挡住坏数据
5. 为何要跟 codex exec 的出口语义错开理解(一句动机)
官方文档写 codex exec 把进度打到 stderr、最终消息打到 stdout,方便管道(Non-interactive mode)。第三方整理也提到:完整 exit 码表官方未集中公布,脚本里宜把非 0 当失败,并用 --json/项目自测收口(Arantic 编译)。更扎手的是:有人复现“内层 command_execution 已失败,外层 codex exec 仍 exit 0”(#15536;Kogen)。所以:项目脚本自己的退出码要干净——别把“Agent 说做完了”当成 CI 绿灯。
是否已在你用的 CLI 版本修好,以发行说明与本机复现为准。
真实用户诀窍/反例
- 诀窍:退出码写进
--help正文。 后人(和下一轮 Agent)都看得到约定。 - 诀窍:用法错误用 2,业务失败用 1(或项目统一表)。 先固定一张小表,别每脚本一套。
- 诀窍:验收命令写进同一轮任务。 “贴 exit code”比“顺便测一下”更常真的跑。
- 诀窍:CI 看脚本码,再看测试码。 两层都绿再发布。
- 反例:except Exception: print(e) 然后正常结束。 日志有错、进程仍 0。
- 反例:把错误信息只打在 stdout 且 exit 0。 管道下游当正常数据吃进去。
- 反例:只教 Agent“写功能”,不提
--help。 两周后没人记得参数名。 - 反例:以为
codex exec一绿就等于仓库门禁过了。 外层 0 与项目自测是两件事。
适用边界
- 本稿是入门实操:让 Codex/Agent 交的 CLI 能被 CI/父脚本用
$?接住。 - 不覆盖:给
codex exec本进程设计完整 exit 码表、GitHub Action 密钥代理细节、每家公司的错误码规范长文。 #15536是否关闭、现行 CLI 是否已传播内层失败,以发行说明与本机复现为准;动机句保留“仍须项目自测”。- 示例代码可改语言;要点仍是 help+退出码+流分工。