Skip to content

Dynamic completion at intermediate position erroneously completes the following argument on bash and zsh #5979

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
2 tasks done
jgreitemann opened this issue Apr 27, 2025 · 1 comment · Fixed by #5985 · May be fixed by #6000
Open
2 tasks done
Labels
A-completion Area: completion generator C-bug Category: bug S-waiting-on-design Status: Waiting on user-facing design to be resolved before implementing

Comments

@jgreitemann
Copy link

Please complete the following tasks

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

  1. Build with cargo build.
  2. 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 | source

Replace ~/.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 --b

Expected 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.)

@jgreitemann jgreitemann added the C-bug Category: bug label Apr 27, 2025
@epage epage added A-completion Area: completion generator S-waiting-on-design Status: Waiting on user-facing design to be resolved before implementing labels Apr 28, 2025
mernen added a commit to mernen/mernen that referenced this issue May 5, 2025
mernen added a commit to mernen/mernen that referenced this issue May 5, 2025
mernen added a commit to mernen/mernen that referenced this issue May 5, 2025
mernen added a commit to mernen/mernen that referenced this issue May 5, 2025
@epage epage closed this as completed in e426f4e May 5, 2025
@jgreitemann
Copy link
Author

Thanks to #5985, bash completions now behave (mostly) the same as fish's. However, the behavior is still broken for zsh in precisely the same way and would need an analogous fix. @epage, do you reckon we should reopen this issue? (GitHub won't let me do that.)

@epage epage reopened this May 7, 2025
mernen added a commit to mernen/mernen that referenced this issue May 11, 2025
@mernen mernen linked a pull request May 11, 2025 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-completion Area: completion generator C-bug Category: bug S-waiting-on-design Status: Waiting on user-facing design to be resolved before implementing
Projects
None yet
2 participants