behave

Zeichnet das Verhalten eines CLI-Tools (stdout, stderr, Exit-Codes) in Golden Tests auf und spielt sie anschließend gegen eine andere Implementierung ab, z. B. eine Neufassung in Rust oder Go. Funktioniert als CLI und als MCP-Server.

Dokumentation

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

behave records how a command-line tool actually behaves, then replays those cases against another implementation. I built it for CLI compatibility testing: checking a Rust or Go port against the original, or catching a refactor that changed visible behaviour.

It treats the tool as a black box. It reads --help and the man page, generates cases (flags, flag pairs, listed values, odd stdin, missing files, bad values), records stdout, stderr and the exit code, and diffs a second binary against that recording.

behave recording jq and checking gojq against it

I have used it on the Rust rewrites of GNU coreutils, findutils, sed and procps. So far that has led to 41 upstream bug reports and 48 fix PRs, 33 of them merged (as of 13 Nov 2026). The full list is in upstream findings.

Install

Homebrew (macOS and Linux), a binary from Releases, or go install:

brew install emirmrkaljevic/tap/behave
# or: go install github.com/emirmrkaljevic/behave/cmd/behave@latest

Example

Record GNU sort, check that the recording is stable, then compare a rewrite:

behave probe sort
behave run behave-sort
behave diff behave-sort --impl ./target/release/sort --skip-tag meta

--skip-tag meta drops --help and --version, which a rewrite is allowed to word differently.

A failing case looks like this (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 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]

The repo also ships ready-made specs under examples/. behave run examples/cut is a small GNU cut suite; behave diff examples/jq --impl gojq checks gojq against jq 1.7. The full list, with case counts, is in docs/examples.md.

For tools that need a positional argument, pass samples with --arg:

behave probe jq --arg . --arg '.a' --arg keys

Add a hand-written case with behave add behave-cut --stdin 'a,b' -- -d, -f2. The commands and spec format page covers the rest of the CLI, including coverage and JUnit output for CI.

MCP (optional)

behave mcp exposes the same probe, record and diff flow over stdio, if you want to drive it from an editor. Setup and tools are in docs/mcp.md.

Limitations

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

Recordings are only as good as the original you pointed at. Record from the release the rewrite actually targets. GNU coreutils 9.12 and 9.7 already disagree on some tail -v headers. Cases that change between runs, such as sort --random-sort, are marked flaky and skipped. Some differences are deliberate design choices, not bugs.

Status: early (v0.1). The CLI works; feedback is welcome.

Docs

MCP Registry name: io.github.emirmrkaljevic/behave

License

MIT