-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Description
Please complete the following tasks
- I have searched the discussions
- I have searched the open and rejected issues
Rust Version
rustc 1.86.0 (05f9846f8 2025-03-31)
Clap Version
clap 4.5.37 / clap_complete 4.5.47
Minimal reproducible code
use clap::{Command, arg};
use clap_complete::CompleteEnv;
fn main() {
if std::env::var("COMPLETE").is_ok() {
CompleteEnv::with_factory(|| {
Command::new("clap-complete-minimal")
.arg(arg!(--foo))
.arg(arg!(--bar))
.arg(arg!(--baz))
})
.complete();
return;
}
println!("Hello, world!");
}Steps to reproduce the bug with the above code
- Build with
cargo build. - Depending on the shell you're testing on, source the completion glue script using one of
$ source <(COMPLETE=bash ~/.cargo/target/debug/clap-complete-minimal)
$ source <(COMPLETE=zsh ~/.cargo/target/debug/clap-complete-minimal)
$ COMPLETE=fish ~/.cargo/target/debug/clap-complete-minimal | sourceReplace ~/.cargo/target/debug/clap-complete-minimal with the path to the executable.
3. Type the following line in your shell, then move the caret to the designated position and hit tab to trigger completion:
$ ~/.cargo/target/debug/clap-complete-minimal --b
# ^Actual Behaviour
On fish, only the -- common to all flags is completed and the user is presented with a choice between all four flags (including --help):
$ clap-complete-minimal -- --b
--foo --bar --baz --help (Print help)On bash and zsh, --ba is inserted. This is the same result that one would get when completing --b, i.e. only --bar and --baz are eligible and --ba is completed as their common prefix. However, here the completion does not replace the existing --b token; it is inserted as a new token at the caret position:
$ ~/.cargo/target/debug/clap-complete-minimal --ba --bExpected Behaviour
The fish behavior seems pretty reasonable and I would expect bash/zsh to behave the same way. In any case, it seems desirable for all shells to be as close as possible in completion behavior.
Additional Context
The bash glue script only forwards COMP_WORDS and COMP_CWORD to the completer. COMP_WORDS does not include an empty "word" when completions are triggered and intermediate positions, so from this information alone, it is not possible to distinguish a completion at an intermediate position from one of the subsequent argument. The same apparently holds for zsh. Relevant design discussion: #5512
This problem should affect pretty much any CLI using dynamic completions. I encountered it when working on tests for Jujutsu's completions. See also the discussion on jj-vcs/jj#6407 (comment).
Debug Output
Bash:
[clap_builder::builder::command]Command::_build: name="clap-complete-minimal"
[clap_builder::builder::command]Command::_propagate:clap-complete-minimal
[clap_builder::builder::command]Command::_check_help_and_version:clap-complete-minimal expand_help_tree=true
[clap_builder::builder::command]Command::long_help_exists
[clap_builder::builder::command]Command::_check_help_and_version: Building default --help
[clap_builder::builder::command]Command::_propagate_global_args:clap-complete-minimal
[clap_builder::builder::debug_asserts]Command::_debug_asserts
[clap_builder::builder::debug_asserts]Arg::_debug_asserts:foo
[clap_builder::builder::debug_asserts]Arg::_debug_asserts:bar
[clap_builder::builder::debug_asserts]Arg::_debug_asserts:baz
[clap_builder::builder::debug_asserts]Arg::_debug_asserts:help
[clap_builder::builder::debug_asserts]Command::_verify_positionals
[clap_builder::builder::command]Command::_build_bin_names
[ clap_builder::output::usage]Usage::get_required_usage_from: incls=[], matcher=false, incl_last=true
[ clap_builder::output::usage]Usage::get_required_usage_from: unrolled_reqs=[]
[ clap_builder::output::usage]Usage::get_required_usage_from: ret_val=[]
[clap_complete::engine::complete] complete: args=["~/.cargo/target/debug/clap-complete-minimal", "--b"], arg_index=1, current_dir=Some("/Users/jgreitemann/git/clap-complete-minimal")
[clap_builder::builder::command]Command::_build: name="clap-complete-minimal"
[clap_builder::builder::command]Command::_build: already built
[clap_builder::builder::command]Command::_build_bin_names
[clap_builder::builder::command]Command::_build_bin_names: already built
[clap_complete::engine::complete] complete: target_cursor=ArgCursor { cursor: 2 }
[clap_complete::engine::complete] complete::next: arg="--b", current_state=ValueDone, cursor=ArgCursor { cursor: 2 }
[clap_complete::engine::complete] complete_arg: arg=ParsedArg { inner: "--b" }, cmd="clap-complete-minimal", current_dir=Some("/Users/jgreitemann/git/clap-complete-minimal"), pos_index=1, state=ValueDone
[clap_complete::engine::complete] complete_subcommand: cmd="clap-complete-minimal", value="--b"
[clap_complete::engine::complete] subcommands: name=clap-complete-minimal
[clap_complete::engine::complete] subcommands: Has subcommands...false
[clap_complete::engine::complete] longs: name=clap-complete-minimal
[clap_complete::engine::complete] longs: name=clap-complete-minimal
Fish:
[clap_builder::builder::command]Command::_build: name="clap-complete-minimal"
[clap_builder::builder::command]Command::_propagate:clap-complete-minimal
[clap_builder::builder::command]Command::_check_help_and_version:clap-complete-minimal expand_help_tree=true
[clap_builder::builder::command]Command::long_help_exists
[clap_builder::builder::command]Command::_check_help_and_version: Building default --help
[clap_builder::builder::command]Command::_propagate_global_args:clap-complete-minimal
[clap_builder::builder::debug_asserts]Command::_debug_asserts
[clap_builder::builder::debug_asserts]Arg::_debug_asserts:foo
[clap_builder::builder::debug_asserts]Arg::_debug_asserts:bar
[clap_builder::builder::debug_asserts]Arg::_debug_asserts:baz
[clap_builder::builder::debug_asserts]Arg::_debug_asserts:help
[clap_builder::builder::debug_asserts]Command::_verify_positionals
[clap_builder::builder::command]Command::_build_bin_names
[ clap_builder::output::usage]Usage::get_required_usage_from: incls=[], matcher=false, incl_last=true
[ clap_builder::output::usage]Usage::get_required_usage_from: unrolled_reqs=[]
[ clap_builder::output::usage]Usage::get_required_usage_from: ret_val=[]
[clap_complete::engine::complete] complete: args=["clap-complete-minimal", ""], arg_index=1, current_dir=Some("/Users/jgreitemann/git/clap-complete-minimal")
[clap_builder::builder::command]Command::_build: name="clap-complete-minimal"
[clap_builder::builder::command]Command::_build: already built
[clap_builder::builder::command]Command::_build_bin_names
[clap_builder::builder::command]Command::_build_bin_names: already built
[clap_complete::engine::complete] complete: target_cursor=ArgCursor { cursor: 2 }
[clap_complete::engine::complete] complete::next: arg="", current_state=ValueDone, cursor=ArgCursor { cursor: 2 }
[clap_complete::engine::complete] complete_arg: arg=ParsedArg { inner: "" }, cmd="clap-complete-minimal", current_dir=Some("/Users/jgreitemann/git/clap-complete-minimal"), pos_index=1, state=ValueDone
[clap_complete::engine::complete] complete_subcommand: cmd="clap-complete-minimal", value=""
[clap_complete::engine::complete] subcommands: name=clap-complete-minimal
[clap_complete::engine::complete] subcommands: Has subcommands...false
[clap_complete::engine::complete] longs: name=clap-complete-minimal
[clap_complete::engine::complete] longs: name=clap-complete-minimal
[clap_complete::engine::complete] shorts: name=clap-complete-minimal
(Zsh swallows stderr when triggering completions.)