关于 behave
behave 将 CLI 的行为记录为测试用例,并可对原工具或另一实现重放。它面向移植、重构或比较命令行工具的开发者,规格可审阅并用于 CI。
主要功能
- 01根据帮助文档生成用例
- 02记录标准输出、错误输出和退出码
- 03对另一实现重放用例
- 04统计文档所列选项的覆盖率
- 05规范化可变输出并识别不稳定用例
- 06生成供 CI 使用的 JUnit 报告
适用场景
- 01检查 CLI 重写版本的兼容性
- 02发现重构或升级后的回归
- 03比较两个命令行实现
- 04跟踪选项测试情况并在 CI 中报告
MCP 相关
MCP 服务提供运行命令、生成和扩展行为规格、列出用例,以及将实现与记录行为进行比较的工具。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
我们还没有获取到这个服务的安装包或远程地址,请按项目 README 中的说明安装。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
behave
如果你觉得这个项目有用,可以购买 SkinToScan 套装来支持我,这是我的另一个项目。这是一次性购买,收入将直接用于资助我的开源工作。
记录任意 CLI 的行为,证明你的重写版本行为一致。
behave 将命令行工具视为黑盒。它会读取 --help(以及 man 页面),
生成数百个测试用例(覆盖每个标志、标志对、帮助中列出的每个标志取值、
stdin 边界情况、文件参数、错误取值、缺失文件),记录工具的实际行为(stdout、stderr、退出码),
并针对另一个实现重放这些用例。

适用于以下场景:
- 将工具移植到 Rust 或 Go,并希望证明移植版本兼容;
- 重构或升级 CLI,并希望在几秒内建立回归防护网;
- 比较同一工具的两个实现。
状态:早期版本(v0.1)。CLI 和 MCP 服务已可用;欢迎反馈。
快速开始
使用 Homebrew(macOS 和 Linux)安装,从 Releases 下载二进制文件,或使用 go install:
brew install emirmrkaljevic/tap/behave
# or: go install github.com/emirmrkaljevic/behave/cmd/behave@latest
# 1. record how GNU sort behaves (writes ./behave-sort/)
behave probe sort
# 2. check that the recording is stable
behave run behave-sort
# 3. compare another implementation against it
behave diff behave-sort --impl ./target/release/sort --skip-tag meta
也可以先尝试已提交的示例:behave run examples/cut(27 个 GNU cut 用例)、
behave run examples/paste(18 个 GNU paste 用例)、behave run examples/join(26 个 GNU join 用例)、behave run examples/nl(26 个 GNU nl 用例)、behave run examples/fold(28 个 GNU fold 用例),或 behave run examples/jq(33 个 jq 1.7 用例);请参阅 examples/。
对于需要位置参数(过滤器、模式、字段列表)的工具,请提供样例:
behave probe jq --arg . --arg '.a' --arg '.[]' --arg 'map(.+1)' --arg keys
behave diff behave-jq --impl gojq --ignore-stderr
如果帮助文本列出了标志可接受的取值,probe 会在错误取值用例旁为每个取值添加一个用例。它支持 GNU 风格(WHEN can be: auto, always, never、--sort=WORD ... to WORD: none (-U), size (-S), ...、The WHEN argument defaults to 'always' and can also be 'auto' or 'never'.)、clap 的 [possible values: ...]、argparse 的 {json,yaml} 和 <fast|safe>。它只保留能够关联到相应标志的纯小写列表。--dry-run 会显示它找到的内容:
$ behave probe ls --dry-run
--color WHEN {always,auto,never}
--sort WORD {none,size,time,version,extension,name,width}
输出示例(节选):
✗ flag --output-delimiter, value -1, arg -f1 0135-flag-output-delimiter-value-1-arg-f1.yaml
$ cut --output-delimiter -1 -f1
exit:
exit: want 0, got 1
stdout:
- hello
flags with differences:
--output-delimiter 2/16 cases differ
flags fully compatible: 9 of 10
cut vs ./uu/cut: 172/175 passed, 3 failed, 0 skipped (differs in: exit 2, stdout 3) [0.1s]
首日发现结果
在此机器上针对真实工具运行,仅使用生成的用例,并排除 --help/--version:
| 原版 | 候选版本 | 用例数 | 通过数 | 显著差异 |
|---|---|---|---|---|
| GNU wc 9.7 | uutils wc 0.12.0 | 63 | 63 | 无 |
| GNU head | uutils head | 51 | 51 | 无 |
| GNU uniq | uutils uniq | 100 | 100 | 无 |
| GNU tr | uutils tr | 63 | 63 | 无 |
| GNU tac 9.12 | uutils tac (main) | 36 | 35 | 拒绝 -s -1(值以 - 开头,#15497,现已修复) |
| GNU tail 9.12 | uutils tail (main) | 60 | 58 | 接受不带计数的 -c(#15528,现已修复);文件名如 007 被打印为 7(#15533,现已在 tail 中修复);手动添加的 -cf FILE 用例被拒绝(#15517) |
| GNU cut | uutils cut | 175 | 172 | 拒绝 --output-delimiter -1(值以 - 开头) |
| GNU base64 | uutils base64 | 36 | 31 | 对无效输入使用 -d:GNU 会打印已解码的字节,uutils 不打印任何内容 |
| GNU sort | uutils sort | 290 | ~260† | 用法错误的退出码是 1 而非 2;--debug 下划线宽度;--check --merge |
| jq 1.7 | gojq 0.12.19 | 596 | 331* | 不支持的标志(--seq、--ascii-output、…)、键顺序、大数格式 |
* 使用 --ignore-stderr;几乎所有情况下的错误消息措辞都不同。
† 结果会有一两个用例的浮动:有些 --random-sort 用例在输入极小时,有时才会被检测为不稳定。
其中许多是有意为之的设计选择,并非 bug。behave 只是让它们显现出来。
请记录改写实际针对的版本。uutils 会跟随最新的 GNU 版本,而 GNU 9.12 在 tail -v 标头中会打印 ==> 'standard input' <==,9.7 则不带引号,因此基于旧版 GNU 记录的规格会报告一些实际上是 GNU 变更造成的差异。behave.yaml
会保留所记录工具的 --version 行,run/diff 会在回放版本旁打印该行。
其中一些是真正的 bug,现已在上游修复:
- uutils
cut --output-delimiter -x(uutils/coreutils#15341,已在 #15345 修复) - uutils
sort --check --merge忽略了检查(uutils/coreutils#15380,已在 #15381 修复) - uutils
tac -s -1拒绝以-开头的分隔符(uutils/coreutils#15497,已在 #15498 修复) - uutils
tail 007将缺失的文件打印为'7'(uutils/coreutils#15533,已在 #15534 中修复 tail;其他工具中的同一 bug 正逐一修复:tac在 #15541 中(已合并),nl在 #15544 中(已合并),md5sum/cksum在 #15548 中(已合并),sort在 #15565 中(已合并),shred在 #15604 中(已合并)) - uutils 裸用
tail -c和tail +NUM时忽略了_POSIX2_VERSION(uutils/coreutils#15528,已在 #15529 修复)
仍未解决:uutils sort -c dir 会以 0 退出且不显示消息,而不是报告读取错误(uutils/coreutils#15615,修复见 #15616);另外还有一些错误消息会丢失文件名,这些问题大多由 v0.1.7 中新增的“文件名末尾带斜杠”探测用例发现:
cp f/ dest会打印Not a directory (os error 20),而不是cannot stat 'f/': Not a directory(#15691,修复见 #15692)paste打开和读取文件出错时未显示文件名(#15254,修复见 #15642)shred dir会显示Not a file,而不是failed to open for writing: Is a directory(#15630,修复见 #15631)mv missing/ g会显示Not a directory,而对不可搜索的d执行mv d/f g会显示No such file or directory;GNU 会给出真正的原因(#15704,修复见 #15705)
MCP 服务
behave mcp 通过 stdio 提供相同的功能,因此编码助手可以探索原始工具,在工作过程中扩展规范,并在每次更改后检查其移植版本。
{
"mcpServers": {
"behave": { "command": "behave", "args": ["mcp"] }
}
}
| 工具 | 功能 |
|---|---|
behave_exec | 在临时目录中运行一次命令,返回退出状态、stdout 和 stderr(用于探索) |
behave_probe | 根据 --help 生成并记录规范 |
behave_add_case | 添加一个测试用例,并记录原始工具的行为 |
behave_list_cases | 列出测试用例,可按标签筛选(flag:--sort、manual) |
behave_diff | 针对某个实现重放测试用例,并返回带差异的失败用例 |
规范格式
规范是一个可提交并审查的目录:
behave-cut/
behave.yaml # command, timeout, normalizers, recorded_from (--version line)
cases/
0001-help.yaml
0135-flag-output-delimiter-value-1-arg-f1.yaml
...
name: flag --output-delimiter, value -1, arg -f1
args: [--output-delimiter, "-1", -f1]
stdin: |
hello
tags: [flag-value, "flag:--output-delimiter"]
expect:
exit: 0
stdout: |
hello
stderr: ""
测试用例还可以设置 env、files(在全新的临时目录中创建)和 timeout_ms。
使用 behave add behave-cut --stdin 'a,b' -- -d, -f2 添加自己的测试用例。
如何确保结果具有确定性
- 每个测试用例都会在新的临时目录中运行,并使用固定环境
(
LC_ALL=C.UTF-8、TZ=UTC、NO_COLOR=1、HOME=临时目录) - 输入文件的修改时间固定;读取当前目录的工具(
ls、du、rg;帮助文本中提到“当前目录”或递归操作的工具)会获得一份小型示例 目录树(--sample-files auto|on|off),其中文件名、大小和扩展名的排序方式各不相同, 因此ls --sort=size、ls -t、du --time和du -S会生成值得比较的输出 - 每个测试用例都会记录 3 次;运行结果有任何变化都会标记为
flaky并跳过 (例如sort --random-sort) - 比较前,规范化器会移除噪声:
ansi、workdir、cmd(jq:与gojq:)、timestamp、hexaddr、duration、pid,以及你在behave.yaml中定义的正则规则 - 超时会终止整个进程组
命令
| 命令 | 功能 |
|---|---|
behave probe <cmd> | 发现标志、生成并记录用例(--arg、--inputs text|json|both、--max、--skip-flag、--sample-files、--dry-run) |
behave record <spec> | 根据原始程序重新记录预期结果 |
behave add <spec> -- ARGS | 添加一个手动编写的用例 |
behave run <spec> | 对照原始程序重放 |
behave diff <spec> --impl <bin> | 对照另一个实现重放(--ignore-stderr、--ignore-exit、--tag、--skip-tag、--json、--junit file、-v) |
behave coverage <spec> | 查看用例覆盖了哪些有文档说明的标志(来自 --help/man)(--min PCT、--all、--json) |
behave normalizers | 列出内置规范化器 |
如果有任何用例失败,run 和 diff 会以 1 退出,因此可直接用于 CI。传入 --junit report.xml 可生成 JUnit XML 报告,供 CI 控制面板使用(GitHub Actions、Jenkins 等)。
coverage 会告诉你某个 spec 尚未测试哪些内容。以手动挑选的 cut 示例为例:
$ behave coverage examples/cut
cut: 27 cases exercise 7 of 12 documented flags (58%)
not exercised (5): -n --zero-terminated --help --version -M
used but not in help: --no-such-flag
它读取标志的方式与 probe 相同,能够识别 --long=value、无歧义的长标志前缀和组合短标志(-sf1);当覆盖率低于 80% 时,传入 --min 80 会使其以 1 退出。
仓库提供了一个多阶段 Dockerfile,用于构建二进制文件,并通过标准输入输出启动 behave mcp(适用于 Glama 和其他从 Dockerfile 构建的 MCP 注册表)。
规划
- 提交更多示例 spec(
head、tail、wc,以及与 ripgrep 对比的grep子集) - Homebrew tap
安全
behave 只会在临时目录中运行你指定的二进制文件。不要在未使用 --skip-flag 和沙盒的情况下,对会更改工作目录之外内容的工具(包管理器、rm -rf 类工具)进行探测。
链接
- MCP Registry 名称:
io.github.emirmrkaljevic/behave
许可证
MIT
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。