CLI: --help after a subcommand prints the usage

`vectorcraft-cli mcp --help` stopped with "unknown mcp option `--help`",
and `run`, `convert`, `info`, `bench` and `perf` treated `--help` and
`-h` as an unknown option or a file name, while `vectorcraft-cli help`
printed the usage. `--help` or `-h` after a subcommand now prints the
same usage and exits successfully, before the subcommand parses a value
or reads a file, so `run --commands missing.json --help` prints it too.
As the value of an option it stays that option's value: `run --cmd -h`
and `convert IN --range -h` still read `-h` as before.

The error for `--connect` with an automation root also had a run of 21
spaces in the middle of its text; it now has one.
This commit is contained in:
dotCooCoo
2026-10-24 05:23:58 -07:00
parent 21a648d850
commit d9891d9b2c
3 changed files with 107 additions and 6 deletions
+42 -5
View File
@@ -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<String> = 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<String> = None;
let mut headless = false;
let (mut read_roots, mut write_roots): (Vec<String>, Vec<String>) = (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 <port> --automation-read-root <dir> --automation-write-root <dir>`"
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 <port> --automation-read-root <dir> --automation-write-root <dir>`"
.into());
}
let backend: Box<dyn Backend> = 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::<u64>, None::<String>, false, false, false);
@@ -422,6 +452,10 @@ fn command_step(v: Value) -> Result<Step, String> {
}
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<String> = None;
let mut steps: Vec<Step> = 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();
+3
View File
@@ -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();
+62 -1
View File
@@ -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 <port>",
);
let mut child = Command::new(BIN)
.args(["mcp", "--automation-read-root", &dir("in"), &format!("--automation-write-root={}", dir("out"))])