CLI、项目结构与配置
从 npm scripts 出发,掌握可测试的命令行入口、配置和目录组织。
从一个命令找到整个项目
JS/TS 项目常用 npm run dev、npm test 和 npm run build 作为入口。Python 项目会把命令放在模块、脚本或项目配置中,C++ 项目则由 CMake target 和生成的可执行文件组成。迁移 CLI 时,最重要的是保持“解析字符串参数、构造配置、调用核心逻辑、报告结果”的顺序,而不是把 Node 的参数库逐字换成另一个库。
无论语言怎样,用户真正需要的是一条清楚的启动路径:输入参数从哪里来、配置在哪里、日志输出到哪里、失败时退出码是什么。
{
"scripts": {
"dev": "node src/cli.js --watch",
"test": "vitest run"
}
} python -m app.cli --input data/raw.json
python -m pytest
python -m app.cli --help 学习目标
- 能从
argv得到经过校验的配置,而不是让业务逻辑直接读取全局环境。 - 能把命令解析、核心转换、文件读写和输出诊断拆成独立职责。
- 能为成功、参数错误和输入失败规定稳定退出码,并写出可重复的测试输入。
推荐的目录职责
cli或cmd:解析参数,组装依赖,不承载全部业务逻辑。domain或core:纯业务规则,尽量可以脱离网络和文件测试。adapters或io:文件、HTTP、传感器和模型服务等外部边界。tests:用小数据验证每个边界,入口测试只检查组合关系。
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
try:
rows = read_rows(args.input)
result = normalize(rows, strict=args.strict)
write_rows(args.output, result)
except ValueError as exc:
print(f"invalid input: {exc}", file=sys.stderr)
return 2
return 0
配置优先级
配置优先级与可测试入口
配置经常来自默认值、配置文件、环境变量和命令行参数。先写出优先级,例如“默认值 < 文件 < 环境变量 < CLI”,然后在一个 load_config 函数中合并并校验。入口函数接收 argv 参数而不是直接读取全局 sys.argv,测试就可以传入固定参数;C++ 的 main(int argc, char** argv) 也应尽快转换成类型明确的 options 对象。
AI 项目尤其容易把模型路径、服务地址和超时散落在代码里。机器人项目还会增加设备名称、坐标系、频率和安全阈值。把配置集中起来,后续迁移会轻松很多。
配置来源要可解释
启动时可以打印 config_source=env timeout_ms=5000 这类安全摘要,但不要打印 API key。AI 项目常需要模型路径、服务地址和批大小;机器人项目还会增加设备名、坐标系、频率和安全阈值。集中配置能让同一份核心逻辑被 CLI、HTTP 服务和机器人节点复用。
常见错误与排错思路
一个常见错误是 CLI 解析器里直接打开文件、调用模型并打印结果,导致参数错误、IO 错误和业务错误无法区分。排错时先运行 --help 验证参数名称,再传入不存在文件、非法数字和空输入,观察退出码是否稳定;检查 stdout 是否仍只输出机器可读结果,诊断信息是否进入 stderr。若 npm 脚本在 CI 失败,展开脚本实际命令,确认工作目录和相对路径。
从参数到核心逻辑的可测入口
把解析函数设计成纯转换:给定参数与环境变量,得到配置或明确错误。入口层只组装依赖,真正的清洗规则不需要知道程序是从终端、测试还是服务调用它。
function parseOptions(argv) {
const flags = argv.filter((arg) => arg.startsWith("--"));
const positional = argv.filter((arg) => !arg.startsWith("--"));
if (flags.some((flag) => flag !== "--strict")) return { ok: false, code: "unknown_option" };
if (positional.length !== 1) return { ok: false, code: "expected_one_input" };
return { ok: true, input: positional[0], strict: flags.includes("--strict") };
}
console.log(parseOptions(["--strict", "readings.json"]));
预期结果是 { ok: true, input: "readings.json", strict: true }。验证时再分别传入空参数、未知开关、缺失文件和不可写目录;参数错误要在打开文件前被发现。跨语言时,Python 可把 argv 显式传给解析函数,C++ 则在 main 顶部把 argc/argv 转为 Options,从此业务层不用再看原始字符串。
让命令成为稳定接口
为 stdout 与 stderr 约定用途:stdout 输出给下游脚本解析的数据,stderr 输出人类可读诊断。给错误类别分配退出码,并让测试直接调用解析函数和核心函数;只在少数集成测试中启动真实子进程。这样既能快速覆盖分支,也能检测工作目录、文件权限与信号处理。
运行验证与退出语义
把解析逻辑作为纯函数传入模拟参数,先断言成功选项,再断言缺输入和未知开关。启动真实命令时分别观察 stdout、stderr 与退出码:成功应返回 0,参数无效应在读文件前失败,数据损坏不应被伪装成空输出。把这三类情况加入 CI,便能发现入口协议变化。
可以按表驱动方式验证同一个 CLI:
| 输入场景 | 预期行为 | 是否调用核心逻辑 |
|---|---|---|
| 参数完整、文件有效 | 输出结果,退出码 0 | 是 |
| 缺少必需参数 | stderr 提示用法,退出码非零 | 否 |
| 文件不存在或不可读 | 返回 IO 错误类别 | 仅在输入读取阶段 |
| 参数格式有效但数据违规 | 返回领域错误类别 | 是 |
检查工作目录变化时,优先让 CLI 接受显式路径或以配置好的项目根目录解析相对路径。不要让测试通过修改全局 cwd 影响其他并行测试;若必须测试 cwd 语义,使用独立子进程并在 finally 中恢复。
在真实子进程测试里另外覆盖中断和超时:父进程收到取消后应终止子进程、排空或关闭管道并等待回收。验证错误信息不包含秘密参数;给 stdout 限定格式,使下游无需解析人类提示语。这样命令既对用户可读,也能被脚本稳定组合。
当 CI 与本地结果不同,先打印当前目录、实际执行命令和非敏感配置来源,再检查路径是否相对入口而非相对工作目录。不要通过把所有路径改成绝对路径掩盖不清晰的职责。
迁移练习
画出一个 CLI 的启动路径
设计一个 normalize-data 命令:它接收输入文件、输出目录和严格模式三个参数,并写出参数错误、输入错误、成功三种退出语义。
给我一点提示
先写 cli.parse_args,再调用 domain.normalize,最后由 io 写文件;诊断输出到 stderr。
查看参考答案
main(argv) 解析参数并构造 Config;读取失败或格式错误返回 2,业务校验失败返回 3,成功写出文件返回 0。CLI 只组装 cli.parse_args、domain.normalize 和 io.write_rows,不在入口实现清洗规则。 本节结论
如果调用顺序可以用五六个函数名说明,项目边界通常已经足够清晰。薄入口让同一业务逻辑可以被测试、服务接口或机器人节点复用。
小结
命令行入口负责把字符串、环境和退出码转换成稳定的应用契约;核心逻辑通过参数注入,文件与模型等副作用放在适配器。下一步迁移到进程通信时,继续明确 stdout、stderr、退出与超时。
本节是阶段检查点。完成练习后,再进入下一阶段。