diff --git a/apps/designcraft-cli/src/main.rs b/apps/designcraft-cli/src/main.rs index a564befc2..11514f709 100644 --- a/apps/designcraft-cli/src/main.rs +++ b/apps/designcraft-cli/src/main.rs @@ -34,6 +34,19 @@ mod perf; use designcraft_engine::Session; use serde_json::{Value, json}; +/// The usage block. Shared by `--help` (stdout, success) and an unknown command (stderr, failure). +const USAGE: &str = "usage: designcraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--page N] [--scale S] [--pdf-options JSON] [--export OUT] [--all-pages DIR]\n (--page, --scale and --pdf-options apply to the exports that follow them)\n designcraft-cli commands [FILTER]\n designcraft-cli describe COMMAND\n designcraft-cli script [FILE|-] [--in FILE | --sample] [--connect PORT] [--save OUT] [--export OUT] [--keep-going]\n designcraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON]\n designcraft-cli mcp [--connect PORT] [--sample]\n designcraft-cli perf [--pages N] [--runs N] [--strict]\n designcraft-cli bench FILE [--runs N]\n designcraft-cli links\n designcraft-cli --version"; + +/// The links line under the usage block. +fn usage_footer() -> String { + format!( + "\nCommunity: {} · {} · {}", + designcraft_engine::links::DISCORD, + designcraft_engine::links::APP_PAGE, + designcraft_engine::links::GITHUB + ) +} + /// stdout went away. A reader that stopped early (a closed pipe) ends the program quietly, as /// ripgrep does; any other write error is reported. fn stdout_failed(e: std::io::Error) -> ! { @@ -76,16 +89,16 @@ fn main() -> ExitCode { outln!("Discord {DISCORD}\nWebsite {WEBSITE}\nApp page {APP_PAGE}\nGitHub {GITHUB}\nIssues {ISSUES}"); ExitCode::SUCCESS } + // Asking for help is not an error: it goes to stdout and succeeds. An unknown command + // still prints the same usage to stderr and fails. + Some("--help" | "-h" | "help") => { + outln!("{USAGE}"); + outln!("{}", usage_footer()); + ExitCode::SUCCESS + } _ => { - eprintln!( - "usage: designcraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--page N] [--scale S] [--pdf-options JSON] [--export OUT] [--all-pages DIR]\n (--page, --scale and --pdf-options apply to the exports that follow them)\n designcraft-cli commands [FILTER]\n designcraft-cli describe COMMAND\n designcraft-cli script [FILE|-] [--in FILE | --sample] [--connect PORT] [--save OUT] [--export OUT] [--keep-going]\n designcraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON]\n designcraft-cli mcp [--connect PORT] [--sample]\n designcraft-cli perf [--pages N] [--runs N] [--strict]\n designcraft-cli bench FILE [--runs N]\n designcraft-cli links\n designcraft-cli --version" - ); - eprintln!( - "\nCommunity: {} · {} · {}", - designcraft_engine::links::DISCORD, - designcraft_engine::links::APP_PAGE, - designcraft_engine::links::GITHUB - ); + eprintln!("{USAGE}"); + eprintln!("{}", usage_footer()); ExitCode::FAILURE } } diff --git a/apps/designcraft-cli/tests/cli.rs b/apps/designcraft-cli/tests/cli.rs index 8c2b0f14b..1a06d13b0 100644 --- a/apps/designcraft-cli/tests/cli.rs +++ b/apps/designcraft-cli/tests/cli.rs @@ -67,6 +67,26 @@ fn run_cmd_references_and_describe() { assert!(v.as_array().unwrap().iter().all(|c| c.to_string().contains("footnote"))); } +/// `designcraft-cli --help` printed usage to stderr and exited 1; asking for help is not an +/// error (#243). It now goes to stdout and succeeds, while an unknown command keeps the same +/// usage on stderr and fails. +#[test] +fn help_goes_to_stdout_and_unknown_args_to_stderr() { + for flag in ["--help", "-h", "help"] { + let out = cli().arg(flag).output().unwrap(); + let stdout = String::from_utf8_lossy(&out.stdout); + assert!(out.status.success(), "{flag}: {:?}", out.status); + assert!(stdout.starts_with("usage: designcraft-cli"), "{flag}: {stdout:?}"); + assert!(stdout.contains("designcraft-cli --version"), "{flag}: {stdout:?}"); + assert!(out.stderr.is_empty(), "{flag} wrote to stderr: {}", String::from_utf8_lossy(&out.stderr)); + } + // An unknown option is still an error: usage on stderr, nothing on stdout, non-zero exit. + let out = cli().arg("--no-such-option").output().unwrap(); + assert!(!out.status.success(), "{:?}", out.status); + assert!(out.stdout.is_empty(), "unexpected stdout: {}", String::from_utf8_lossy(&out.stdout)); + assert!(String::from_utf8_lossy(&out.stderr).starts_with("usage: designcraft-cli")); +} + /// `designcraft-cli commands | head -1` panicked with "failed printing to /// stdout: Broken pipe (os error 32)" and exit status 101. #[test]