shell output tester
Go to file
user.email 0b0a66b6d4 Add syntax subcommand to print .shout file format reference
Gives users a quick built-in reference for the file format, directives,
wildcards, exit codes, and CLI options without needing external docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 11:34:50 -07:00
scripts Add multi-target cross-compilation support 2026-04-02 19:26:32 -07:00
src Add syntax subcommand to print .shout file format reference 2026-04-10 11:34:50 -07:00
tests Support # as comment syntax and \# escaping 2026-04-03 08:03:01 -07:00
vim Support # as comment syntax and \# escaping 2026-04-03 08:03:01 -07:00
web Refactor sections to use consistent box styling 2026-04-03 09:59:08 -07:00
.gitignore omg rust 2026-04-02 15:18:22 -07:00
.npmrc shout it out 2026-03-09 21:13:41 -07:00
Cargo.lock Remove regex dependency, replace with manual parsing 2026-04-02 18:32:34 -07:00
Cargo.toml Remove regex dependency, replace with manual parsing 2026-04-02 18:32:34 -07:00
CLAUDE.md Support # as comment syntax and \# escaping 2026-04-03 08:03:01 -07:00
README.md Support # as comment syntax and \# escaping 2026-04-03 08:03:01 -07:00

shout

shout is kinda like really basic integration testing for your cli.

Write .shout files that look like shell sessions and run shout to test 'em.

Install

bun install -g @because/shout

Features

Everything you could ever ask for:

$ echo hello
hello

$ brew --version
Homebrew 5...

$ ls missing
ls: missing: No such file or directory
[1]

... matches anything — a whole line inline, or any number of lines on its own.

[1] after the expected output matches the exit code.

# at the start of a line is a comment — not executed, no output expected:

# start the server
$ my-server &
# now test it
$ curl localhost:8080
OK

If expected output starts with #, escape it with \#:

$ echo "#hashtag"
\#hashtag

# elsewhere in expected output is literal (not a comment).

Usage

$ shout test
...............
15 passed in 23ms

shout test runs code in a temp directory. -k/--keep keeps it around.

--update will modify .shout files to match reality, without running any tests.

Each line in a .shout file is run sequentially, unless --parallel is passed.

Directives

Directives go at the top of a .shout file, before any commands.

@env

Set environment variables for the test:

@env GREETING=hello
@env TARGET=world

$ echo "$GREETING $TARGET"
hello world

@setup

Prepend commands (and @env directives) from another .shout file:

@setup setup-shared.shout

Setup commands run first and their failures abort the test. Setup files cannot themselves contain @setup — no nesting. If both the setup file and the user file define the same @env, the user file wins.

@teardown

Run a cleanup command after all test commands, regardless of pass/fail:

@teardown rm -f "$SHOUT_PROJECT_DIR/data/test.db"

$ create-db && run-tests
...

Teardown failures produce warnings but don't affect test results. You can also put @teardown in setup files:

# setup.shout
export DB_URL=sqlite:data/test.db
@teardown rm -f "$SHOUT_PROJECT_DIR/data/test.db"

@def

Define a macro that substitutes a command by name:

@def greet echo "hello world"

$ greet
hello world

Use backslash \ for multi-line bodies. The body can start on the same line or on the next continuation line:

@def serve \
  python3 -m http.server $PORT & \
  sleep 0.5

$ serve

Macros defined in setup files are inherited. A user file @def with the same name overrides the setup version.

Usage: shout test [options] [files...]

Run .shout test files

Arguments:
  files            Files or directories to test

Options:
  -u, --update     Rewrite expected output in-place with actual output
  -k, --keep       Keep temp directories after run
  --clean-env      Start with empty environment
  --path <path>    Prepend <path> to PATH (repeatable)
  --timeout <dur>  Per-command timeout (default: "10s")
  -t, --filter     Only run files matching <pattern> (substring match)
  -v, --verbose    Print each command as it runs
  --port-from <n>  Auto-assign $PORT starting from n (default: "5400")
  --parallel       Run files in parallel
  -h, --help       display help for command

Environment Variables

Set automatically

Variable Value
HOME Path to the temp directory created for the test
SHOUT_DIR Same as HOME — the temp directory for the test
SHOUT_SOURCE_DIR Directory containing the .shout file being run
SHOUT_PROJECT_DIR The cwd where shout was invoked
PORT Auto-assigned starting from 5400 (or the value of --port-from), increments per file. Not set if PORT is already defined via @env or @setup.

Inherited

By default, the test shell inherits all environment variables from the parent process. Use --clean-env to start with an empty environment instead.

Modified

PATH is prepended with any directories passed via --path <path>.

User-defined

Use @env KEY=VALUE directives to set arbitrary variables. See Directives.

Print an example .shout file:

$ shout example

Editor Support

Neovim

The vim/ directory contains a Neovim plugin with syntax highlighting and commands (:ShoutRun, :ShoutUpdate, :ShoutRunAll).

lazy.nvim

{
  dir = "~/path/to/shout/vim",
  ft = "shout",
}

Manual

mkdir -p ~/.local/share/nvim/site/pack/shout/start
ln -s /path/to/shout/vim ~/.local/share/nvim/site/pack/shout/start/shout