cli 设计

设计目标

其他每个包要么是纯的,要么把副作用限制在测量循环内。cli 是文件、标准流、进程执行和退出码所在之处。它让这一适配层保持精简,并使唯一危险的操作——执行记录在产物中的命令——变得显式。

数学背景

命令行是一个从参数向量到请求的小型全函数,后接一个带副作用的解释器:

argv→ parse_args CliRequest→ main effects×{0,1,2}.\text{argv} \xrightarrow{\ \texttt{parse\_args}\ } \text{CliRequest} \xrightarrow{\ \texttt{main}\ } \text{effects} \times \{0, 1, 2\}.

parse_args 从不失败:每个向量都映射到一个请求,错误记录在某个字段中。这使解析器在每个目标上都可测试,并把“请求了什么”与“发生了什么”分开。退出码把结果划分为成功(00)、运行时失败(11:IO、无效数据、重放失败)和用法错误(22),这样脚本就能区分输入有误与调用有误。

设计决策

纯辅助函数,native 副作用

问题。 进程执行和异步标准流只存在于 native 目标上。选择。 解析、帮助文本、JSONL 重放信息提取和报告渲染都是 cli.mbt 中的普通函数;native 的 main.mbt 是异步的,使用 moonbitlang/async 处理 stdin、stdout 和进程;其他目标使用 main_unimplemented.mbt,它支持文件到文件的 report 并拒绝 replay。理由。 同一个包可以在每个目标上构建,逻辑也无需启动任何进程即可测试。

受保护的重放

问题。 重放产物包含一条命令行;从某个 issue 中找到的文件里盲目执行它会运行任意代码。选择。 replay 要求输入文件(不接受 stdin),用 --dry-run 打印命令,只有带 --yes 时才执行,强制使用记录的超时,并在退出码非零时失败。理由。 安全的默认行为是先查看;执行是显式的第二步。

只读取所需内容

replay_spec_from_jsonl 在检查第一个 validation_failure 之前每一行的版本后,停在该事件处,并只读取三个重放字段。包含大量事件和一个失败的流可以正常处理,而来自未知版本的产物会在任何东西运行之前就被拒绝。

可移植的报告

report 读取全部输入,以目标标签 native 构建文档,并写出一个 HTML 文件。输入或输出使用 - 时会连接到管道,因此它能与其他工具组合。

正确性与不变量

  • parse_args 是全函数,且不接触环境。
  • replay 没有 --yes 时从不执行,在非 native 目标上也从不执行。
  • 用法错误以 2 退出;运行时失败以 1 退出。
  • 解析事件失败时,report 不写出任何内容。

被否决的方案

  • 标志解析库。 两个命令和四个标志不足以引入一个依赖。
  • 默认执行重放。 对来自别处的产物不安全。
  • 把 CLI 逻辑嵌入 report。 会把文件 IO 带入一个纯包。

边界

  • 没有 run 命令:基准测试是 MoonBit 代码,通过对你自己的包执行 moon test 或 moon run 来运行。
  • --open 使用 macOS 的 open 命令。
  • replay 只读取文件,只执行第一个失败,并把命令直接传给进程 API(不经过 shell)。
  • 进度行打印的是一个以微秒计、却标注为 ms 的值。
  • 版本字符串在源码中固定。