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>
This commit is contained in:
user.email 2026-04-10 11:34:50 -07:00
parent c62c1d3161
commit 0b0a66b6d4

View File

@ -50,6 +50,7 @@ fn print_usage() {
eprintln!(" test [options] [files...] Run .shout test files"); eprintln!(" test [options] [files...] Run .shout test files");
eprintln!(" version Print the version"); eprintln!(" version Print the version");
eprintln!(" example Print an example .shout file"); eprintln!(" example Print an example .shout file");
eprintln!(" syntax Print the .shout file format reference");
eprintln!(" help [command] display help for command"); eprintln!(" help [command] display help for command");
} }
@ -89,6 +90,7 @@ fn parse_args() -> Option<(&'static str, TestOpts)> {
match args[1].as_str() { match args[1].as_str() {
"test" => print_test_help(), "test" => print_test_help(),
"example" => print_example_help(), "example" => print_example_help(),
"syntax" => print_syntax_help(),
"version" => print_version_help(), "version" => print_version_help(),
"help" => print_help_help(), "help" => print_help_help(),
other => { other => {
@ -112,6 +114,10 @@ fn parse_args() -> Option<(&'static str, TestOpts)> {
print_example(); print_example();
process::exit(0); process::exit(0);
} }
"syntax" => {
print_syntax();
process::exit(0);
}
"test" => {} "test" => {}
other => { other => {
eprintln!("Unknown command: {other}"); eprintln!("Unknown command: {other}");
@ -552,6 +558,183 @@ $ true
); );
} }
fn print_syntax_help() {
eprintln!("Usage: shout syntax");
eprintln!();
eprintln!("Print the .shout file format reference");
}
fn print_syntax() {
println!(
r##"SHOUT FILE FORMAT
=================
Each .shout file describes a shell session: commands to run and their
expected output. Each file runs in a fresh temp directory with a single
/bin/sh session. State carries between commands within a file.
COMMANDS AND OUTPUT
-------------------
Lines starting with "$ " are commands. Lines between commands are expected
output (stdout and stderr are merged).
$ echo hello
hello
$ ls missing
ls: missing: No such file or directory
WILDCARDS
---------
"..." matches any characters for the rest of the line (inline wildcard):
$ date
...
$ brew --version
Homebrew ...
"..." on its own line matches zero or more entire lines (multi-line wildcard):
$ echo "one"; echo "two"; echo "three"
one
...
three
EXIT CODES
----------
"[N]" on the last line of expected output asserts exit code N:
$ false
[1]
$ sh -c "exit 42"
[42]
"[*]" asserts any non-zero exit code:
$ false
[*]
"[0]" explicitly asserts exit code 0 (the default when no code is given):
$ true
[0]
COMMENTS
--------
Lines starting with "#" are comments (not executed, no output expected):
# this is a comment
$ echo hello
hello
$ echo hi # comments after commands are also stripped
hi
Use "\#" in expected output to match a literal line starting with "#":
$ echo "# heading"
\# heading
DIRECTIVES
----------
Directives appear before the first command.
@env KEY=VALUE
Set an environment variable for the session.
@env DATABASE_URL=sqlite::memory:
@env DEBUG=1
@setup <path>
Prepend commands (and @env/@teardown/@def) from another file. The setup
file uses a plain format: each line is a command (no "$ " prefix needed),
"#" lines are comments, blank lines are ignored. Setup files cannot
reference other setup files (no nesting).
@setup shared.shout
If a setup command fails (non-zero exit), the test aborts with an error.
User file @env overrides setup file @env for the same key.
User file @def overrides setup file @def for the same name.
@teardown <command>
Run a cleanup command after all test commands, regardless of pass/fail.
Teardown failures produce warnings but don't affect test results.
Can appear in both .shout files and setup files.
@teardown rm -rf "$SHOUT_DIR/tmp"
@teardown docker rm -f test-container
@def <name> <body>
Define a macro. If a command matches <name> exactly, <body> is
substituted before execution. Use "\" at end of line to continue the
body onto the next line. Allowed in both .shout files and setup files.
@def start-server \
PORT=$PORT node server.js &
$ start-server
$ curl localhost:$PORT
OK
ENVIRONMENT VARIABLES
---------------------
These are set automatically before running commands:
HOME temp directory for this test file
SHOUT_DIR same temp directory
SHOUT_SOURCE_DIR directory containing the .shout file
SHOUT_PROJECT_DIR directory where shout was invoked
PORT auto-assigned (from 5400 or --port-from), increments per file
PATH prepended with --path dirs, if any
SETUP FILE FORMAT
-----------------
Setup files (referenced by @setup) use a simpler format:
- Each line is a shell command (no "$ " prefix)
- "#" lines are comments, blank lines are ignored
- @env, @teardown, and @def directives are supported
- @setup is not allowed (no nesting)
Example setup file:
# setup.shout
@env DB_URL=sqlite:test.db
@teardown rm -f "$SHOUT_PROJECT_DIR/test.db"
npm install --silent
CLI OPTIONS
-----------
shout test [options] [files...]
-u, --update Rewrite .shout files with actual output
-k, --keep Keep temp directories after run
--clean-env Start with empty environment
--path <path> Prepend to $PATH (repeatable)
--timeout <dur> Per-command timeout (default: 10s)
-t, --filter <pattern> Only run files matching <pattern>
-v, --verbose Print each command as it runs
--port-from <n> Auto-assign $PORT starting from <n> (default: 5400)
--parallel Run files in parallel"##
);
}
fn main() { fn main() {
let (_, opts) = parse_args().unwrap(); let (_, opts) = parse_args().unwrap();