fix(cli): pick the argument-error format the way clap picks -o

The raw scan took the first -o and missed clustered short flags, so
`idx -o table config -o json get` and `idx -qojson config get` printed
plain-text errors. Resolve -o with clap itself, ignoring the error being
reported (Pullfrog review on #56).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
hermes 2026-10-17 12:23:47 +00:00
commit 0f2806f9f5
2 changed files with 34 additions and 19 deletions

View file

@ -112,27 +112,19 @@ fn argument_error(err: clap::Error) -> IdxError {
} }
/// The output format the raw arguments ask for, when they did not parse: /// The output format the raw arguments ask for, when they did not parse:
/// `-o`/`--output` (any spelling), then `IDX_OUTPUT`, then auto. /// `-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 { fn requested_output_from_args() -> OutputFormat {
// args_os: an argument that is not UTF-8 (itself the error being use clap::CommandFactory;
// reported) must not panic here. // ignore_errors keeps every argument clap could read; args_os, because an
let args: Vec<String> = std::env::args_os() // argument that is not UTF-8 (itself the error being reported) must not
.skip(1) // panic here.
.map(|arg| arg.to_string_lossy().into_owned()) let flag = Cli::command()
.collect(); .ignore_errors(true)
let flag = args.iter().enumerate().find_map(|(i, arg)| { .try_get_matches_from(std::env::args_os())
if arg == "-o" || arg == "--output" { .ok()
args.get(i + 1).cloned() .and_then(|matches| matches.get_one::<OutputSetting>("output").copied());
} else if let Some(value) = arg.strip_prefix("--output=") {
Some(value.to_string())
} else if arg.starts_with("-o") && arg.len() > 2 {
Some(arg[2..].trim_start_matches('=').to_string())
} else {
None
}
});
let setting = flag let setting = flag
.and_then(|value| OutputSetting::parse(&value))
.or_else(|| { .or_else(|| {
std::env::var("IDX_OUTPUT") std::env::var("IDX_OUTPUT")
.ok() .ok()

View file

@ -2117,6 +2117,29 @@ fn offline_and_no_cache_flags_are_rejected() {
); );
} }
#[test]
fn argument_errors_follow_the_output_format_clap_resolves() {
// The last -o wins at any subcommand level, and -o may sit in a cluster:
// argument errors pick the same format a successful command would.
for args in [
&["-o", "table", "config", "-o", "json", "get"][..],
&["-qojson", "config", "get"][..],
] {
run_error_json(test_bin("argument-error-json").args(args), "INVALIDINPUT");
}
for args in [
&["-o", "json", "config", "-o", "table", "get"][..],
&["-qotable", "config", "get"][..],
] {
test_bin("argument-error-table")
.args(args)
.assert()
.failure()
.stdout("")
.stderr(predicate::str::starts_with("error: "));
}
}
#[test] #[test]
fn msn_screen_rejects_invalid_filter() { fn msn_screen_rejects_invalid_filter() {
test_bin("msn-screen-invalid-filter") test_bin("msn-screen-invalid-filter")