diff --git a/apps/vectorcraft-cli/src/main.rs b/apps/vectorcraft-cli/src/main.rs index 10a859ac5..9f915f6fc 100644 --- a/apps/vectorcraft-cli/src/main.rs +++ b/apps/vectorcraft-cli/src/main.rs @@ -134,6 +134,29 @@ fn usage() -> String { format!("{USAGE}\nReadable formats: .{}\nWritable formats: .{}\n", OPEN_EXTS.join(", ."), export_extensions().join(", .")) } +/// Does a subcommand's `args` ask for the usage? `--help` or `-h` counts where an option goes, not as +/// the value of one of the `valued` options before it, so `run --cmd -h` keeps `-h` for `--cmd`. The +/// subcommands check this before they parse or read anything else. +fn help_requested(args: &[String], valued: &[&str]) -> bool { + let mut it = args.iter(); + while let Some(a) = it.next() { + match a.as_str() { + "--help" | "-h" => return true, + a if valued.contains(&a) => { + it.next(); + } + _ => {} + } + } + false +} + +/// Prints the usage, for `help`, `--help` and `-h`. +fn help() -> Result<(), String> { + out!("{}", usage()); + Ok(()) +} + fn main() -> ExitCode { // Before any font lookup: the fonts font services load (#579). #[cfg(all(windows, not(target_vendor = "win7")))] @@ -143,6 +166,9 @@ fn main() -> ExitCode { prefs_dir::install_fonts(); let args: Vec = std::env::args().skip(1).collect(); let r = match args.first().map(String::as_str) { + // `commands` and `info` take no options, so `--help` or `-h` anywhere after them prints the + // usage. The other subcommands check for it themselves. + Some("commands" | "info") if help_requested(&args[1..], &[]) => help(), Some("mcp") => mcp(&args[1..]), Some("run") => run(&args[1..]), Some("commands") => commands(), @@ -150,10 +176,7 @@ fn main() -> ExitCode { Some("info") => info(&args[1..]), Some("bench") => bench(&args[1..]), Some("perf") => perf::run(&args[1..]), - Some("-h" | "--help" | "help") | None => { - out!("{}", usage()); - Ok(()) - } + Some("-h" | "--help" | "help") | None => help(), Some("-V" | "--version") => { outln!("vectorcraft-cli {}", env!("CARGO_PKG_VERSION")); Ok(()) @@ -180,6 +203,9 @@ fn with_stores(mut h: Headless) -> Headless { } fn mcp(args: &[String]) -> Result<(), String> { + if help_requested(args, &["--connect", "--automation-read-root", "--automation-write-root"]) { + return help(); + } let mut connect: Option = None; let mut headless = false; let (mut read_roots, mut write_roots): (Vec, Vec) = (vec![], vec![]); @@ -207,7 +233,7 @@ fn mcp(args: &[String]) -> Result<(), String> { let write: Vec<&Path> = write_roots.iter().map(Path::new).collect(); let roots = AutomationRoots::new(&read, &write)?; if roots.is_some() && connect.is_some() { - return Err("--automation-read-root and --automation-write-root confine the headless server; a running app enforces its own roots: start it with `vectorcraft --control --automation-read-root --automation-write-root `" + return Err("--automation-read-root and --automation-write-root confine the headless server; a running app enforces its own roots: start it with `vectorcraft --control --automation-read-root --automation-write-root `" .into()); } let backend: Box = if let Some(roots) = roots { @@ -243,6 +269,10 @@ fn commands() -> Result<(), String> { } fn convert(args: &[String]) -> Result<(), String> { + let valued = ["--scale", "-s", "--artboard", "-a", "--range", "-r", "--decimals", "--ai-version", "--hidden-objects", "--transparent-images"]; + if help_requested(args, &valued) { + return help(); + } let mut files = vec![]; let (mut scale, mut artboard, mut range, mut outline_text, mut replace_lossy, mut editing_data) = (1.0f64, None::, None::, false, false, false); @@ -422,6 +452,10 @@ fn command_step(v: Value) -> Result { } fn run(args: &[String]) -> Result<(), String> { + let valued = ["--in", "-i", "--cmd", "-c", "--params", "-p", "--commands", "--export", "-o", "--scale", "-s"]; + if help_requested(args, &valued) { + return help(); + } let mut input: Option = None; let mut steps: Vec = vec![]; // The step of the `--cmd` just given, which a `--params` sets. @@ -522,6 +556,9 @@ fn run(args: &[String]) -> Result<(), String> { } fn bench(args: &[String]) -> Result<(), String> { + if help_requested(args, &["--size", "--iters"]) { + return help(); + } let mut file = None; let (mut w, mut h, mut iters) = (2880u32, 1800u32, 5u32); let mut it = args.iter(); diff --git a/apps/vectorcraft-cli/src/perf.rs b/apps/vectorcraft-cli/src/perf.rs index d760cfefe..8fe3d23b7 100644 --- a/apps/vectorcraft-cli/src/perf.rs +++ b/apps/vectorcraft-cli/src/perf.rs @@ -341,6 +341,9 @@ fn deep(n: usize) -> Result<(), String> { } pub fn run(args: &[String]) -> Result<(), String> { + if crate::help_requested(args, &["--paths"]) { + return crate::help(); + } let mut n = 50_000usize; let mut want_deep = false; let mut it = args.iter(); diff --git a/apps/vectorcraft-cli/tests/it/cli.rs b/apps/vectorcraft-cli/tests/it/cli.rs index e06444b39..c45c5d867 100644 --- a/apps/vectorcraft-cli/tests/it/cli.rs +++ b/apps/vectorcraft-cli/tests/it/cli.rs @@ -201,6 +201,64 @@ fn run_refuses_malformed_commands_naming_the_step() { assert_eq!(printed, 2); } +/// `--help` and `-h` right after a subcommand print the usage that `vectorcraft-cli help` prints. +#[test] +fn help_after_a_subcommand_prints_the_usage() { + let usage = Command::new(BIN).arg("help").output().unwrap(); + assert!(usage.status.success() && String::from_utf8_lossy(&usage.stdout).contains("USAGE:")); + for sub in ["mcp", "run", "commands", "convert", "info", "bench", "perf"] { + for flag in ["--help", "-h"] { + let out = Command::new(BIN).args([sub, flag]).stdin(Stdio::null()).output().unwrap(); + assert!(out.status.success(), "{sub} {flag}: {}", String::from_utf8_lossy(&out.stderr)); + assert!(out.stdout == usage.stdout, "{sub} {flag}: {}", String::from_utf8_lossy(&out.stdout)); + } + } +} + +/// `--help` and `-h` after other arguments print the usage too, before the subcommand does anything +/// with them. +#[test] +fn help_later_in_the_line_prints_the_usage() { + let usage = Command::new(BIN).arg("help").output().unwrap(); + let mut wrong = vec![]; + for args in [ + &["mcp", "--headless", "--help"][..], + &["mcp", "--headless", "-h"], + &["mcp", "--connect", "127.0.0.1:1", "--help"], + &["run", "--cmd", "file.new", "--help"], + &["run", "--in", "missing.svg", "-h"], + &["run", "--commands", "missing.json", "--help"], + &["run", "--cmd", "file.new", "--params", "not json", "-h"], + &["commands", "x", "-h"], + &["convert", "missing.svg", "out.png", "--help"], + &["convert", "missing.svg", "--scale", "2", "-h"], + &["info", "missing.svg", "--help"], + &["bench", "missing.svg", "-h"], + &["bench", "--size", "10x10", "--help"], + &["bench", "--size", "bad", "-h"], + &["perf", "--paths", "10", "--help"], + &["perf", "--paths", "many", "-h"], + &["convert", "--decimals", "many", "--help"], + ] { + let out = Command::new(BIN).args(args).stdin(Stdio::null()).output().unwrap(); + if !out.status.success() || out.stdout != usage.stdout { + wrong.push(format!("{args:?}: {}{}", String::from_utf8_lossy(&out.stderr), String::from_utf8_lossy(&out.stdout))); + } + } + assert!(wrong.is_empty(), "{}", wrong.join("\n")); +} + +/// Further along the line, `-h` is the value of the option before it. +#[test] +fn help_as_an_option_value_goes_to_the_option() { + let out = Command::new(BIN).args(["run", "--cmd", "-h"]).output().unwrap(); + let err = String::from_utf8_lossy(&out.stderr); + assert!(!out.status.success() && err.contains("vectorcraft-cli: step 0 (-h):") && out.stdout.is_empty(), "{err}"); + let out = Command::new(BIN).args(["convert", "in.svg", "--range", "-h"]).output().unwrap(); + let err = String::from_utf8_lossy(&out.stderr); + assert!(!out.status.success() && err.contains("convert needs IN and OUT") && out.stdout.is_empty(), "{err}"); +} + #[test] fn mcp_headless_over_stdio() { let mut child = Command::new(BIN).args(["mcp", "--headless"]).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::null()).spawn().unwrap(); @@ -319,7 +377,10 @@ fn automation_roots(base: &std::path::Path) { fails(&["mcp", "--automation-read-root", &dir("missing")], "cannot open the automation read root"); fails(&["mcp", "--automation-read-root", &dir("in"), "--automation-read-root", &dir("missing")], "cannot open the automation read root"); fails(&["mcp", "--automation-write-root"], "needs a folder"); - fails(&["mcp", "--connect", "127.0.0.1:1", &format!("--automation-write-root={}", dir("out"))], "a running app enforces its own roots"); + fails( + &["mcp", "--connect", "127.0.0.1:1", &format!("--automation-write-root={}", dir("out"))], + "a running app enforces its own roots: start it with `vectorcraft --control ", + ); let mut child = Command::new(BIN) .args(["mcp", "--automation-read-root", &dir("in"), &format!("--automation-write-root={}", dir("out"))])