diff --git a/clap_complete/src/env/shells.rs b/clap_complete/src/env/shells.rs index a46d5d439ee..bcb93d2d35d 100644 --- a/clap_complete/src/env/shells.rs +++ b/clap_complete/src/env/shells.rs @@ -54,6 +54,15 @@ _clap_complete_NAME() { elif [[ $_CLAP_COMPLETE_SPACE == false ]] && [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then compopt -o nospace fi + if [[ -n ${COMP_WORDBREAKS+x} ]]; then + # If the current word contains a word break character, we need to strip + # the prefix up to that character from each completion + local prefix + prefix="${2%"${2##*[${COMP_WORDBREAKS}]}"}" + if [[ -n "$prefix" ]]; then + COMPREPLY=("${COMPREPLY[@]#"$prefix"}") + fi + fi } if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then complete -o nospace -o bashdefault -o nosort -F _clap_complete_NAME BIN @@ -463,13 +472,40 @@ impl Zsh { #[cfg(test)] mod tests { use super::*; - use snapbox::assert_data_eq; + + #[test] + #[cfg(feature = "unstable-dynamic")] + fn bash_registration_handles_wordbreaks() { + let mut buf = Vec::new(); + let bash = Bash; + bash.write_registration("COMPLETE", "my-app", "my-app", "my-app", &mut buf) + .expect("write_registration failed"); + let script = String::from_utf8(buf).expect("Invalid UTF-8"); + + // The registration script must check for COMP_WORDBREAKS to handle shells + // where `=` and `:` are word-break characters. Without this, completing + // `--flag=value` would produce `--flag=--flag=value` because Bash splits + // at `=` and only replaces the part after it. + assert!( + script.contains("COMP_WORDBREAKS"), + "registration script must reference COMP_WORDBREAKS to handle word-break characters" + ); + + // The script must strip the prefix up to the word-break character from + // each completion entry using the `${COMPREPLY[@]#"$prefix"}` pattern. + // This is the shell-side fix that prevents double-prefixing. + assert!( + script.contains(r#"COMPREPLY=("${COMPREPLY[@]#"$prefix"}")"#), + "registration script must strip word-break prefix from COMPREPLY entries" + ); + } // This test verifies that fish shell path quoting works with or without spaces in the path. #[test] #[cfg(all(unix, feature = "unstable-dynamic"))] #[cfg(feature = "unstable-shell-tests")] fn fish_env_completer_path_quoting_works() { + use snapbox::assert_data_eq; // Returns the dynamic registration line for the fish shell, for example: // complete --keep-order --exclusive --command my-bin --arguments "(COMPLETE=fish /path/to/my-bin ... )" let get_fish_registration = |completer_bin: &str| { diff --git a/clap_complete/tests/snapshots/home/dynamic-env/exhaustive/bash/.bashrc b/clap_complete/tests/snapshots/home/dynamic-env/exhaustive/bash/.bashrc index 7ecc315c6ac..c658f2da87b 100644 --- a/clap_complete/tests/snapshots/home/dynamic-env/exhaustive/bash/.bashrc +++ b/clap_complete/tests/snapshots/home/dynamic-env/exhaustive/bash/.bashrc @@ -27,6 +27,15 @@ _clap_complete_exhaustive() { elif [[ $_CLAP_COMPLETE_SPACE == false ]] && [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then compopt -o nospace fi + if [[ -n ${COMP_WORDBREAKS+x} ]]; then + # If the current word contains a word break character, we need to strip + # the prefix up to that character from each completion + local prefix + prefix="${2%"${2##*[${COMP_WORDBREAKS}]}"}" + if [[ -n "$prefix" ]]; then + COMPREPLY=("${COMPREPLY[@]#"$prefix"}") + fi + fi } if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then complete -o nospace -o bashdefault -o nosort -F _clap_complete_exhaustive exhaustive