behave

io.github.emirmrkaljevic由 GitHub 账号 emirmrkaljevic 发布v0.1.7更新于 Oct 24, 2026

Record how any CLI behaves and check that a rewrite behaves the same (golden tests).

已验证STDIO仅桌面开发者工具安全与监控

概览

AI 生成的概览

记录命令行工具的行为(依据帮助文本生成用例),再把记录回放到另一实现上,用来证明重写版本行为一致。

可以做什么
  • 在临时目录中运行一次命令,返回退出码、标准输出和标准错误
  • 根据工具的帮助文本和手册页生成并记录测试用例规格
  • 添加手写用例,并按标签列出已记录的用例
  • 把规格回放到另一实现上,返回失败用例及其差异
适用场景
适合把命令行工具移植到其他语言、重构或升级 CLI 时建立回归保障,或对比同一工具的两个实现。
运行要求
以本地进程方式通过 stdio 运行,用 behave 可执行文件加 mcp 参数启动。可通过 Homebrew、发布版二进制或 go install 安装。未声明需要账号、API 密钥或环境变量。
安装前请注意
它会实际执行你指定的二进制,因此不要在没有跳过相关参数和沙箱的情况下探测会改动工作目录之外内容的工具(如包管理器、破坏性命令)。README 还提到可通过购买作者另一个项目的一次性产品来支持开发。

安装

在 SourceWeft 中

  1. 打开 控制台中的 behave,将其添加到工作区。
  2. 为需要使用其工具的对话启用该服务。

Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。

其他 MCP 客户端

参照 仓库 中的启动说明。

README

behave

[Support my open-source work: get a SkinToScan pack]
If you find this useful, you can support me by getting a SkinToScan pack, my other project. It's a one-time purchase that directly funds my open-source work.

[Buy Single Picture Check, €4.49] [Buy 5 Picture Pack, €19.99 (popular)] [Buy 15 Picture Pack, €54.99]

Record how any CLI behaves. Prove your rewrite behaves the same.

Proof: running it against uutils coreutils (the Rust rewrite of GNU coreutils) has so far led to 30 upstream bug reports and 37 fix PRs, 21 of them merged (as of Nov 6, 2026). See what it found.

behave treats a command-line tool as a black box. It reads --help (and the man page), generates hundreds of test cases (every flag, flag pairs, every value the help lists for a flag, edge-case stdin, file arguments, bad values, missing files), records what the tool actually does (stdout, stderr, exit code), and replays those cases against another implementation.

[behave recording jq and checking gojq against it]

Use it when you are:

  • porting a tool to Rust or Go and want proof that the port is compatible,
  • refactoring or upgrading a CLI and want a regression net in a few seconds,
  • comparing two implementations of the same tool.

Porting a CLI?

If you are rewriting an existing command-line tool in Rust or Go, behave gives you a compatibility suite without reading the original source. Three commands:

sh
behave probe sort                      # record what the original does, case by casebehave run behave-sort                 # make sure the recording is stablebehave diff behave-sort --impl ./target/release/sort   # every place your port differs

This is how it led to 30 bug reports and 37 fix PRs in uutils coreutils (21 merged so far). A second, ready to run pair: behave diff examples/jq --impl gojq passes 29 of 33 jq 1.7 cases, and the 4 differences are all error wording on stderr (see examples/jq).

Status: early (v0.1). CLI and MCP server work; feedback welcome.

Quick start

Install with Homebrew (macOS and Linux), download a binary from Releases, or use go install:

sh
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 stablebehave run behave-sort
# 3. compare another implementation against itbehave diff behave-sort --impl ./target/release/sort --skip-tag meta

Or try the committed examples first: behave run examples/cut (27 GNU cut cases), behave run examples/paste (18 GNU paste cases), behave run examples/join (26 GNU join cases), behave run examples/nl (26 GNU nl cases), behave run examples/fold (28 GNU fold cases), behave run examples/head (24 GNU head cases), behave run examples/expand (21 GNU expand cases), behave run examples/unexpand (26 GNU unexpand cases), behave run examples/tr (29 GNU tr cases), behave run examples/comm (30 GNU comm cases), behave run examples/uniq (31 GNU uniq cases), behave run examples/sort (35 GNU sort cases), behave run examples/cat (39 GNU cat cases), behave run examples/wc (38 GNU wc cases), behave run examples/tac (33 GNU tac cases), behave run examples/numfmt (41 GNU numfmt cases), behave run examples/od (36 GNU od cases), behave run examples/basenc (32 GNU basenc cases), behave run examples/seq (39 GNU seq cases), behave run examples/expr (43 GNU expr cases), behave run examples/printf (38 GNU printf cases), behave run examples/factor (31 GNU factor cases), behave run examples/cksum (35 GNU 9.12 cksum cases), behave run examples/md5sum (37 GNU 9.12 md5sum cases), behave run examples/tail (33 GNU 9.12 tail cases), behave run examples/base64 (34 GNU 9.12 base64 cases), behave run examples/base32 (36 GNU 9.12 base32 cases), behave run examples/basename (35 GNU basename cases), behave run examples/dirname (26 GNU dirname cases), behave run examples/fmt (36 GNU 9.12 fmt cases), behave run examples/realpath (38 GNU 9.12 realpath cases), behave run examples/pr (40 GNU 9.12 pr cases), or behave run examples/jq (33 jq 1.7 cases); see examples/.

For tools that need a positional argument (a filter, a pattern, a field list), give samples:

sh
behave probe jq --arg . --arg '.a' --arg '.[]' --arg 'map(.+1)' --arg keysbehave diff behave-jq --impl gojq --ignore-stderr

When the help text lists the values a flag accepts, probe adds one case per value next to the invalid-value cases. It understands GNU style (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's [possible values: ...], argparse's {json,yaml} and <fast|safe>. It only keeps plain lowercase lists it can tie to that flag. --dry-run shows what it found:

$ behave probe ls --dry-run       --color                      WHEN  {always,auto,never}       --sort                       WORD  {none,size,time,version,extension,name,width}

Example output (abridged):

✗ 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 differflags fully compatible: 9 of 10
cut vs ./uu/cut: 172/175 passed, 3 failed, 0 skipped  (differs in: exit 2, stdout 3)  [0.1s]

What it found on day one

Run on this machine against real tools, generated cases only, --help/--version excluded:

originalcandidatecasespassnotable differences
GNU wc 9.7uutils wc 0.12.06363none
GNU headuutils head5151none
GNU uniquutils uniq100100none
GNU truutils tr6363none
GNU tac 9.12uutils tac (main)3635-s -1 rejected (value starting with -, #15497, now fixed)
GNU tail 9.12uutils tail (main)6058-c with no count accepted (#15528, now fixed); file names like 007 printed as 7 (#15533, now fixed in tail); a hand-added -cf FILE case is rejected (#15517)
GNU cutuutils cut175172--output-delimiter -1 rejected (value starting with -)
GNU base64uutils base643631-d on invalid input: GNU prints the bytes decoded so far, uutils prints nothing
GNU sortuutils sort290~260†usage errors exit 1 instead of 2; --debug underline width; --check --merge
jq 1.7gojq 0.12.19596331*unsupported flags (--seq, --ascii-output, …), key order, big-number formatting

* with --ignore-stderr; error message wording differs almost everywhere. † varies by one or two: some --random-sort cases are only sometimes detected as flaky with tiny inputs.

Many of these are deliberate design choices, not bugs. behave just makes them visible. Record from the release the rewrite actually targets. uutils follows the newest GNU release, and GNU 9.12 prints ==> 'standard input' <== in tail -v headers where 9.7 prints no quotes, so a spec recorded from an older GNU reports differences that are really GNU changes. behave.yaml keeps the --version line of the recorded tool, and run/diff print it next to the version being replayed. Some were real bugs and are now fixed upstream:

Still open: a few error messages that lose the file name, mostly caught by the "file with trailing slash" probe case added in v0.1.7:

  • cp f/ dest prints Not a directory (os error 20) instead of cannot stat 'f/': Not a directory (#15691, fix in #15692)
  • paste open and read errors without the file name (#15254, fix in #15642)
  • shred dir says Not a file instead of failed to open for writing: Is a directory (#15630, fix in #15631)
  • mv missing/ g says Not a directory and mv d/f g with an unsearchable d says No such file or directory, where GNU gives the real reason (#15704, fix in #15705)
  • ln f/ link and ln missing link say failed to create hard link 'link' => ... instead of naming the source with failed to access 'f/' (#15713, fix in #15714)
  • mv a missing/b and mv a ro/b print only No such file or directory (os error 2) or Permission denied (os error 13), where GNU says cannot move 'a' to 'missing/b': ... (#15721, fix in #15722)
  • mv -T f d (file onto an existing directory) prints cannot overwrite directory 'd' with non-directory without the source, where GNU adds 'f' (#15724, fix in #15725)
  • mv -n z z (and --update=none) fails with 'z' and 'z' are the same file, where GNU skips the move and exits 0 (#15824, fixed in #15825)
  • chown 1000 f/ and chgrp 1000 missing say cannot dereference 'f/', where GNU says cannot access and keeps cannot dereference for dangling symlinks (#15867, fix in #15868)
  • chmod -v 644 missing prints nothing on stdout, and for a dangling symlink a made-up from 0000 (---------) to 1500 (r-x-----T) line, where GNU prints 'missing' could not be accessed (#15883, fix in #15884)
  • tr a '[:alpha:]' prints the set2 class error on one line and says set2, where GNU splits it over two lines and says string2 (#15948, fix in #15949)
  • uniq -f x says Invalid argument for skip-fields: x, where GNU says x: invalid number of fields to skip (same for -s and -w), caught by the new examples/uniq spec (#15970, fix in #15971)
  • sort -t ab and sort -t '' word the bad separator errors differently from GNU, and sort -c a b adds a usage hint GNU doesn't print, caught by examples/sort (#15975, fix in #15976)
  • od -A q and od -t x3 word the invalid radix and type string errors differently from GNU, caught by examples/od (#16071, fix in #16072)
  • seq --foo says invalid floating point argument and seq -inf 1 prints -inf forever, because option-like arguments before the first operand are read as numbers where GNU treats them as options, caught by examples/seq (#16113, fixed in #16114)
  • factor '30 ' and factor $'\t30' print factors where GNU rejects the argument, because every argument is trimmed while GNU skips only leading spaces, caught by examples/factor (#16142, fixed in #16143)
  • md5sum -c rejects the lines md5sum itself writes for names with a backslash or a newline (they start with \), and unescapes names on lines without that marker, caught by examples/md5sum (#16166, fixed in #16167)
  • base64 -w 99999999999999999999 fails with invalid wrap size where GNU turns wrapping off for a size too large to represent, caught by examples/base64 (#16180, fix in #16181)
  • base32 -d (and base64/basenc) accepts a carriage return in encoded input where GNU rejects it unless -i, caught by examples/base32 (#16223, fix in #16224)
  • fmt -w 20 could print a line longer than 20 columns (e.g. at a narrow width), because it sometimes broke a line too late, caught by examples/fmt (#16190, fix in #16191)
  • realpath '' /usr stopped with error: error: invalid value for one of the arguments and never printed /usr, while GNU reports '' as a missing file and resolves the rest, caught by examples/realpath (#16206, fix in #16207)
  • pr -o indented body lines but not the page header (GNU pads the first blank separator line and the header text), caught by examples/pr (#16211, fix in #16212)

MCP server

behave mcp serves the same features over stdio, so a coding assistant can explore the original tool, grow the spec while it works, and check its port after every change.

json
{  "mcpServers": {    "behave": { "command": "behave", "args": ["mcp"] }  }}
toolwhat it does
behave_execrun a command once in a temp dir, return exit/stdout/stderr (exploration)
behave_probegenerate and record a spec from --help
behave_add_caseadd a case and record the original's behavior
behave_list_caseslist cases, optionally by tag (flag:--sort, manual)
behave_diffreplay against an implementation, return failing cases with diffs

Spec format

A spec is a directory you can commit and review:

behave-cut/  behave.yaml           # command, timeout, normalizers, recorded_from (--version line)  cases/    0001-help.yaml    0135-flag-output-delimiter-value-1-arg-f1.yaml    ...
yaml
name: flag --output-delimiter, value -1, arg -f1args: [--output-delimiter, "-1", -f1]stdin: |  hellotags: [flag-value, "flag:--output-delimiter"]expect:  exit: 0  stdout: |    hello  stderr: ""

Cases can also set env, files (created in a fresh temp dir), and timeout_ms. Add your own with behave add behave-cut --stdin 'a,b' -- -d, -f2.

How it stays deterministic

  • every case runs in a new temp directory with a fixed environment (LC_ALL=C.UTF-8, TZ=UTC, NO_COLOR=1, HOME=temp dir)
  • input files get fixed modification times, and tools that read the current directory (ls, du, rg; help text mentions "current directory" or recursion) get a small sample tree (--sample-files auto|on|off) whose names, sizes and extensions sort differently, so ls --sort=size, ls -t, du --time and du -S produce output worth comparing
  • each case is recorded 3 times; anything that changes between runs is marked flaky and skipped (e.g. sort --random-sort)
  • normalizers remove noise before comparing: ansi, workdir, cmd (jq: vs gojq:), timestamp, hexaddr, duration, pid, plus your own regex rules in behave.yaml
  • timeouts kill the whole process group

Commands

commandwhat it does
behave probe <cmd>discover flags, generate and record cases (--arg, --inputs text|json|both, --max, --skip-flag, --sample-files, --dry-run)
behave record <spec>re-record expectations from the original
behave add <spec> -- ARGSadd one hand-written case
behave run <spec>replay against the original
behave diff <spec> --impl <bin>replay against another implementation (--ignore-stderr, --ignore-exit, --tag, --skip-tag, --json, --junit file, -v)
behave coverage <spec>which documented flags (from --help/man) the cases exercise (--min PCT, --all, --json)
behave normalizerslist built-in normalizers

run and diff exit with 1 when any case fails, so they drop straight into CI. Pass --junit report.xml to write a JUnit XML report for CI dashboards (GitHub Actions, Jenkins, etc.).

coverage tells you what a spec doesn't test yet. On the hand-picked cut example:

$ behave coverage examples/cutcut: 27 cases exercise 7 of 12 documented flags (58%)not exercised (5): -n --zero-terminated --help --version -Mused but not in help: --no-such-flag

It reads flags the same way probe does, understands --long=value, unambiguous long prefixes and clustered short flags (-sf1), and exits 1 with --min 80 when coverage drops below 80%.

The repo ships a multi-stage Dockerfile that builds the binary and starts behave mcp on stdio (for Glama and other MCP registries that build from a Dockerfile).

Roadmap

  • more committed example specs (head, tail, wc, a grep subset vs ripgrep)
  • Homebrew tap

Safety

behave only runs the binaries you point it at, in a temp directory. Don't probe tools that change things outside their working directory (package managers, rm -rf-style tools) without --skip-flag and a sandbox.

Links

  • MCP Registry name: io.github.emirmrkaljevic/behave

License

MIT

来源:README.md,提交 2504c27

工具

0
SourceWeft 曾在隔离沙箱中以无凭据方式启动该软件包一次,以列出其工具,但未能完成:该软件包类型无法在沙箱中运行。

版本历史

7
  1. v0.1.7最新Oct 23, 2026
  2. v0.1.6Oct 20, 2026
  3. v0.1.5Oct 18, 2026
  4. v0.1.4Oct 17, 2026
  5. v0.1.3Oct 16, 2026
  6. v0.1.2Oct 14, 2026
  7. v0.1.1Oct 14, 2026

评分

已满足适用检查项的 50%

该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。

  • 近 30 天内可连通25 分

    请保持端点能够响应 MCP 握手;SourceWeft 每 30 天探测一次。

  • 已声明认证方式10 分

    请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。

  • 已列出工具15 分

    请让服务器在无凭据时响应 tools/list,或发布可在沙箱中无凭据启动的软件包。

  • 描述必需15 分
  • 图标10 分
  • 文档15 分
  • 许可证10 分