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..8b46f92 100644 --- a/lib/completely/templates/template.erb +++ b/lib/completely/templates/template.erb @@ -5,9 +5,10 @@ # Modifying it manually is not recommended <%= function_name %>_filter() { - local words="$1" + 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,28 +16,26 @@ used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - if [[ "${cur:0:1}" == "-" ]]; then - # Completing an option: offer everything (including options) - echo "$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 - echo "${result[*]}" - fi + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") + done + + echo "${result[*]}" } <%= function_name %>() { diff --git a/spec/approvals/cli/generated-script b/spec/approvals/cli/generated-script index 94e256e..8e7ee14 100644 --- a/spec/approvals/cli/generated-script +++ b/spec/approvals/cli/generated-script @@ -5,9 +5,10 @@ # Modifying it manually is not recommended _mygit_completions_filter() { - local words="$1" + 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,28 +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) - echo "$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 - echo "${result[*]}" - fi + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") + done + + echo "${result[*]}" } _mygit_completions() { @@ -59,7 +58,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 +66,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..23eb09b 100644 --- a/spec/approvals/cli/generated-script-alt +++ b/spec/approvals/cli/generated-script-alt @@ -5,9 +5,10 @@ # Modifying it manually is not recommended _mycomps_filter() { - local words="$1" + 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,28 +16,26 @@ _mycomps_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - if [[ "${cur:0:1}" == "-" ]]; then - # Completing an option: offer everything (including options) - echo "$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 - echo "${result[*]}" - fi + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") + done + + echo "${result[*]}" } _mycomps() { @@ -59,7 +58,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 +66,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..875142e 100644 --- a/spec/approvals/cli/generated-wrapped-script +++ b/spec/approvals/cli/generated-wrapped-script @@ -6,9 +6,10 @@ 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 $' local want_options=0' echo $'' echo $' # words the user already typed (excluding the command itself)' echo $' local used=()' @@ -16,28 +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 $' echo "$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 $'' - echo $' echo "${result[*]}"' - echo $' fi' + echo $' # compgen -W expects shell-escaped words in one space-delimited string.' + echo $' printf -v word \'%q\' "$word"' + echo $' result+=("$word")' + echo $' done' + echo $'' + echo $' echo "${result[*]}"' echo $'}' echo $'' echo $'_mygit_completions() {' @@ -60,7 +59,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 +67,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..7d141a9 100644 --- a/spec/approvals/cli/test/completely-tester-1.sh +++ b/spec/approvals/cli/test/completely-tester-1.sh @@ -13,9 +13,10 @@ fi # Modifying it manually is not recommended _mygit_completions_filter() { - local words="$1" + 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,28 +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) - echo "$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 - echo "${result[*]}" - fi + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") + done + + echo "${result[*]}" } _mygit_completions() { @@ -67,7 +66,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 +74,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..73555b9 100644 --- a/spec/approvals/cli/test/completely-tester-2.sh +++ b/spec/approvals/cli/test/completely-tester-2.sh @@ -13,9 +13,10 @@ fi # Modifying it manually is not recommended _mygit_completions_filter() { - local words="$1" + 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,28 +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) - echo "$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 - echo "${result[*]}" - fi + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") + done + + echo "${result[*]}" } _mygit_completions() { @@ -67,7 +66,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 +74,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..a3235d5 100644 --- a/spec/approvals/cli/test/completely-tester.sh +++ b/spec/approvals/cli/test/completely-tester.sh @@ -13,9 +13,10 @@ fi # Modifying it manually is not recommended _mygit_completions_filter() { - local words="$1" + 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,28 +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) - echo "$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 - echo "${result[*]}" - fi + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") + done + + echo "${result[*]}" } _mygit_completions() { @@ -67,7 +66,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 +74,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..ffaa11c 100644 --- a/spec/approvals/completions/function +++ b/spec/approvals/completions/function @@ -6,9 +6,10 @@ 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 $' local want_options=0' echo $'' echo $' # words the user already typed (excluding the command itself)' echo $' local used=()' @@ -16,28 +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 $' echo "$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 $'' - echo $' echo "${result[*]}"' - echo $' fi' + echo $' # compgen -W expects shell-escaped words in one space-delimited string.' + echo $' printf -v word \'%q\' "$word"' + echo $' result+=("$word")' + echo $' done' + echo $'' + echo $' echo "${result[*]}"' echo $'}' echo $'' echo $'_completely_completions() {' @@ -52,7 +51,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 +59,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..bb33f7b 100644 --- a/spec/approvals/completions/script +++ b/spec/approvals/completions/script @@ -5,9 +5,10 @@ # Modifying it manually is not recommended _completely_completions_filter() { - local words="$1" + 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,28 +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) - echo "$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 - echo "${result[*]}" - fi + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") + done + + echo "${result[*]}" } _completely_completions() { @@ -51,7 +50,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 +58,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..8e6347c 100644 --- a/spec/approvals/completions/script-complete-options +++ b/spec/approvals/completions/script-complete-options @@ -5,9 +5,10 @@ # Modifying it manually is not recommended _mygit_completions_filter() { - local words="$1" + 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,28 +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) - echo "$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 - echo "${result[*]}" - fi + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") + done + + echo "${result[*]}" } _mygit_completions() { @@ -51,7 +50,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..f980115 100644 --- a/spec/approvals/completions/script-only-spaces +++ b/spec/approvals/completions/script-only-spaces @@ -5,9 +5,10 @@ # Modifying it manually is not recommended _completely_completions_filter() { - local words="$1" + 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,28 +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) - echo "$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 - echo "${result[*]}" - fi + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") + done + + echo "${result[*]}" } _completely_completions() { @@ -51,7 +50,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..7a3673c 100644 --- a/spec/approvals/completions/script-with-debug +++ b/spec/approvals/completions/script-with-debug @@ -5,9 +5,10 @@ # Modifying it manually is not recommended _completely_completions_filter() { - local words="$1" + 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,28 +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) - echo "$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 - echo "${result[*]}" - fi + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") + done + + echo "${result[*]}" } _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..c4df327 100644 --- a/spec/fixtures/tester/default.bash +++ b/spec/fixtures/tester/default.bash @@ -5,9 +5,10 @@ # Modifying it manually is not recommended _cli_completions_filter() { - local words="$1" + 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,28 +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) - echo "$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 - echo "${result[*]}" - fi + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") + done + + echo "${result[*]}" } _cli_completions() { @@ -51,19 +50,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