diff --git a/src/main.rs b/src/main.rs index 3546ba8..14e584f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,6 +50,7 @@ fn print_usage() { eprintln!(" test [options] [files...] Run .shout test files"); eprintln!(" version Print the version"); eprintln!(" example Print an example .shout file"); + eprintln!(" syntax Print the .shout file format reference"); eprintln!(" help [command] display help for command"); } @@ -89,6 +90,7 @@ fn parse_args() -> Option<(&'static str, TestOpts)> { match args[1].as_str() { "test" => print_test_help(), "example" => print_example_help(), + "syntax" => print_syntax_help(), "version" => print_version_help(), "help" => print_help_help(), other => { @@ -112,6 +114,10 @@ fn parse_args() -> Option<(&'static str, TestOpts)> { print_example(); process::exit(0); } + "syntax" => { + print_syntax(); + process::exit(0); + } "test" => {} 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 + + 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 + + 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 + + Define a macro. If a command matches exactly, 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 Prepend to $PATH (repeatable) + --timeout Per-command timeout (default: 10s) + -t, --filter Only run files matching + -v, --verbose Print each command as it runs + --port-from Auto-assign $PORT starting from (default: 5400) + --parallel Run files in parallel"## + ); +} + fn main() { let (_, opts) = parse_args().unwrap();