diff --git a/crates/idx-cli/src/main.rs b/crates/idx-cli/src/main.rs index 7214d17..6c1f349 100644 --- a/crates/idx-cli/src/main.rs +++ b/crates/idx-cli/src/main.rs @@ -115,16 +115,9 @@ fn argument_error(err: clap::Error) -> IdxError { /// `-o`/`--output` as clap reads it (clustered short flags, the last one /// at any subcommand level), then `IDX_OUTPUT`, then auto. fn requested_output_from_args() -> OutputFormat { - use clap::CommandFactory; - // ignore_errors keeps every argument clap could read; args_os, because an - // argument that is not UTF-8 (itself the error being reported) must not - // panic here. - let flag = Cli::command() - .ignore_errors(true) - .try_get_matches_from(std::env::args_os()) - .ok() - .and_then(|matches| matches.get_one::("output").copied()); - let setting = flag + // args_os: an argument that is not UTF-8 (itself the error being + // reported) must not panic here. + let setting = output_flag(std::env::args_os().collect()) .or_else(|| { std::env::var("IDX_OUTPUT") .ok() @@ -134,6 +127,37 @@ fn requested_output_from_args() -> OutputFormat { setting.resolve(config::stdout_is_terminal()) } +/// clap stops at an unknown argument, so an `-o` after it would be missed; +/// drop each unknown argument and parse again. What is left is read with +/// ignore_errors, which keeps the matches made before any other error. +fn output_flag(mut args: Vec) -> Option { + use clap::CommandFactory; + use clap::error::{ContextKind, ContextValue, ErrorKind}; + for _ in 0..args.len() { + let Err(err) = Cli::command().try_get_matches_from(&args) else { + break; + }; + if err.kind() != ErrorKind::UnknownArgument { + break; + } + let Some(ContextValue::String(bad)) = err.get(ContextKind::InvalidArg) else { + break; + }; + let Some(index) = args.iter().skip(1).position(|arg| { + let arg = arg.to_string_lossy(); + arg == bad.as_str() || arg.starts_with(&format!("{bad}=")) + }) else { + break; + }; + args.remove(index + 1); + } + Cli::command() + .ignore_errors(true) + .try_get_matches_from(args) + .ok() + .and_then(|matches| matches.get_one::("output").copied()) +} + fn bootstrap_output_format(cli: &Cli) -> OutputFormat { let setting = cli .output diff --git a/crates/idx-cli/tests/cli.rs b/crates/idx-cli/tests/cli.rs index c3c4ff5..9ab558e 100644 --- a/crates/idx-cli/tests/cli.rs +++ b/crates/idx-cli/tests/cli.rs @@ -2127,6 +2127,19 @@ fn argument_errors_follow_the_output_format_clap_resolves() { ] { run_error_json(test_bin("argument-error-json").args(args), "INVALIDINPUT"); } + // An -o after an unknown argument still counts, over IDX_OUTPUT too. + run_error_json( + test_bin("argument-error-json") + .env("IDX_OUTPUT", "table") + .args(["config", "get", "--bogus", "-o", "json"]), + "INVALIDINPUT", + ); + test_bin("argument-error-table") + .args(["config", "get", "--bogus", "-o", "table"]) + .assert() + .failure() + .stdout("") + .stderr(predicate::str::starts_with("error: ")); for args in [ &["-o", "json", "config", "-o", "table", "get"][..], &["-qotable", "config", "get"][..],