迁移基础 · 运行与表达 · LESSON 07

CLI、项目结构与配置

从 npm scripts 出发,掌握可测试的命令行入口、配置和目录组织。

12 分钟cli · project structure · configuration · testing

从一个命令找到整个项目

JS/TS 项目常用 npm run devnpm testnpm run build 作为入口。Python 项目会把命令放在模块、脚本或项目配置中,C++ 项目则由 CMake target 和生成的可执行文件组成。迁移 CLI 时,最重要的是保持“解析字符串参数、构造配置、调用核心逻辑、报告结果”的顺序,而不是把 Node 的参数库逐字换成另一个库。

无论语言怎样,用户真正需要的是一条清楚的启动路径:输入参数从哪里来、配置在哪里、日志输出到哪里、失败时退出码是什么。

TRANSLATION LENS 同一个意图,两种工程表达 窄屏可左右滑动查看完整代码
JS / TS
{
  "scripts": {
    "dev": "node src/cli.js --watch",
    "test": "vitest run"
  }
}
Python
python -m app.cli --input data/raw.json
python -m pytest
python -m app.cli --help

学习目标

  • 能从 argv 得到经过校验的配置,而不是让业务逻辑直接读取全局环境。
  • 能把命令解析、核心转换、文件读写和输出诊断拆成独立职责。
  • 能为成功、参数错误和输入失败规定稳定退出码,并写出可重复的测试输入。

推荐的目录职责

  • clicmd:解析参数,组装依赖,不承载全部业务逻辑。
  • domaincore:纯业务规则,尽量可以脱离网络和文件测试。
  • adaptersio:文件、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 与本地结果不同,先打印当前目录、实际执行命令和非敏感配置来源,再检查路径是否相对入口而非相对工作目录。不要通过把所有路径改成绝对路径掩盖不清晰的职责。

迁移练习

01
TRY IT YOURSELF

画出一个 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、退出与超时。

当前学习阶段运行与表达
0/7

本节是阶段检查点。完成练习后,再进入下一阶段。