fix(cli): read an -o that follows an unknown argument

clap stops parsing at an unknown argument, so `idx config get --bogus
-o json` lost the -o. Drop each unknown argument and parse again
before reading the output flag (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:47:36 +00:00
commit 70e923334d
2 changed files with 47 additions and 10 deletions

View file

@ -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::<OutputSetting>("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<std::ffi::OsString>) -> Option<OutputSetting> {
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::<OutputSetting>("output").copied())
}
fn bootstrap_output_format(cli: &Cli) -> OutputFormat {
let setting = cli
.output

View file

@ -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"][..],