README
¶
cmdcomp
cmdcomp -- compare the output of two commands with optional preprocessing and customizable diff
Usage
cmdcomp [flags] -- COMMON_ARGS [-- LEFT_ARGS [-- RIGHT_ARGS]]
MCP Server
cmdcomp can run as a Model Context Protocol (MCP) server over stdio using the --mcp flag. This allows LLMs and AI agents to invoke cmdcomp as a tool.
MCP Server Registration Example
Add the following to your MCP client configuration:
{
"mcpServers": {
"cmdcomp": {
"command": "cmdcomp",
"args": ["--mcp"]
}
}
}
Tool Call Arguments Example (JSON)
When an AI agent invokes tools on the cmdcomp MCP server, use JSON arguments as follows:
// 1. Basic command diff
{
"name": "cmdcomp_diff",
"arguments": {
"common_args": ["echo"],
"left_args": ["hello left"],
"right_args": ["hello right"]
}
}
// 2. Diff with preset and custom diff tool
{
"name": "cmdcomp_diff",
"arguments": {
"presets": ["json"],
"diff": "diff -u",
"common_args": ["cat"],
"left_args": ["left.json"],
"right_args": ["right.json"]
}
}
// 3. Diff using literal stdin content
{
"name": "cmdcomp_diff",
"arguments": {
"common_args": ["cat"],
"left_preprocess": ["sed 's/foo/bar/'"],
"right_preprocess": ["sed 's/foo/baz/'"],
"stdin_content": "foo 123\n"
}
}
// 4. Generate dry-run script
{
"name": "cmdcomp_dryrun",
"arguments": {
"common_args": ["echo"],
"left_args": ["a"],
"right_args": ["b"]
}
}
Available Tools
cmdcomp_diff: Compare stdout of two commands or snapshots with optional preprocessing filters and custom diff tools.cmdcomp_dryrun: Generate an executable bash script capturing the full execution pipeline.cmdcomp_list_presets: List available presets defined in configuration files or built-in presets.
Lifecycle & Data Flow
cmdcomp executes subcommands and pipelines in the following order:
[stdin input] ('-' or '@filename')
│ (replicated to both commands if --stdin is specified)
├───────────────────────────────┐
▼ ▼
[left command] [right command]
│ ▲
│ ──► [interceptor hooks] ──────┘ (if interceptor specified: run sequentially)
│ │
▼ (stdout) ▼ (stdout)
[preprocess:left pipeline] [preprocess:right pipeline]
(common + left-preprocess) (common + right-preprocess)
│ │
▼ ▼
[left tempfile] [right tempfile]
│ │
└───────────────┬───────────────┘
▼
[diff command] (e.g. diff left_file right_file)
│
▼
[cleanup hooks] (guaranteed to run via defer)
- startup: Setup commands run sequentially before executing left/right commands (e.g. helm repo update).
- stdin setup: If '--stdin', '--left-stdin', or '--right-stdin' is specified, input from stdin ('-') or files ('@filename') is prepared for left and right commands (individual '--left-stdin' / '--right-stdin' takes precedence over '--stdin').
- snapshot setup: If '--snapshot', '--left-snapshot', or '--right-snapshot' is specified, command execution is skipped for that side and the given input is used directly as the command output. Individual '--left-snapshot' / '--right-snapshot' takes precedence over '--snapshot'. Both '--stdin' and '--snapshot' cannot be used together for the same side.
- left command & right command:
- Without interceptor: Left and right commands run concurrently.
- With interceptor: Left command runs first -> interceptor hooks run sequentially (e.g. git checkout ) -> Right command runs.
- Sides with a snapshot configured are skipped entirely.
- preprocess pipeline: Standard output of left and right commands (or snapshot inputs) are piped through preprocess filters:
- Left output: piped through preprocess -> left-preprocess
- Right output: piped through preprocess -> right-preprocess
- diff: Output files from the preprocess pipelines are passed to the diff tool (' LEFT_FILE RIGHT_FILE').
- cleanup: Teardown hooks are guaranteed to run when cmdcomp exits, even on failure or error.
Examples
Basic Comparison
Compare the standard output of two commands:
# Equivalent shell workflow:
# echo a > leftfile
# echo b > rightfile
# diff leftfile rightfile
cmdcomp -- echo -- a -- b
Custom Diff Tool & Label
Use a customized diff tool (e.g. 'diff -u', 'colordiff') and pass argument labels with '-l' / '--label':
# Unified diff with labels:
cmdcomp -x 'diff -u' -l -- echo -- a -- b
Common Preprocess Pipeline
Apply filter pipelines (e.g. jq, yq, sed) to both outputs before diffing:
# Filter both command outputs through sed:
cmdcomp -p 'sed "s|a|c|"' -- echo -- a -- b
Asymmetric Preprocessing (Left & Right)
Apply specific preprocess filters only to the left or right command output in addition to common filters:
# Replace strings differently on left vs right side:
cmdcomp --left-preprocess 'sed "s|a|c|"' --right-preprocess 'sed "s|a|d|"' -- echo -- a -- a
Startup Hooks & Advanced Preprocessing
Run setup commands sequentially before running the compare commands:
# Run startup hook before comparing filtered outputs:
cmdcomp -s 'echo "starting setup"' -p 'sed "s|foo|bar|"' -x 'diff -u' -- echo -- foo1 -- foo2
Sequential Execution with Interceptor
Execute left command first, run interceptor hooks (e.g. git checkout, state update), then run right command:
# Run interceptor hook between left and right commands:
cmdcomp -i 'echo "state changed"' -x 'diff -u' -- echo -- left -- right
Cleanup Hooks & Working Directory
Ensure teardown hooks run on exit and optionally preserve temporary files in a specified directory:
# Preserve temp files and clean up resources:
cmdcomp --work-dir ./tmp-workdir -c 'echo "cleanup done"' -- echo -- a -- b
Environment Variables (Common, Left, Right)
Pass environment variables to all commands or exclusively to the left or right side:
# Inject environment variables per side:
cmdcomp -e 'COMMON=1' --left-env 'TARGET=left' --right-env 'TARGET=right' -- bash -c 'echo "$COMMON:$TARGET"'
Replicating Standard Input (--stdin)
Replicate input from standard input ('-') or a file ('@filename') into the stdin of subcommands:
# Pass shared stdin to subcommands:
# echo -e "alpha\nbeta" | cmdcomp -I - -- grep -- alpha -- beta
# Pass input from file to left command and stdin to right command:
cmdcomp --left-stdin '@data.txt' --right-stdin '-' -- cat
Snapshot Comparison (--snapshot)
Bypass command execution on one or both sides and compare directly against static files or stdin:
# Compare a pre-generated baseline snapshot file against live command output:
# cmdcomp --left-snapshot '@baseline.txt' -p 'sed "s|x|y|"' -- echo -- live
# Compare two static snapshot files through common preprocess filters without commands:
cmdcomp --left-snapshot '@file1.json' --right-snapshot '@file2.json' -p 'sed "s|10|20|"'
Dry Run Mode (--dry-run)
Generate an executable bash script capturing the exact execution pipeline without running any commands:
# Output shell script for inspection or reproduction:
# cmdcomp -n -x 'diff -u' -p 'sed "s|v1|common|"' -- echo -- api-v1 -- echo -- api-v2
cmdcomp -x 'diff -u' -p 'sed "s|v1|common|"' -- echo -- api-v1 -- echo -- api-v2
Exit Code & Success Override (--success)
Exit with 0 even when diffs are detected (useful for CI summary steps without failing build):
# Return exit code 0 on diff:
cmdcomp --success -- echo -- a -- b
Custom Delimiter
Change the argument delimiter from '--' to another token:
# Use '===' as delimiter when subcommands themselves take '--':
cmdcomp --delimiter '===' -- echo === echo -- a === echo -- b
Nested Comparison (Comparing Diffs)
Compare the diff outputs of two inner cmdcomp executions (e.g. comparing the effect of branch B vs branch C against baseline A):
# Compare two diff outputs using --delimiter to avoid nested delimiter collision:
cmdcomp --delimiter '===' -- cmdcomp --success -- echo -- base -- === branch-b === branch-c
Multiple Presets Composition
Compose multiple presets together (e.g. combining 'json' preprocessing with 'u' unified diff):
# Compose 'json' (jq preprocess) and 'u' (diff -u) presets:
cmdcomp -P json -P u -- cat -- left.json -- right.json
Config file
Search Paths
cmdcomp automatically searches for configuration files in the following order:
$XDG_CONFIG_HOME/cmdcomp/config.yml(oros.UserConfigDir()/cmdcomp/config.yml)$HOME/.cmdcomp.yml.cmdcomp.yml(in current working directory) Explicit--config / -Cflag orCMDCOMP_CONFIGenvironment variable overrides the search order.
Format
A configuration file is written in YAML and consists of two top-level sections:
default: Optional base configuration applied automatically to all executions (unless--no-defaultis specified).presets: A dictionary of named configurations that can be selected or composed via--preset / -P(e.g.--preset preset1,preset2).
Each preset (and default) accepts identical fields directly corresponding to CLI flags and options:
| YAML Key | Type | Corresponding Flag | Description |
|---|---|---|---|
diff |
string | -x, --diff |
Diff command to compare outputs (e.g. diff -u, colordiff, dyff) |
shell |
string | --shell |
Shell executable used to run subcommands (default: bash) |
delimiter |
string | --delimiter |
Delimiter separating common, left, and right args (default: --) |
label |
boolean | -l, --label |
Pass --label arguments to diff command |
success |
boolean | --success |
Exit 0 even when diffs are detected (diff status 1) |
dry-run |
boolean | -n, --dry-run |
Generate shell script capturing execution pipeline |
debug |
boolean | --debug |
Enable debug log output |
show-cmd-log |
boolean | --show-cmd-log |
Print subcommands stdout and stderr to logs |
work-dir |
string | --work-dir |
Directory for temporary files (preserves files if set) |
timeout |
duration | --timeout |
Maximum timeout for entire execution (e.g. 30s, 2m) |
process-timeout |
duration | --process-timeout |
Timeout for each subcommand (e.g. 10s, 1m) |
startup |
string list | -s, --startup |
Setup commands run sequentially before execution |
interceptor |
string list | -i, --interceptor |
Commands run between left and right commands |
preprocess |
string list | -p, --preprocess |
Pipe filter commands applied to both outputs (e.g. jq, sed) |
left-preprocess |
string list | -L, --left-preprocess |
Filter commands applied only to left output |
right-preprocess |
string list | -R, --right-preprocess |
Filter commands applied only to right output |
cleanup |
string list | -c, --cleanup |
Teardown commands guaranteed to execute on exit |
env |
string list | -e, --env |
Environment variables for all commands (KEY=VAL) |
left-env |
string list | -E, --left-env |
Environment variables for left command only |
right-env |
string list | -F, --right-env |
Environment variables for right command only |
stdin |
string | -I, --stdin |
Input piped to stdin (- for stdin, @file for file) |
left-stdin |
string | -J, --left-stdin |
Input piped to left command stdin only |
right-stdin |
string | -K, --right-stdin |
Input piped to right command stdin only |
snapshot |
string | -S, --snapshot |
Static snapshot replacing both command outputs |
left-snapshot |
string | -T, --left-snapshot |
Static snapshot replacing left command output |
right-snapshot |
string | -U, --right-snapshot |
Static snapshot replacing right command output |
common-args |
string list | (CLI trailing args) | Preset common args prepended to both commands |
left-args |
string list | (CLI trailing args) | Preset left-specific command args |
right-args |
string list | (CLI trailing args) | Preset right-specific command args |
Full example structure:
default:
diff: diff -u
shell: bash
presets:
example:
show-cmd-log: true
debug: true
startup:
- echo startup
interceptor:
- echo interceptor
preprocess:
- grep common
left-preprocess:
- grep left
right-preprocess:
- grep right
diff: diff
work-dir: workdir
shell: bash
delimiter: --
label: true
env:
- X=1
left-env:
- Y=2
right-env:
- Y=3
cleanup:
- echo cleanup
stdin: "-"
left-stdin: "-"
right-stdin: "@data.txt"
snapshot: "-"
left-snapshot: "@left.txt"
right-snapshot: "@right.txt"
common-args:
- echo
left-args:
- common left
right-args:
- common right
timeout: 1m0s
process-timeout: 10s
success: true
Builtin presets
presets:
dyff:
diff: dyff between --omit-header --set-exit-code
shell: bash
delimiter: --
json:
preprocess:
- jq -S .
diff: diff -u --color
shell: bash
delimiter: --
objdiff:
diff: objdiff -cv
shell: bash
delimiter: --
u:
diff: diff -u
shell: bash
delimiter: --
uc:
diff: diff -u --color
shell: bash
delimiter: --
yaml:
preprocess:
- yq -P 'sort_keys(..)'
diff: diff -u --color
shell: bash
delimiter: --
Usage
# automatically loads config from default search paths (~/.cmdcomp.yml, .cmdcomp.yml, etc.)
# uses 'default' section if present in the config file
cmdcomp -- ...
# use specific config file
cmdcomp --config CONFIG_PATH -- ...
# ignore 'default' section in config file
cmdcomp --no-default -- ...
# use builtin "json" preset
cmdcomp --preset json -- ...
# compose multiple presets with config file and CLI flags
cmdcomp --config CONFIG_PATH --preset example,u -x 'diff -u' -- ...
Examples
default:
diff: diff -u --color
shell: bash
presets:
sentry:
diff: objdiff -cv
common-args: ["helm", "template", "sentry/sentry", "--version", "$VERSION"]
then
# helm template sentry/sentry --version 28.0.3 > leftfile
# helm template sentry/sentry --version 29.5.1 > rightfile
# objdiff -cv leftfile rightfile
cmdcomp --config CONFIG --preset sentry --left-env 'VERSION=28.0.3' --right-env 'VERSION=29.5.1'
Exit Codes
- 0: No diff detected, dryrun, version/help displayed, or diff detected with --success.
- 1: Diff detected (exit code of diff command).
- 2: Process failure (command error, hook error, pipeline error, timeout, config/flag error). Always returns 2 even if --success is specified.
Environment Variables
All flags can be specified via environment variables using the 'CMDCOMP_' prefix (e.g. CMDCOMP_DIFF, CMDCOMP_PRESET, CMDCOMP_SHOW_CMD_LOG). Precedence: Default/Preset < Environment Variables < Command-line Flags
- Command lists (CMDCOMP_STARTUP, CMDCOMP_PREPROCESS, etc.): Separate multiple commands with newline.
- Environment variable pairs (CMDCOMP_ENV, CMDCOMP_LEFT_ENV, CMDCOMP_RIGHT_ENV): Separate entries with comma (,).
Flags
-c, --cleanup stringArray command(s) guaranteed to execute before cmdcomp exits, even on failure. Can be specified multiple times. In env vars, separate commands with newlines
-C, --config string configuration file path (default search order: UserConfigDir/cmdcomp/config.yml, $HOME/.cmdcomp.yml, .cmdcomp.yml)
--debug enable debug log output
--delimiter string delimiter token separating [COMMON_ARGS], [LEFT_ARGS], and [RIGHT_ARGS] (e.g. '===') (default "--")
-x, --diff string diff command invoked as '<diff> LEFT_FILE RIGHT_FILE' (e.g. 'diff -u', 'colordiff', 'dyff', 'objdiff -c') (default "diff")
-n, --dry-run print generated bash script capturing the full execution pipeline without executing commands
-e, --env stringArray environment variables passed to all subcommands along with system environment (KEY=VALUE). Can be specified multiple times or comma-separated
-i, --interceptor stringArray command(s) executed sequentially after left command and before right command (e.g. git checkout). Can be specified multiple times. In env vars, separate commands with newlines
-l, --label pass '--label LEFT_ARG' and '--label RIGHT_ARG' to the diff command (useful for diff/colordiff)
-E, --left-env stringArray environment variables passed only to left command and left preprocess (KEY=VALUE). Can be specified multiple times or comma-separated
-L, --left-preprocess stringArray additional filter pipeline command(s) applied only to left output after common preprocess. Multiple flags form a piped chain. In env vars, separate commands with newlines
-T, --left-snapshot string use input as left command output without executing the left command ('-' for stdin, '@filename' for file)
-J, --left-stdin string pass input to stdin of left command only ('-' for stdin, '@filename' for file)
--mcp start MCP server on stdio
--no-default disable loading the 'default' configuration from the config file
-p, --preprocess stringArray filter pipeline command(s) applied to both left and right outputs before diffing. Reads stdin, writes stdout (e.g. jq, yq, sed). Multiple flags form a piped chain. In env vars, separate commands with newlines
-P, --preset stringArray name of preset configuration to load from config file or built-in presets. Can be specified multiple times or comma-separated
--process-timeout duration maximum timeout for each individual subcommand execution (e.g. '10s', '1m')
-F, --right-env stringArray environment variables passed only to right command and right preprocess (KEY=VALUE). Can be specified multiple times or comma-separated
-R, --right-preprocess stringArray additional filter pipeline command(s) applied only to right output after common preprocess. Multiple flags form a piped chain. In env vars, separate commands with newlines
-U, --right-snapshot string use input as right command output without executing the right command ('-' for stdin, '@filename' for file)
-K, --right-stdin string pass input to stdin of right command only ('-' for stdin, '@filename' for file)
--shell string shell executable used to run subcommands (default "bash")
--show-cmd-log print stdout and stderr of executed subcommands to log output
-S, --snapshot string use input as both left and right command outputs without executing commands ('-' for stdin, '@filename' for file)
-s, --startup stringArray command(s) executed sequentially before running commands (e.g. repo updates). Can be specified multiple times. In env vars, separate commands with newlines
-I, --stdin string pass input to stdin of both left and right commands ('-' for stdin, '@filename' for file)
--success exit 0 when diffs are detected (exit status 1 from diff command). Failures (exit code 2) still return 2
--timeout duration maximum timeout for entire cmdcomp execution (e.g. '30s', '2m')
--version display version and exit
--work-dir string working directory for temporary output files. When specified, temporary files are preserved after execution
Install
go install github.com/berquerant/cmdcomp/cmd/cmdcomp@latest