From dce16c37b9ddd549aa7de197810252d91bd3805a Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 18 Feb 2026 21:59:57 +0200 Subject: [PATCH 1/2] - Fix completion escaping --- lib/completely/pattern.rb | 10 +++++++- lib/completely/templates/template.erb | 16 +++++++++---- spec/approvals/cli/generated-script | 20 ++++++++++------ spec/approvals/cli/generated-script-alt | 20 ++++++++++------ spec/approvals/cli/generated-wrapped-script | 20 ++++++++++------ .../approvals/cli/test/completely-tester-1.sh | 20 ++++++++++------ .../approvals/cli/test/completely-tester-2.sh | 20 ++++++++++------ spec/approvals/cli/test/completely-tester.sh | 20 ++++++++++------ spec/approvals/completions/function | 20 ++++++++++------ spec/approvals/completions/script | 20 ++++++++++------ .../completions/script-complete-options | 18 +++++++++----- spec/approvals/completions/script-only-spaces | 18 +++++++++----- spec/approvals/completions/script-with-debug | 16 +++++++++---- spec/completely/pattern_spec.rb | 12 ++++++++-- spec/fixtures/tester/default.bash | 24 ++++++++++++------- 15 files changed, 184 insertions(+), 90 deletions(-) diff --git a/lib/completely/pattern.rb b/lib/completely/pattern.rb index a276a29..1c6f6b7 100644 --- a/lib/completely/pattern.rb +++ b/lib/completely/pattern.rb @@ -54,8 +54,16 @@ def compgen def compgen! result = [] result << actions.join(' ').to_s if actions.any? - result << %[-W "$(#{function_name} "#{words.join ' '}")"] if words.any? + result << %[-W "$(#{function_name} #{quoted_words.join ' '})"] if words.any? result.any? ? result.join(' ') : nil end + + def quoted_words + @quoted_words ||= words.map { |word| %("#{escape_for_double_quotes word}") } + end + + def escape_for_double_quotes(word) + word.gsub(/["\\]/, '\\\\\&') + end end end diff --git a/lib/completely/templates/template.erb b/lib/completely/templates/template.erb index b5dfbe4..6a587b2 100644 --- a/lib/completely/templates/template.erb +++ b/lib/completely/templates/template.erb @@ -5,7 +5,7 @@ # Modifying it manually is not recommended <%= function_name %>_filter() { - local words="$1" + local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() @@ -17,12 +17,12 @@ if [[ "${cur:0:1}" == "-" ]]; then # Completing an option: offer everything (including options) - echo "$words" + result=("${words[@]}") else # Completing a non-option: offer only non-options, # and don't re-offer ones already used earlier in the line. - for word in $words; do + for word in "${words[@]}"; do [[ "${word:0:1}" == "-" ]] && continue local seen=0 @@ -34,9 +34,15 @@ done ((!seen)) && result+=("$word") done - - echo "${result[*]}" fi + + local escaped=() + for word in "${result[@]}"; do + printf -v word '%q' "$word" + escaped+=("$word") + done + + echo "${escaped[*]}" } <%= function_name %>() { diff --git a/spec/approvals/cli/generated-script b/spec/approvals/cli/generated-script index 94e256e..73d1d11 100644 --- a/spec/approvals/cli/generated-script +++ b/spec/approvals/cli/generated-script @@ -5,7 +5,7 @@ # Modifying it manually is not recommended _mygit_completions_filter() { - local words="$1" + local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() @@ -17,12 +17,12 @@ _mygit_completions_filter() { if [[ "${cur:0:1}" == "-" ]]; then # Completing an option: offer everything (including options) - echo "$words" + result=("${words[@]}") else # Completing a non-option: offer only non-options, # and don't re-offer ones already used earlier in the line. - for word in $words; do + for word in "${words[@]}"; do [[ "${word:0:1}" == "-" ]] && continue local seen=0 @@ -34,9 +34,15 @@ _mygit_completions_filter() { done ((!seen)) && result+=("$word") done - - echo "${result[*]}" fi + + local escaped=() + for word in "${result[@]}"; do + printf -v word '%q' "$word" + escaped+=("$word") + done + + echo "${escaped[*]}" } _mygit_completions() { @@ -59,7 +65,7 @@ _mygit_completions() { ;; 'status'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "--help --verbose --branch -b")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "--help" "--verbose" "--branch" "-b")" -- "$cur") ;; 'init'*) @@ -67,7 +73,7 @@ _mygit_completions() { ;; *) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "-h -v --help --version init status")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "-h" "-v" "--help" "--version" "init" "status")" -- "$cur") ;; esac diff --git a/spec/approvals/cli/generated-script-alt b/spec/approvals/cli/generated-script-alt index 217aae4..c3840ce 100644 --- a/spec/approvals/cli/generated-script-alt +++ b/spec/approvals/cli/generated-script-alt @@ -5,7 +5,7 @@ # Modifying it manually is not recommended _mycomps_filter() { - local words="$1" + local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() @@ -17,12 +17,12 @@ _mycomps_filter() { if [[ "${cur:0:1}" == "-" ]]; then # Completing an option: offer everything (including options) - echo "$words" + result=("${words[@]}") else # Completing a non-option: offer only non-options, # and don't re-offer ones already used earlier in the line. - for word in $words; do + for word in "${words[@]}"; do [[ "${word:0:1}" == "-" ]] && continue local seen=0 @@ -34,9 +34,15 @@ _mycomps_filter() { done ((!seen)) && result+=("$word") done - - echo "${result[*]}" fi + + local escaped=() + for word in "${result[@]}"; do + printf -v word '%q' "$word" + escaped+=("$word") + done + + echo "${escaped[*]}" } _mycomps() { @@ -59,7 +65,7 @@ _mycomps() { ;; 'status'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter "--help --verbose --branch -b")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter "--help" "--verbose" "--branch" "-b")" -- "$cur") ;; 'init'*) @@ -67,7 +73,7 @@ _mycomps() { ;; *) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter "-h -v --help --version init status")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter "-h" "-v" "--help" "--version" "init" "status")" -- "$cur") ;; esac diff --git a/spec/approvals/cli/generated-wrapped-script b/spec/approvals/cli/generated-wrapped-script index e37ade0..50bfcb9 100644 --- a/spec/approvals/cli/generated-wrapped-script +++ b/spec/approvals/cli/generated-wrapped-script @@ -6,7 +6,7 @@ give_comps() { echo $'# Modifying it manually is not recommended' echo $'' echo $'_mygit_completions_filter() {' - echo $' local words="$1"' + echo $' local words=("$@")' echo $' local cur=${COMP_WORDS[COMP_CWORD]}' echo $' local result=()' echo $'' @@ -18,12 +18,12 @@ give_comps() { echo $'' echo $' if [[ "${cur:0:1}" == "-" ]]; then' echo $' # Completing an option: offer everything (including options)' - echo $' echo "$words"' + echo $' result=("${words[@]}")' echo $'' echo $' else' echo $' # Completing a non-option: offer only non-options,' echo $' # and don\'t re-offer ones already used earlier in the line.' - echo $' for word in $words; do' + echo $' for word in "${words[@]}"; do' echo $' [[ "${word:0:1}" == "-" ]] && continue' echo $'' echo $' local seen=0' @@ -35,9 +35,15 @@ give_comps() { echo $' done' echo $' ((!seen)) && result+=("$word")' echo $' done' - echo $'' - echo $' echo "${result[*]}"' echo $' fi' + echo $'' + echo $' local escaped=()' + echo $' for word in "${result[@]}"; do' + echo $' printf -v word \'%q\' "$word"' + echo $' escaped+=("$word")' + echo $' done' + echo $'' + echo $' echo "${escaped[*]}"' echo $'}' echo $'' echo $'_mygit_completions() {' @@ -60,7 +66,7 @@ give_comps() { echo $' ;;' echo $'' echo $' \'status\'*)' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "--help --verbose --branch -b")" -- "$cur")' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "--help" "--verbose" "--branch" "-b")" -- "$cur")' echo $' ;;' echo $'' echo $' \'init\'*)' @@ -68,7 +74,7 @@ give_comps() { echo $' ;;' echo $'' echo $' *)' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "-h -v --help --version init status")" -- "$cur")' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "-h" "-v" "--help" "--version" "init" "status")" -- "$cur")' echo $' ;;' echo $'' echo $' esac' diff --git a/spec/approvals/cli/test/completely-tester-1.sh b/spec/approvals/cli/test/completely-tester-1.sh index 50d5b48..8d68e40 100644 --- a/spec/approvals/cli/test/completely-tester-1.sh +++ b/spec/approvals/cli/test/completely-tester-1.sh @@ -13,7 +13,7 @@ fi # Modifying it manually is not recommended _mygit_completions_filter() { - local words="$1" + local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() @@ -25,12 +25,12 @@ _mygit_completions_filter() { if [[ "${cur:0:1}" == "-" ]]; then # Completing an option: offer everything (including options) - echo "$words" + result=("${words[@]}") else # Completing a non-option: offer only non-options, # and don't re-offer ones already used earlier in the line. - for word in $words; do + for word in "${words[@]}"; do [[ "${word:0:1}" == "-" ]] && continue local seen=0 @@ -42,9 +42,15 @@ _mygit_completions_filter() { done ((!seen)) && result+=("$word") done - - echo "${result[*]}" fi + + local escaped=() + for word in "${result[@]}"; do + printf -v word '%q' "$word" + escaped+=("$word") + done + + echo "${escaped[*]}" } _mygit_completions() { @@ -67,7 +73,7 @@ _mygit_completions() { ;; 'status'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "--help --verbose --branch -b")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "--help" "--verbose" "--branch" "-b")" -- "$cur") ;; 'init'*) @@ -75,7 +81,7 @@ _mygit_completions() { ;; *) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "-h -v --help --version init status")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "-h" "-v" "--help" "--version" "init" "status")" -- "$cur") ;; esac diff --git a/spec/approvals/cli/test/completely-tester-2.sh b/spec/approvals/cli/test/completely-tester-2.sh index a0fb1e3..39bea97 100644 --- a/spec/approvals/cli/test/completely-tester-2.sh +++ b/spec/approvals/cli/test/completely-tester-2.sh @@ -13,7 +13,7 @@ fi # Modifying it manually is not recommended _mygit_completions_filter() { - local words="$1" + local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() @@ -25,12 +25,12 @@ _mygit_completions_filter() { if [[ "${cur:0:1}" == "-" ]]; then # Completing an option: offer everything (including options) - echo "$words" + result=("${words[@]}") else # Completing a non-option: offer only non-options, # and don't re-offer ones already used earlier in the line. - for word in $words; do + for word in "${words[@]}"; do [[ "${word:0:1}" == "-" ]] && continue local seen=0 @@ -42,9 +42,15 @@ _mygit_completions_filter() { done ((!seen)) && result+=("$word") done - - echo "${result[*]}" fi + + local escaped=() + for word in "${result[@]}"; do + printf -v word '%q' "$word" + escaped+=("$word") + done + + echo "${escaped[*]}" } _mygit_completions() { @@ -67,7 +73,7 @@ _mygit_completions() { ;; 'status'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "--help --verbose --branch -b")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "--help" "--verbose" "--branch" "-b")" -- "$cur") ;; 'init'*) @@ -75,7 +81,7 @@ _mygit_completions() { ;; *) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "-h -v --help --version init status")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "-h" "-v" "--help" "--version" "init" "status")" -- "$cur") ;; esac diff --git a/spec/approvals/cli/test/completely-tester.sh b/spec/approvals/cli/test/completely-tester.sh index 108dbf9..233b68a 100644 --- a/spec/approvals/cli/test/completely-tester.sh +++ b/spec/approvals/cli/test/completely-tester.sh @@ -13,7 +13,7 @@ fi # Modifying it manually is not recommended _mygit_completions_filter() { - local words="$1" + local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() @@ -25,12 +25,12 @@ _mygit_completions_filter() { if [[ "${cur:0:1}" == "-" ]]; then # Completing an option: offer everything (including options) - echo "$words" + result=("${words[@]}") else # Completing a non-option: offer only non-options, # and don't re-offer ones already used earlier in the line. - for word in $words; do + for word in "${words[@]}"; do [[ "${word:0:1}" == "-" ]] && continue local seen=0 @@ -42,9 +42,15 @@ _mygit_completions_filter() { done ((!seen)) && result+=("$word") done - - echo "${result[*]}" fi + + local escaped=() + for word in "${result[@]}"; do + printf -v word '%q' "$word" + escaped+=("$word") + done + + echo "${escaped[*]}" } _mygit_completions() { @@ -67,7 +73,7 @@ _mygit_completions() { ;; 'status'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "--help --verbose --branch -b")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "--help" "--verbose" "--branch" "-b")" -- "$cur") ;; 'init'*) @@ -75,7 +81,7 @@ _mygit_completions() { ;; *) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "-h -v --help --version init status")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "-h" "-v" "--help" "--version" "init" "status")" -- "$cur") ;; esac diff --git a/spec/approvals/completions/function b/spec/approvals/completions/function index 1b31b34..efdc9ab 100644 --- a/spec/approvals/completions/function +++ b/spec/approvals/completions/function @@ -6,7 +6,7 @@ send_completions() { echo $'# Modifying it manually is not recommended' echo $'' echo $'_completely_completions_filter() {' - echo $' local words="$1"' + echo $' local words=("$@")' echo $' local cur=${COMP_WORDS[COMP_CWORD]}' echo $' local result=()' echo $'' @@ -18,12 +18,12 @@ send_completions() { echo $'' echo $' if [[ "${cur:0:1}" == "-" ]]; then' echo $' # Completing an option: offer everything (including options)' - echo $' echo "$words"' + echo $' result=("${words[@]}")' echo $'' echo $' else' echo $' # Completing a non-option: offer only non-options,' echo $' # and don\'t re-offer ones already used earlier in the line.' - echo $' for word in $words; do' + echo $' for word in "${words[@]}"; do' echo $' [[ "${word:0:1}" == "-" ]] && continue' echo $'' echo $' local seen=0' @@ -35,9 +35,15 @@ send_completions() { echo $' done' echo $' ((!seen)) && result+=("$word")' echo $' done' - echo $'' - echo $' echo "${result[*]}"' echo $' fi' + echo $'' + echo $' local escaped=()' + echo $' for word in "${result[@]}"; do' + echo $' printf -v word \'%q\' "$word"' + echo $' escaped+=("$word")' + echo $' done' + echo $'' + echo $' echo "${escaped[*]}"' echo $'}' echo $'' echo $'_completely_completions() {' @@ -52,7 +58,7 @@ send_completions() { echo $'' echo $' case "$compline" in' echo $' \'generate\'*)' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_completely_completions_filter "--help --force")" -- "$cur")' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_completely_completions_filter "--help" "--force")" -- "$cur")' echo $' ;;' echo $'' echo $' \'init\'*)' @@ -60,7 +66,7 @@ send_completions() { echo $' ;;' echo $'' echo $' *)' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_completely_completions_filter "--help --version init generate")" -- "$cur")' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_completely_completions_filter "--help" "--version" "init" "generate")" -- "$cur")' echo $' ;;' echo $'' echo $' esac' diff --git a/spec/approvals/completions/script b/spec/approvals/completions/script index cba794b..e15f20f 100644 --- a/spec/approvals/completions/script +++ b/spec/approvals/completions/script @@ -5,7 +5,7 @@ # Modifying it manually is not recommended _completely_completions_filter() { - local words="$1" + local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() @@ -17,12 +17,12 @@ _completely_completions_filter() { if [[ "${cur:0:1}" == "-" ]]; then # Completing an option: offer everything (including options) - echo "$words" + result=("${words[@]}") else # Completing a non-option: offer only non-options, # and don't re-offer ones already used earlier in the line. - for word in $words; do + for word in "${words[@]}"; do [[ "${word:0:1}" == "-" ]] && continue local seen=0 @@ -34,9 +34,15 @@ _completely_completions_filter() { done ((!seen)) && result+=("$word") done - - echo "${result[*]}" fi + + local escaped=() + for word in "${result[@]}"; do + printf -v word '%q' "$word" + escaped+=("$word") + done + + echo "${escaped[*]}" } _completely_completions() { @@ -51,7 +57,7 @@ _completely_completions() { case "$compline" in 'generate'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_completely_completions_filter "--help --force")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_completely_completions_filter "--help" "--force")" -- "$cur") ;; 'init'*) @@ -59,7 +65,7 @@ _completely_completions() { ;; *) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_completely_completions_filter "--help --version init generate")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_completely_completions_filter "--help" "--version" "init" "generate")" -- "$cur") ;; esac diff --git a/spec/approvals/completions/script-complete-options b/spec/approvals/completions/script-complete-options index a0dff27..a3b05f8 100644 --- a/spec/approvals/completions/script-complete-options +++ b/spec/approvals/completions/script-complete-options @@ -5,7 +5,7 @@ # Modifying it manually is not recommended _mygit_completions_filter() { - local words="$1" + local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() @@ -17,12 +17,12 @@ _mygit_completions_filter() { if [[ "${cur:0:1}" == "-" ]]; then # Completing an option: offer everything (including options) - echo "$words" + result=("${words[@]}") else # Completing a non-option: offer only non-options, # and don't re-offer ones already used earlier in the line. - for word in $words; do + for word in "${words[@]}"; do [[ "${word:0:1}" == "-" ]] && continue local seen=0 @@ -34,9 +34,15 @@ _mygit_completions_filter() { done ((!seen)) && result+=("$word") done - - echo "${result[*]}" fi + + local escaped=() + for word in "${result[@]}"; do + printf -v word '%q' "$word" + escaped+=("$word") + done + + echo "${escaped[*]}" } _mygit_completions() { @@ -51,7 +57,7 @@ _mygit_completions() { case "$compline" in *) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "status commit")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "status" "commit")" -- "$cur") ;; esac diff --git a/spec/approvals/completions/script-only-spaces b/spec/approvals/completions/script-only-spaces index 44695a6..2ab1044 100644 --- a/spec/approvals/completions/script-only-spaces +++ b/spec/approvals/completions/script-only-spaces @@ -5,7 +5,7 @@ # Modifying it manually is not recommended _completely_completions_filter() { - local words="$1" + local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() @@ -17,12 +17,12 @@ _completely_completions_filter() { if [[ "${cur:0:1}" == "-" ]]; then # Completing an option: offer everything (including options) - echo "$words" + result=("${words[@]}") else # Completing a non-option: offer only non-options, # and don't re-offer ones already used earlier in the line. - for word in $words; do + for word in "${words[@]}"; do [[ "${word:0:1}" == "-" ]] && continue local seen=0 @@ -34,9 +34,15 @@ _completely_completions_filter() { done ((!seen)) && result+=("$word") done - - echo "${result[*]}" fi + + local escaped=() + for word in "${result[@]}"; do + printf -v word '%q' "$word" + escaped+=("$word") + done + + echo "${escaped[*]}" } _completely_completions() { @@ -51,7 +57,7 @@ _completely_completions() { case "$compline" in 'generate'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_completely_completions_filter "--help --force")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_completely_completions_filter "--help" "--force")" -- "$cur") ;; 'init'*) diff --git a/spec/approvals/completions/script-with-debug b/spec/approvals/completions/script-with-debug index afc0053..3a5acd2 100644 --- a/spec/approvals/completions/script-with-debug +++ b/spec/approvals/completions/script-with-debug @@ -5,7 +5,7 @@ # Modifying it manually is not recommended _completely_completions_filter() { - local words="$1" + local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() @@ -17,12 +17,12 @@ _completely_completions_filter() { if [[ "${cur:0:1}" == "-" ]]; then # Completing an option: offer everything (including options) - echo "$words" + result=("${words[@]}") else # Completing a non-option: offer only non-options, # and don't re-offer ones already used earlier in the line. - for word in $words; do + for word in "${words[@]}"; do [[ "${word:0:1}" == "-" ]] && continue local seen=0 @@ -34,9 +34,15 @@ _completely_completions_filter() { done ((!seen)) && result+=("$word") done - - echo "${result[*]}" fi + + local escaped=() + for word in "${result[@]}"; do + printf -v word '%q' "$word" + escaped+=("$word") + done + + echo "${escaped[*]}" } _completely_completions() { diff --git a/spec/completely/pattern_spec.rb b/spec/completely/pattern_spec.rb index 912ed3e..93952a4 100644 --- a/spec/completely/pattern_spec.rb +++ b/spec/completely/pattern_spec.rb @@ -105,7 +105,7 @@ describe '#compgen' do it 'returns a line of compgen arguments' do - expect(subject.compgen).to eq '-A file -A user -W "$(_filter "--message --help")"' + expect(subject.compgen).to eq '-A file -A user -W "$(_filter "--message" "--help")"' end context 'when there are no words for -W' do @@ -120,7 +120,15 @@ let(:completions) { %w[--message --help] } it 'omits the -A arguments' do - expect(subject.compgen).to eq '-W "$(_filter "--message --help")"' + expect(subject.compgen).to eq '-W "$(_filter "--message" "--help")"' + end + end + + context 'when words include spaces and quotes' do + let(:completions) { ['hello world', 'one"quote'] } + + it 'shell-escapes words before passing them to the filter' do + expect(subject.compgen).to eq '-W "$(_filter "hello world" "one\"quote")"' end end diff --git a/spec/fixtures/tester/default.bash b/spec/fixtures/tester/default.bash index a5a878f..b03570b 100644 --- a/spec/fixtures/tester/default.bash +++ b/spec/fixtures/tester/default.bash @@ -5,7 +5,7 @@ # Modifying it manually is not recommended _cli_completions_filter() { - local words="$1" + local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() @@ -17,12 +17,12 @@ _cli_completions_filter() { if [[ "${cur:0:1}" == "-" ]]; then # Completing an option: offer everything (including options) - echo "$words" + result=("${words[@]}") else # Completing a non-option: offer only non-options, # and don't re-offer ones already used earlier in the line. - for word in $words; do + for word in "${words[@]}"; do [[ "${word:0:1}" == "-" ]] && continue local seen=0 @@ -34,9 +34,15 @@ _cli_completions_filter() { done ((!seen)) && result+=("$word") done - - echo "${result[*]}" fi + + local escaped=() + for word in "${result[@]}"; do + printf -v word '%q' "$word" + escaped+=("$word") + done + + echo "${escaped[*]}" } _cli_completions() { @@ -51,19 +57,19 @@ _cli_completions() { case "$compline" in 'command childcommand'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--quiet --verbose -q -v")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--quiet" "--verbose" "-q" "-v")" -- "$cur") ;; 'command subcommand'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--force --quiet")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--force" "--quiet")" -- "$cur") ;; 'command'*) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "subcommand childcommand")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "subcommand" "childcommand")" -- "$cur") ;; *) - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--help --version command conquer")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--help" "--version" "command" "conquer")" -- "$cur") ;; esac From 834ed782af98b8fd4c5d6e959a66de7951ca968b Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 18 Feb 2026 22:10:50 +0200 Subject: [PATCH 2/2] - Refactor template filter function --- lib/completely/templates/template.erb | 29 +++++++------------ spec/approvals/cli/generated-script | 29 +++++++------------ spec/approvals/cli/generated-script-alt | 29 +++++++------------ spec/approvals/cli/generated-wrapped-script | 29 +++++++------------ .../approvals/cli/test/completely-tester-1.sh | 29 +++++++------------ .../approvals/cli/test/completely-tester-2.sh | 29 +++++++------------ spec/approvals/cli/test/completely-tester.sh | 29 +++++++------------ spec/approvals/completions/function | 29 +++++++------------ spec/approvals/completions/script | 29 +++++++------------ .../completions/script-complete-options | 29 +++++++------------ spec/approvals/completions/script-only-spaces | 29 +++++++------------ spec/approvals/completions/script-with-debug | 29 +++++++------------ spec/fixtures/tester/default.bash | 29 +++++++------------ 13 files changed, 143 insertions(+), 234 deletions(-) diff --git a/lib/completely/templates/template.erb b/lib/completely/templates/template.erb index 6a587b2..8b46f92 100644 --- a/lib/completely/templates/template.erb +++ b/lib/completely/templates/template.erb @@ -8,6 +8,7 @@ local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() + local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -15,34 +16,26 @@ used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - if [[ "${cur:0:1}" == "-" ]]; then - # Completing an option: offer everything (including options) - result=("${words[@]}") - - else - # Completing a non-option: offer only non-options, - # and don't re-offer ones already used earlier in the line. - for word in "${words[@]}"; do + # Completing an option: offer everything. + # Completing a non-option: drop options and already-used words. + [[ "${cur:0:1}" == "-" ]] && want_options=1 + for word in "${words[@]}"; do + if ((!want_options)); then [[ "${word:0:1}" == "-" ]] && continue - local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - seen=1 - break + continue 2 fi done - ((!seen)) && result+=("$word") - done - fi + fi - local escaped=() - for word in "${result[@]}"; do + # compgen -W expects shell-escaped words in one space-delimited string. printf -v word '%q' "$word" - escaped+=("$word") + result+=("$word") done - echo "${escaped[*]}" + echo "${result[*]}" } <%= function_name %>() { diff --git a/spec/approvals/cli/generated-script b/spec/approvals/cli/generated-script index 73d1d11..8e7ee14 100644 --- a/spec/approvals/cli/generated-script +++ b/spec/approvals/cli/generated-script @@ -8,6 +8,7 @@ _mygit_completions_filter() { local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() + local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -15,34 +16,26 @@ _mygit_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - if [[ "${cur:0:1}" == "-" ]]; then - # Completing an option: offer everything (including options) - result=("${words[@]}") - - else - # Completing a non-option: offer only non-options, - # and don't re-offer ones already used earlier in the line. - for word in "${words[@]}"; do + # Completing an option: offer everything. + # Completing a non-option: drop options and already-used words. + [[ "${cur:0:1}" == "-" ]] && want_options=1 + for word in "${words[@]}"; do + if ((!want_options)); then [[ "${word:0:1}" == "-" ]] && continue - local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - seen=1 - break + continue 2 fi done - ((!seen)) && result+=("$word") - done - fi + fi - local escaped=() - for word in "${result[@]}"; do + # compgen -W expects shell-escaped words in one space-delimited string. printf -v word '%q' "$word" - escaped+=("$word") + result+=("$word") done - echo "${escaped[*]}" + echo "${result[*]}" } _mygit_completions() { diff --git a/spec/approvals/cli/generated-script-alt b/spec/approvals/cli/generated-script-alt index c3840ce..23eb09b 100644 --- a/spec/approvals/cli/generated-script-alt +++ b/spec/approvals/cli/generated-script-alt @@ -8,6 +8,7 @@ _mycomps_filter() { local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() + local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -15,34 +16,26 @@ _mycomps_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - if [[ "${cur:0:1}" == "-" ]]; then - # Completing an option: offer everything (including options) - result=("${words[@]}") - - else - # Completing a non-option: offer only non-options, - # and don't re-offer ones already used earlier in the line. - for word in "${words[@]}"; do + # Completing an option: offer everything. + # Completing a non-option: drop options and already-used words. + [[ "${cur:0:1}" == "-" ]] && want_options=1 + for word in "${words[@]}"; do + if ((!want_options)); then [[ "${word:0:1}" == "-" ]] && continue - local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - seen=1 - break + continue 2 fi done - ((!seen)) && result+=("$word") - done - fi + fi - local escaped=() - for word in "${result[@]}"; do + # compgen -W expects shell-escaped words in one space-delimited string. printf -v word '%q' "$word" - escaped+=("$word") + result+=("$word") done - echo "${escaped[*]}" + echo "${result[*]}" } _mycomps() { diff --git a/spec/approvals/cli/generated-wrapped-script b/spec/approvals/cli/generated-wrapped-script index 50bfcb9..875142e 100644 --- a/spec/approvals/cli/generated-wrapped-script +++ b/spec/approvals/cli/generated-wrapped-script @@ -9,6 +9,7 @@ give_comps() { echo $' local words=("$@")' echo $' local cur=${COMP_WORDS[COMP_CWORD]}' echo $' local result=()' + echo $' local want_options=0' echo $'' echo $' # words the user already typed (excluding the command itself)' echo $' local used=()' @@ -16,34 +17,26 @@ give_comps() { echo $' used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' echo $' fi' echo $'' - echo $' if [[ "${cur:0:1}" == "-" ]]; then' - echo $' # Completing an option: offer everything (including options)' - echo $' result=("${words[@]}")' - echo $'' - echo $' else' - echo $' # Completing a non-option: offer only non-options,' - echo $' # and don\'t re-offer ones already used earlier in the line.' - echo $' for word in "${words[@]}"; do' + echo $' # Completing an option: offer everything.' + echo $' # Completing a non-option: drop options and already-used words.' + echo $' [[ "${cur:0:1}" == "-" ]] && want_options=1' + echo $' for word in "${words[@]}"; do' + echo $' if ((!want_options)); then' echo $' [[ "${word:0:1}" == "-" ]] && continue' echo $'' - echo $' local seen=0' echo $' for u in "${used[@]}"; do' echo $' if [[ "$u" == "$word" ]]; then' - echo $' seen=1' - echo $' break' + echo $' continue 2' echo $' fi' echo $' done' - echo $' ((!seen)) && result+=("$word")' - echo $' done' - echo $' fi' + echo $' fi' echo $'' - echo $' local escaped=()' - echo $' for word in "${result[@]}"; do' + echo $' # compgen -W expects shell-escaped words in one space-delimited string.' echo $' printf -v word \'%q\' "$word"' - echo $' escaped+=("$word")' + echo $' result+=("$word")' echo $' done' echo $'' - echo $' echo "${escaped[*]}"' + echo $' echo "${result[*]}"' echo $'}' echo $'' echo $'_mygit_completions() {' diff --git a/spec/approvals/cli/test/completely-tester-1.sh b/spec/approvals/cli/test/completely-tester-1.sh index 8d68e40..7d141a9 100644 --- a/spec/approvals/cli/test/completely-tester-1.sh +++ b/spec/approvals/cli/test/completely-tester-1.sh @@ -16,6 +16,7 @@ _mygit_completions_filter() { local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() + local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -23,34 +24,26 @@ _mygit_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - if [[ "${cur:0:1}" == "-" ]]; then - # Completing an option: offer everything (including options) - result=("${words[@]}") - - else - # Completing a non-option: offer only non-options, - # and don't re-offer ones already used earlier in the line. - for word in "${words[@]}"; do + # Completing an option: offer everything. + # Completing a non-option: drop options and already-used words. + [[ "${cur:0:1}" == "-" ]] && want_options=1 + for word in "${words[@]}"; do + if ((!want_options)); then [[ "${word:0:1}" == "-" ]] && continue - local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - seen=1 - break + continue 2 fi done - ((!seen)) && result+=("$word") - done - fi + fi - local escaped=() - for word in "${result[@]}"; do + # compgen -W expects shell-escaped words in one space-delimited string. printf -v word '%q' "$word" - escaped+=("$word") + result+=("$word") done - echo "${escaped[*]}" + echo "${result[*]}" } _mygit_completions() { diff --git a/spec/approvals/cli/test/completely-tester-2.sh b/spec/approvals/cli/test/completely-tester-2.sh index 39bea97..73555b9 100644 --- a/spec/approvals/cli/test/completely-tester-2.sh +++ b/spec/approvals/cli/test/completely-tester-2.sh @@ -16,6 +16,7 @@ _mygit_completions_filter() { local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() + local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -23,34 +24,26 @@ _mygit_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - if [[ "${cur:0:1}" == "-" ]]; then - # Completing an option: offer everything (including options) - result=("${words[@]}") - - else - # Completing a non-option: offer only non-options, - # and don't re-offer ones already used earlier in the line. - for word in "${words[@]}"; do + # Completing an option: offer everything. + # Completing a non-option: drop options and already-used words. + [[ "${cur:0:1}" == "-" ]] && want_options=1 + for word in "${words[@]}"; do + if ((!want_options)); then [[ "${word:0:1}" == "-" ]] && continue - local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - seen=1 - break + continue 2 fi done - ((!seen)) && result+=("$word") - done - fi + fi - local escaped=() - for word in "${result[@]}"; do + # compgen -W expects shell-escaped words in one space-delimited string. printf -v word '%q' "$word" - escaped+=("$word") + result+=("$word") done - echo "${escaped[*]}" + echo "${result[*]}" } _mygit_completions() { diff --git a/spec/approvals/cli/test/completely-tester.sh b/spec/approvals/cli/test/completely-tester.sh index 233b68a..a3235d5 100644 --- a/spec/approvals/cli/test/completely-tester.sh +++ b/spec/approvals/cli/test/completely-tester.sh @@ -16,6 +16,7 @@ _mygit_completions_filter() { local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() + local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -23,34 +24,26 @@ _mygit_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - if [[ "${cur:0:1}" == "-" ]]; then - # Completing an option: offer everything (including options) - result=("${words[@]}") - - else - # Completing a non-option: offer only non-options, - # and don't re-offer ones already used earlier in the line. - for word in "${words[@]}"; do + # Completing an option: offer everything. + # Completing a non-option: drop options and already-used words. + [[ "${cur:0:1}" == "-" ]] && want_options=1 + for word in "${words[@]}"; do + if ((!want_options)); then [[ "${word:0:1}" == "-" ]] && continue - local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - seen=1 - break + continue 2 fi done - ((!seen)) && result+=("$word") - done - fi + fi - local escaped=() - for word in "${result[@]}"; do + # compgen -W expects shell-escaped words in one space-delimited string. printf -v word '%q' "$word" - escaped+=("$word") + result+=("$word") done - echo "${escaped[*]}" + echo "${result[*]}" } _mygit_completions() { diff --git a/spec/approvals/completions/function b/spec/approvals/completions/function index efdc9ab..ffaa11c 100644 --- a/spec/approvals/completions/function +++ b/spec/approvals/completions/function @@ -9,6 +9,7 @@ send_completions() { echo $' local words=("$@")' echo $' local cur=${COMP_WORDS[COMP_CWORD]}' echo $' local result=()' + echo $' local want_options=0' echo $'' echo $' # words the user already typed (excluding the command itself)' echo $' local used=()' @@ -16,34 +17,26 @@ send_completions() { echo $' used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' echo $' fi' echo $'' - echo $' if [[ "${cur:0:1}" == "-" ]]; then' - echo $' # Completing an option: offer everything (including options)' - echo $' result=("${words[@]}")' - echo $'' - echo $' else' - echo $' # Completing a non-option: offer only non-options,' - echo $' # and don\'t re-offer ones already used earlier in the line.' - echo $' for word in "${words[@]}"; do' + echo $' # Completing an option: offer everything.' + echo $' # Completing a non-option: drop options and already-used words.' + echo $' [[ "${cur:0:1}" == "-" ]] && want_options=1' + echo $' for word in "${words[@]}"; do' + echo $' if ((!want_options)); then' echo $' [[ "${word:0:1}" == "-" ]] && continue' echo $'' - echo $' local seen=0' echo $' for u in "${used[@]}"; do' echo $' if [[ "$u" == "$word" ]]; then' - echo $' seen=1' - echo $' break' + echo $' continue 2' echo $' fi' echo $' done' - echo $' ((!seen)) && result+=("$word")' - echo $' done' - echo $' fi' + echo $' fi' echo $'' - echo $' local escaped=()' - echo $' for word in "${result[@]}"; do' + echo $' # compgen -W expects shell-escaped words in one space-delimited string.' echo $' printf -v word \'%q\' "$word"' - echo $' escaped+=("$word")' + echo $' result+=("$word")' echo $' done' echo $'' - echo $' echo "${escaped[*]}"' + echo $' echo "${result[*]}"' echo $'}' echo $'' echo $'_completely_completions() {' diff --git a/spec/approvals/completions/script b/spec/approvals/completions/script index e15f20f..bb33f7b 100644 --- a/spec/approvals/completions/script +++ b/spec/approvals/completions/script @@ -8,6 +8,7 @@ _completely_completions_filter() { local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() + local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -15,34 +16,26 @@ _completely_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - if [[ "${cur:0:1}" == "-" ]]; then - # Completing an option: offer everything (including options) - result=("${words[@]}") - - else - # Completing a non-option: offer only non-options, - # and don't re-offer ones already used earlier in the line. - for word in "${words[@]}"; do + # Completing an option: offer everything. + # Completing a non-option: drop options and already-used words. + [[ "${cur:0:1}" == "-" ]] && want_options=1 + for word in "${words[@]}"; do + if ((!want_options)); then [[ "${word:0:1}" == "-" ]] && continue - local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - seen=1 - break + continue 2 fi done - ((!seen)) && result+=("$word") - done - fi + fi - local escaped=() - for word in "${result[@]}"; do + # compgen -W expects shell-escaped words in one space-delimited string. printf -v word '%q' "$word" - escaped+=("$word") + result+=("$word") done - echo "${escaped[*]}" + echo "${result[*]}" } _completely_completions() { diff --git a/spec/approvals/completions/script-complete-options b/spec/approvals/completions/script-complete-options index a3b05f8..8e6347c 100644 --- a/spec/approvals/completions/script-complete-options +++ b/spec/approvals/completions/script-complete-options @@ -8,6 +8,7 @@ _mygit_completions_filter() { local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() + local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -15,34 +16,26 @@ _mygit_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - if [[ "${cur:0:1}" == "-" ]]; then - # Completing an option: offer everything (including options) - result=("${words[@]}") - - else - # Completing a non-option: offer only non-options, - # and don't re-offer ones already used earlier in the line. - for word in "${words[@]}"; do + # Completing an option: offer everything. + # Completing a non-option: drop options and already-used words. + [[ "${cur:0:1}" == "-" ]] && want_options=1 + for word in "${words[@]}"; do + if ((!want_options)); then [[ "${word:0:1}" == "-" ]] && continue - local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - seen=1 - break + continue 2 fi done - ((!seen)) && result+=("$word") - done - fi + fi - local escaped=() - for word in "${result[@]}"; do + # compgen -W expects shell-escaped words in one space-delimited string. printf -v word '%q' "$word" - escaped+=("$word") + result+=("$word") done - echo "${escaped[*]}" + echo "${result[*]}" } _mygit_completions() { diff --git a/spec/approvals/completions/script-only-spaces b/spec/approvals/completions/script-only-spaces index 2ab1044..f980115 100644 --- a/spec/approvals/completions/script-only-spaces +++ b/spec/approvals/completions/script-only-spaces @@ -8,6 +8,7 @@ _completely_completions_filter() { local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() + local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -15,34 +16,26 @@ _completely_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - if [[ "${cur:0:1}" == "-" ]]; then - # Completing an option: offer everything (including options) - result=("${words[@]}") - - else - # Completing a non-option: offer only non-options, - # and don't re-offer ones already used earlier in the line. - for word in "${words[@]}"; do + # Completing an option: offer everything. + # Completing a non-option: drop options and already-used words. + [[ "${cur:0:1}" == "-" ]] && want_options=1 + for word in "${words[@]}"; do + if ((!want_options)); then [[ "${word:0:1}" == "-" ]] && continue - local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - seen=1 - break + continue 2 fi done - ((!seen)) && result+=("$word") - done - fi + fi - local escaped=() - for word in "${result[@]}"; do + # compgen -W expects shell-escaped words in one space-delimited string. printf -v word '%q' "$word" - escaped+=("$word") + result+=("$word") done - echo "${escaped[*]}" + echo "${result[*]}" } _completely_completions() { diff --git a/spec/approvals/completions/script-with-debug b/spec/approvals/completions/script-with-debug index 3a5acd2..7a3673c 100644 --- a/spec/approvals/completions/script-with-debug +++ b/spec/approvals/completions/script-with-debug @@ -8,6 +8,7 @@ _completely_completions_filter() { local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() + local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -15,34 +16,26 @@ _completely_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - if [[ "${cur:0:1}" == "-" ]]; then - # Completing an option: offer everything (including options) - result=("${words[@]}") - - else - # Completing a non-option: offer only non-options, - # and don't re-offer ones already used earlier in the line. - for word in "${words[@]}"; do + # Completing an option: offer everything. + # Completing a non-option: drop options and already-used words. + [[ "${cur:0:1}" == "-" ]] && want_options=1 + for word in "${words[@]}"; do + if ((!want_options)); then [[ "${word:0:1}" == "-" ]] && continue - local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - seen=1 - break + continue 2 fi done - ((!seen)) && result+=("$word") - done - fi + fi - local escaped=() - for word in "${result[@]}"; do + # compgen -W expects shell-escaped words in one space-delimited string. printf -v word '%q' "$word" - escaped+=("$word") + result+=("$word") done - echo "${escaped[*]}" + echo "${result[*]}" } _completely_completions() { diff --git a/spec/fixtures/tester/default.bash b/spec/fixtures/tester/default.bash index b03570b..c4df327 100644 --- a/spec/fixtures/tester/default.bash +++ b/spec/fixtures/tester/default.bash @@ -8,6 +8,7 @@ _cli_completions_filter() { local words=("$@") local cur=${COMP_WORDS[COMP_CWORD]} local result=() + local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -15,34 +16,26 @@ _cli_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - if [[ "${cur:0:1}" == "-" ]]; then - # Completing an option: offer everything (including options) - result=("${words[@]}") - - else - # Completing a non-option: offer only non-options, - # and don't re-offer ones already used earlier in the line. - for word in "${words[@]}"; do + # Completing an option: offer everything. + # Completing a non-option: drop options and already-used words. + [[ "${cur:0:1}" == "-" ]] && want_options=1 + for word in "${words[@]}"; do + if ((!want_options)); then [[ "${word:0:1}" == "-" ]] && continue - local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - seen=1 - break + continue 2 fi done - ((!seen)) && result+=("$word") - done - fi + fi - local escaped=() - for word in "${result[@]}"; do + # compgen -W expects shell-escaped words in one space-delimited string. printf -v word '%q' "$word" - escaped+=("$word") + result+=("$word") done - echo "${escaped[*]}" + echo "${result[*]}" } _cli_completions() {