Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion cli/src/cli/complete_word.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,19 @@ impl CompleteWord {
ctx.insert("PREV", &(cword - 1));
}

let parsed = usage::parse::parse_partial(spec, &words)?;
// The words before the cursor may not parse (e.g. an unexpected word
// earlier on the line). A completion helper must stay silent rather than
// surface a hard parse error to stderr, which garbles the shell prompt
// mid-completion — emit no candidates instead. (#596)
let parsed = match usage::parse::parse_partial(spec, &words) {
Ok(parsed) => parsed,
Err(e) => {
debug!(
"parse_partial failed during completion (words: {words:?}), no candidates: {e}"
);
return Ok(vec![]);
}
};
debug!("parsed cmd: {}", parsed.cmd.full_cmd.join(" "));

// Check if previous token was a restart_token - if so, complete from first arg
Expand Down
24 changes: 21 additions & 3 deletions cli/tests/complete_word.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ fn complete_word_completer() {
.stdout("plugin-1\tdesc\nplugin-2\tdesc\nplugin-3\tdesc\n");
}

#[test]
fn complete_word_after_unexpected_word_is_silent() {
// #596: completion after an unexpected word must NOT print a hard parse error
// to stderr (which garbles the shell prompt) — the helper should exit 0 with
// no candidates instead.
let spec = "name \"mycli\"\ncmd \"build\"\ncmd \"test\"\n";
Command::new(cargo::cargo_bin!("usage"))
.args([
"cw", "--shell", "bash", "--spec", spec, "--", "mycli", "help", "",
])
.assert()
.success()
.stdout("");
}

#[test]
fn complete_word_subcommands() {
assert_cmd("basic.usage.kdl", &["plugins", "install"]).stdout(contains("install"));
Expand Down Expand Up @@ -258,11 +273,14 @@ fn complete_word_non_global_flags_stop_search() {
path.insert(0, env::current_dir().unwrap().join("..").join("examples"));
env::set_var("PATH", env::join_paths(path).unwrap());

// Non-global flag --local should stop subcommand search
// The parser will fail to recognize 'run' as a subcommand and error
// Non-global flag --local stops the subcommand search, so 'run' is not
// recognized as a subcommand. Completion must still exit cleanly with no
// candidates — NOT surface a hard parse error to stderr, which would garble
// the shell prompt (#596). (Previously this asserted .failure() with
// "unexpected word", which was the bug.)
let mut cmd = cmd("test-boolean-flags.sh", Some("fish"));
cmd.args(["--", "--local", "run", ""]);
cmd.assert().failure().stderr(contains("unexpected word"));
cmd.assert().success().stdout("");
}

#[test]
Expand Down