Skip to content

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

@jgreitemann

Description

@jgreitemann

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-completionArea: completion generatorC-bugCategory: bugS-waiting-on-designStatus: Waiting on user-facing design to be resolved before implementing

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions