From 4455eb94ae147d8c28b81d90265b2c665e4dc023 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Fri, 20 Feb 2026 01:53:52 +0200 Subject: [PATCH 1/8] - Fix dynamic completions (regression) --- AGENTS.md | 69 +++++++++++++++++++ lib/completely/pattern.rb | 20 +++++- lib/completely/templates/template.erb | 33 ++++++--- spec/README.md | 25 +++++-- spec/approvals/cli/generated-script | 37 ++++++---- spec/approvals/cli/generated-script-alt | 37 ++++++---- spec/approvals/cli/generated-wrapped-script | 37 ++++++---- .../approvals/cli/test/completely-tester-1.sh | 37 ++++++---- .../approvals/cli/test/completely-tester-2.sh | 37 ++++++---- spec/approvals/cli/test/completely-tester.sh | 37 ++++++---- spec/approvals/completions/function | 33 ++++++--- spec/approvals/completions/script | 33 ++++++--- .../completions/script-complete-options | 33 ++++++--- spec/approvals/completions/script-only-spaces | 33 ++++++--- spec/approvals/completions/script-with-debug | 33 ++++++--- spec/completely/commands/generate_spec.rb | 4 +- spec/completely/integration.yml | 13 +++- spec/fixtures/integration/dynamic.txt | 2 + spec/fixtures/integration/dynamic.yaml | 3 + spec/fixtures/tester/default.bash | 33 ++++++--- 20 files changed, 420 insertions(+), 169 deletions(-) create mode 100644 AGENTS.md create mode 100644 spec/fixtures/integration/dynamic.txt create mode 100644 spec/fixtures/integration/dynamic.yaml diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..252ce00 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,69 @@ +# AGENTS.md + +Guidance for coding agents working in this repository. + +## Repo Snapshot + +- Project: `completely` (Ruby gem that generates Bash completion scripts from YAML). +- Key generation code: + - `lib/completely/pattern.rb` + - `lib/completely/templates/template.erb` +- Core behavior tests: + - `spec/completely/integration_spec.rb` + - `spec/completely/commands/generate_spec.rb` + +## Working Rules + +- Keep changes minimal and localized, especially in: + - completion-word serialization (`Pattern`) + - generated script runtime behavior (`template.erb`) +- Do not change generated approvals. +- Do not run approval prompts interactively on behalf of the developer. +- If an approval spec changes, stop and ask the developer to review/approve manually. +- Prefer adding regression coverage in integration fixtures for completion behavior changes. + +## Fast Validation Loop + +Run these first after edits: + +```bash +respec tagged script_quality +respec only integration +``` + +If touching quoting/escaping or dynamic completions, also run: + +```bash +respec only pattern +respec only completions +``` + +## Formatting and Linting Notes + +- `shellcheck` and `shfmt` requirements are enforced by specs tagged `:script_quality` in `spec/completely/commands/generate_spec.rb`. +- `shfmt` uses flags: + - `shfmt -d -i 2 -ci completely.bash` +- Small whitespace differences in heredoc/redirect forms (like `<<<"$x"` vs `<<< "$x"`) can fail shfmt. + +## Approval Specs + +- Some specs use `rspec_approvals` and may prompt interactively if output changes. +- In non-interactive runs this can fail with `Errno::ENOTTY`. +- Approval decisions are always developer-owned. Agents should not approve/update snapshots. + +## Completion Semantics to Preserve + +- Literal YAML words with spaces/quotes must complete correctly. +- Dynamic `$(...)` entries must produce multiple completion candidates when command output contains multiple words. +- ``, ``, and other `<...>` entries map to `compgen -A ...` actions and should remain unaffected by `-W` serialization changes. + +## Manual Repro Pattern + +Useful local sanity check: + +```bash +cd dev +ruby -I../lib ../bin/completely test "cli " +``` + +Expected: sensible mixed output for dynamic values and quoted/spaced literals. diff --git a/lib/completely/pattern.rb b/lib/completely/pattern.rb index 1c6f6b7..62e954b 100644 --- a/lib/completely/pattern.rb +++ b/lib/completely/pattern.rb @@ -1,5 +1,7 @@ module Completely class Pattern + DYNAMIC_WORD_PREFIX = '__completely_dynamic__' + attr_reader :text, :completions, :function_name def initialize(text, completions, function_name) @@ -54,12 +56,24 @@ def compgen def compgen! result = [] result << actions.join(' ').to_s if actions.any? - result << %[-W "$(#{function_name} #{quoted_words.join ' '})"] if words.any? + result << %[-W "$(#{function_name} #{serialized_words.join ' '})"] if words.any? result.any? ? result.join(' ') : nil end - def quoted_words - @quoted_words ||= words.map { |word| %("#{escape_for_double_quotes word}") } + def serialized_words + @serialized_words ||= words.map { |word| serialize_word(word) } + end + + def serialize_word(word) + if dynamic_word?(word) + return %("#{DYNAMIC_WORD_PREFIX}#{escape_for_double_quotes word}") + end + + %("#{escape_for_double_quotes word}") + end + + def dynamic_word?(word) + word.match?(/\A\$\(.+\)\z/) end def escape_for_double_quotes(word) diff --git a/lib/completely/templates/template.erb b/lib/completely/templates/template.erb index 8b46f92..a2c1bf1 100644 --- a/lib/completely/templates/template.erb +++ b/lib/completely/templates/template.erb @@ -9,6 +9,7 @@ local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 + local dynamic_prefix="<%= Completely::Pattern::DYNAMIC_WORD_PREFIX %>" # words the user already typed (excluding the command itself) local used=() @@ -20,19 +21,29 @@ # 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 - - for u in "${used[@]}"; do - if [[ "$u" == "$word" ]]; then - continue 2 - fi - done + local candidates=("$word") + if [[ "$word" == "$dynamic_prefix"* ]]; then + word="${word#"$dynamic_prefix"}" + word="${word//$'\r'/ }" + word="${word//$'\n'/ }" + read -r -a candidates <<<"$word" fi - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") + for candidate in "${candidates[@]}"; do + if ((!want_options)); then + [[ "${candidate:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$candidate" ]]; then + continue 2 + fi + done + fi + + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v candidate '%q' "$candidate" + result+=("$candidate") + done done echo "${result[*]}" diff --git a/spec/README.md b/spec/README.md index 25343ef..c00362f 100644 --- a/spec/README.md +++ b/spec/README.md @@ -2,19 +2,29 @@ ## Running tests +You can run specs with `rspec` as usual. + +We recommend using [`respec`][2], which wraps common spec workflows: + ```bash -$ rspec +rspec # or -$ run spec -# or, to run just tests in a given file -$ run spec zsh -# or, to run just specs tagged with :focus -$ run spec :focus +respec ``` You might need to prefix the commands with `bundle exec`, depending on the way Ruby is installed. +Useful helper shortcuts: + +```bash +# script quality checks (shellcheck + shfmt generated script tests) +respec tagged script_quality + +# integration behavior suite +respec only integration +``` + ## Interactive Approvals Some tests may prompt you for an interactive approval of changes. This @@ -29,4 +39,5 @@ ZSH compatibility test is done by running the completely tester script inside a zsh container. This is all done automatically by `spec/completely/zsh_spec.rb`. -[1]: https://github.com/dannyben/rspec_approvals \ No newline at end of file +[1]: https://github.com/dannyben/rspec_approvals +[2]: https://github.com/DannyBen/respec diff --git a/spec/approvals/cli/generated-script b/spec/approvals/cli/generated-script index 8e7ee14..270eff7 100644 --- a/spec/approvals/cli/generated-script +++ b/spec/approvals/cli/generated-script @@ -9,6 +9,7 @@ _mygit_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 + local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -20,19 +21,29 @@ _mygit_completions_filter() { # 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 - - for u in "${used[@]}"; do - if [[ "$u" == "$word" ]]; then - continue 2 - fi - done + local candidates=("$word") + if [[ "$word" == "$dynamic_prefix"* ]]; then + word="${word#"$dynamic_prefix"}" + word="${word//$'\r'/ }" + word="${word//$'\n'/ }" + read -r -a candidates <<<"$word" fi - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") + for candidate in "${candidates[@]}"; do + if ((!want_options)); then + [[ "${candidate:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$candidate" ]]; then + continue 2 + fi + done + fi + + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v candidate '%q' "$candidate" + result+=("$candidate") + done done echo "${result[*]}" @@ -50,11 +61,11 @@ _mygit_completions() { case "$compline" in 'status'*'--branch') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*'-b') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*) diff --git a/spec/approvals/cli/generated-script-alt b/spec/approvals/cli/generated-script-alt index 23eb09b..db0ecca 100644 --- a/spec/approvals/cli/generated-script-alt +++ b/spec/approvals/cli/generated-script-alt @@ -9,6 +9,7 @@ _mycomps_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 + local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -20,19 +21,29 @@ _mycomps_filter() { # 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 - - for u in "${used[@]}"; do - if [[ "$u" == "$word" ]]; then - continue 2 - fi - done + local candidates=("$word") + if [[ "$word" == "$dynamic_prefix"* ]]; then + word="${word#"$dynamic_prefix"}" + word="${word//$'\r'/ }" + word="${word//$'\n'/ }" + read -r -a candidates <<<"$word" fi - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") + for candidate in "${candidates[@]}"; do + if ((!want_options)); then + [[ "${candidate:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$candidate" ]]; then + continue 2 + fi + done + fi + + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v candidate '%q' "$candidate" + result+=("$candidate") + done done echo "${result[*]}" @@ -50,11 +61,11 @@ _mycomps() { case "$compline" in 'status'*'--branch') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*'-b') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*) diff --git a/spec/approvals/cli/generated-wrapped-script b/spec/approvals/cli/generated-wrapped-script index 875142e..da503c5 100644 --- a/spec/approvals/cli/generated-wrapped-script +++ b/spec/approvals/cli/generated-wrapped-script @@ -10,6 +10,7 @@ give_comps() { echo $' local cur=${COMP_WORDS[COMP_CWORD]}' echo $' local result=()' echo $' local want_options=0' + echo $' local dynamic_prefix="__completely_dynamic__"' echo $'' echo $' # words the user already typed (excluding the command itself)' echo $' local used=()' @@ -21,19 +22,29 @@ give_comps() { 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 $' for u in "${used[@]}"; do' - echo $' if [[ "$u" == "$word" ]]; then' - echo $' continue 2' - echo $' fi' - echo $' done' + echo $' local candidates=("$word")' + echo $' if [[ "$word" == "$dynamic_prefix"* ]]; then' + echo $' word="${word#"$dynamic_prefix"}"' + echo $' word="${word//$\'\r\'/ }"' + echo $' word="${word//$\'\n\'/ }"' + echo $' read -r -a candidates <<<"$word"' echo $' fi' echo $'' - echo $' # compgen -W expects shell-escaped words in one space-delimited string.' - echo $' printf -v word \'%q\' "$word"' - echo $' result+=("$word")' + echo $' for candidate in "${candidates[@]}"; do' + echo $' if ((!want_options)); then' + echo $' [[ "${candidate:0:1}" == "-" ]] && continue' + echo $'' + echo $' for u in "${used[@]}"; do' + echo $' if [[ "$u" == "$candidate" ]]; then' + echo $' continue 2' + echo $' fi' + echo $' done' + echo $' fi' + echo $'' + echo $' # compgen -W expects shell-escaped words in one space-delimited string.' + echo $' printf -v candidate \'%q\' "$candidate"' + echo $' result+=("$candidate")' + echo $' done' echo $' done' echo $'' echo $' echo "${result[*]}"' @@ -51,11 +62,11 @@ give_comps() { echo $'' echo $' case "$compline" in' echo $' \'status\'*\'--branch\')' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format=\'%(refname:short)\' 2>/dev/null)")" -- "$cur")' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format=\'%(refname:short)\' 2>/dev/null)")" -- "$cur")' echo $' ;;' echo $'' echo $' \'status\'*\'-b\')' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format=\'%(refname:short)\' 2>/dev/null)")" -- "$cur")' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format=\'%(refname:short)\' 2>/dev/null)")" -- "$cur")' echo $' ;;' echo $'' echo $' \'status\'*)' diff --git a/spec/approvals/cli/test/completely-tester-1.sh b/spec/approvals/cli/test/completely-tester-1.sh index 7d141a9..48b87e3 100644 --- a/spec/approvals/cli/test/completely-tester-1.sh +++ b/spec/approvals/cli/test/completely-tester-1.sh @@ -17,6 +17,7 @@ _mygit_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 + local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -28,19 +29,29 @@ _mygit_completions_filter() { # 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 - - for u in "${used[@]}"; do - if [[ "$u" == "$word" ]]; then - continue 2 - fi - done + local candidates=("$word") + if [[ "$word" == "$dynamic_prefix"* ]]; then + word="${word#"$dynamic_prefix"}" + word="${word//$'\r'/ }" + word="${word//$'\n'/ }" + read -r -a candidates <<<"$word" fi - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") + for candidate in "${candidates[@]}"; do + if ((!want_options)); then + [[ "${candidate:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$candidate" ]]; then + continue 2 + fi + done + fi + + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v candidate '%q' "$candidate" + result+=("$candidate") + done done echo "${result[*]}" @@ -58,11 +69,11 @@ _mygit_completions() { case "$compline" in 'status'*'--branch') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*'-b') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*) diff --git a/spec/approvals/cli/test/completely-tester-2.sh b/spec/approvals/cli/test/completely-tester-2.sh index 73555b9..978816a 100644 --- a/spec/approvals/cli/test/completely-tester-2.sh +++ b/spec/approvals/cli/test/completely-tester-2.sh @@ -17,6 +17,7 @@ _mygit_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 + local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -28,19 +29,29 @@ _mygit_completions_filter() { # 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 - - for u in "${used[@]}"; do - if [[ "$u" == "$word" ]]; then - continue 2 - fi - done + local candidates=("$word") + if [[ "$word" == "$dynamic_prefix"* ]]; then + word="${word#"$dynamic_prefix"}" + word="${word//$'\r'/ }" + word="${word//$'\n'/ }" + read -r -a candidates <<<"$word" fi - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") + for candidate in "${candidates[@]}"; do + if ((!want_options)); then + [[ "${candidate:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$candidate" ]]; then + continue 2 + fi + done + fi + + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v candidate '%q' "$candidate" + result+=("$candidate") + done done echo "${result[*]}" @@ -58,11 +69,11 @@ _mygit_completions() { case "$compline" in 'status'*'--branch') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*'-b') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*) diff --git a/spec/approvals/cli/test/completely-tester.sh b/spec/approvals/cli/test/completely-tester.sh index a3235d5..0f42b58 100644 --- a/spec/approvals/cli/test/completely-tester.sh +++ b/spec/approvals/cli/test/completely-tester.sh @@ -17,6 +17,7 @@ _mygit_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 + local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -28,19 +29,29 @@ _mygit_completions_filter() { # 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 - - for u in "${used[@]}"; do - if [[ "$u" == "$word" ]]; then - continue 2 - fi - done + local candidates=("$word") + if [[ "$word" == "$dynamic_prefix"* ]]; then + word="${word#"$dynamic_prefix"}" + word="${word//$'\r'/ }" + word="${word//$'\n'/ }" + read -r -a candidates <<<"$word" fi - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") + for candidate in "${candidates[@]}"; do + if ((!want_options)); then + [[ "${candidate:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$candidate" ]]; then + continue 2 + fi + done + fi + + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v candidate '%q' "$candidate" + result+=("$candidate") + done done echo "${result[*]}" @@ -58,11 +69,11 @@ _mygit_completions() { case "$compline" in 'status'*'--branch') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*'-b') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*) diff --git a/spec/approvals/completions/function b/spec/approvals/completions/function index ffaa11c..c6d1d04 100644 --- a/spec/approvals/completions/function +++ b/spec/approvals/completions/function @@ -10,6 +10,7 @@ send_completions() { echo $' local cur=${COMP_WORDS[COMP_CWORD]}' echo $' local result=()' echo $' local want_options=0' + echo $' local dynamic_prefix="__completely_dynamic__"' echo $'' echo $' # words the user already typed (excluding the command itself)' echo $' local used=()' @@ -21,19 +22,29 @@ send_completions() { 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 $' for u in "${used[@]}"; do' - echo $' if [[ "$u" == "$word" ]]; then' - echo $' continue 2' - echo $' fi' - echo $' done' + echo $' local candidates=("$word")' + echo $' if [[ "$word" == "$dynamic_prefix"* ]]; then' + echo $' word="${word#"$dynamic_prefix"}"' + echo $' word="${word//$\'\r\'/ }"' + echo $' word="${word//$\'\n\'/ }"' + echo $' read -r -a candidates <<<"$word"' echo $' fi' echo $'' - echo $' # compgen -W expects shell-escaped words in one space-delimited string.' - echo $' printf -v word \'%q\' "$word"' - echo $' result+=("$word")' + echo $' for candidate in "${candidates[@]}"; do' + echo $' if ((!want_options)); then' + echo $' [[ "${candidate:0:1}" == "-" ]] && continue' + echo $'' + echo $' for u in "${used[@]}"; do' + echo $' if [[ "$u" == "$candidate" ]]; then' + echo $' continue 2' + echo $' fi' + echo $' done' + echo $' fi' + echo $'' + echo $' # compgen -W expects shell-escaped words in one space-delimited string.' + echo $' printf -v candidate \'%q\' "$candidate"' + echo $' result+=("$candidate")' + echo $' done' echo $' done' echo $'' echo $' echo "${result[*]}"' diff --git a/spec/approvals/completions/script b/spec/approvals/completions/script index bb33f7b..06641ed 100644 --- a/spec/approvals/completions/script +++ b/spec/approvals/completions/script @@ -9,6 +9,7 @@ _completely_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 + local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -20,19 +21,29 @@ _completely_completions_filter() { # 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 - - for u in "${used[@]}"; do - if [[ "$u" == "$word" ]]; then - continue 2 - fi - done + local candidates=("$word") + if [[ "$word" == "$dynamic_prefix"* ]]; then + word="${word#"$dynamic_prefix"}" + word="${word//$'\r'/ }" + word="${word//$'\n'/ }" + read -r -a candidates <<<"$word" fi - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") + for candidate in "${candidates[@]}"; do + if ((!want_options)); then + [[ "${candidate:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$candidate" ]]; then + continue 2 + fi + done + fi + + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v candidate '%q' "$candidate" + result+=("$candidate") + done done echo "${result[*]}" diff --git a/spec/approvals/completions/script-complete-options b/spec/approvals/completions/script-complete-options index 8e6347c..6ecdc99 100644 --- a/spec/approvals/completions/script-complete-options +++ b/spec/approvals/completions/script-complete-options @@ -9,6 +9,7 @@ _mygit_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 + local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -20,19 +21,29 @@ _mygit_completions_filter() { # 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 - - for u in "${used[@]}"; do - if [[ "$u" == "$word" ]]; then - continue 2 - fi - done + local candidates=("$word") + if [[ "$word" == "$dynamic_prefix"* ]]; then + word="${word#"$dynamic_prefix"}" + word="${word//$'\r'/ }" + word="${word//$'\n'/ }" + read -r -a candidates <<<"$word" fi - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") + for candidate in "${candidates[@]}"; do + if ((!want_options)); then + [[ "${candidate:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$candidate" ]]; then + continue 2 + fi + done + fi + + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v candidate '%q' "$candidate" + result+=("$candidate") + done done echo "${result[*]}" diff --git a/spec/approvals/completions/script-only-spaces b/spec/approvals/completions/script-only-spaces index f980115..401453f 100644 --- a/spec/approvals/completions/script-only-spaces +++ b/spec/approvals/completions/script-only-spaces @@ -9,6 +9,7 @@ _completely_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 + local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -20,19 +21,29 @@ _completely_completions_filter() { # 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 - - for u in "${used[@]}"; do - if [[ "$u" == "$word" ]]; then - continue 2 - fi - done + local candidates=("$word") + if [[ "$word" == "$dynamic_prefix"* ]]; then + word="${word#"$dynamic_prefix"}" + word="${word//$'\r'/ }" + word="${word//$'\n'/ }" + read -r -a candidates <<<"$word" fi - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") + for candidate in "${candidates[@]}"; do + if ((!want_options)); then + [[ "${candidate:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$candidate" ]]; then + continue 2 + fi + done + fi + + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v candidate '%q' "$candidate" + result+=("$candidate") + done done echo "${result[*]}" diff --git a/spec/approvals/completions/script-with-debug b/spec/approvals/completions/script-with-debug index 7a3673c..688a963 100644 --- a/spec/approvals/completions/script-with-debug +++ b/spec/approvals/completions/script-with-debug @@ -9,6 +9,7 @@ _completely_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 + local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -20,19 +21,29 @@ _completely_completions_filter() { # 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 - - for u in "${used[@]}"; do - if [[ "$u" == "$word" ]]; then - continue 2 - fi - done + local candidates=("$word") + if [[ "$word" == "$dynamic_prefix"* ]]; then + word="${word#"$dynamic_prefix"}" + word="${word//$'\r'/ }" + word="${word//$'\n'/ }" + read -r -a candidates <<<"$word" fi - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") + for candidate in "${candidates[@]}"; do + if ((!want_options)); then + [[ "${candidate:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$candidate" ]]; then + continue 2 + fi + done + fi + + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v candidate '%q' "$candidate" + result+=("$candidate") + done done echo "${result[*]}" diff --git a/spec/completely/commands/generate_spec.rb b/spec/completely/commands/generate_spec.rb index 9d152ea..65eee8e 100644 --- a/spec/completely/commands/generate_spec.rb +++ b/spec/completely/commands/generate_spec.rb @@ -22,12 +22,12 @@ expect(File.read 'completely.bash').to match_approval('cli/generated-script') end - it 'generates a shellcheck compliant script' do + it 'generates a shellcheck compliant script', :script_quality do expect { subject.execute %w[generate] }.to output_approval('cli/generate/no-args') expect(`shellcheck completely.bash 2>&1`).to be_empty end - it 'generates a shfmt compliant script' do + it 'generates a shfmt compliant script', :script_quality do expect { subject.execute %w[generate] }.to output_approval('cli/generate/no-args') expect(`shfmt -d -i 2 -ci completely.bash 2>&1`).to be_empty end diff --git a/spec/completely/integration.yml b/spec/completely/integration.yml index eb90815..05f3b53 100644 --- a/spec/completely/integration.yml +++ b/spec/completely/integration.yml @@ -9,7 +9,7 @@ ftp: expected: [download] - compline: "ftp download " - expected: [another-dir, dir with spaces, dummy-dir, file with spaces.txt, ftp.yaml, gradual.yaml, wildcard.yaml] + expected: [another-dir, dir with spaces, dummy-dir, dynamic.txt, dynamic.yaml, file with spaces.txt, ftp.yaml, gradual.yaml, wildcard.yaml] - compline: "ftp download -" expected: [--help, --override] @@ -29,6 +29,16 @@ ftp: - compline: "/anything/goes/ftp list -" expected: [--help, --short] +dynamic: +- compline: "dynamic " + expected: [foo bar, hello, help] + +- compline: "dynamic h" + expected: [hello, help] + +- compline: "dynamic f" + expected: [foo bar] + gradual: - compline: "cli " expected: [command, conquer] @@ -93,4 +103,3 @@ wildcard: - compline: "wildcard download --contest " expected: [everything, nothing] - diff --git a/spec/fixtures/integration/dynamic.txt b/spec/fixtures/integration/dynamic.txt new file mode 100644 index 0000000..da2146a --- /dev/null +++ b/spec/fixtures/integration/dynamic.txt @@ -0,0 +1,2 @@ +hello +help diff --git a/spec/fixtures/integration/dynamic.yaml b/spec/fixtures/integration/dynamic.yaml new file mode 100644 index 0000000..c677ab1 --- /dev/null +++ b/spec/fixtures/integration/dynamic.yaml @@ -0,0 +1,3 @@ +dynamic: +- $(cat dynamic.txt) +- foo bar diff --git a/spec/fixtures/tester/default.bash b/spec/fixtures/tester/default.bash index c4df327..f31db9d 100644 --- a/spec/fixtures/tester/default.bash +++ b/spec/fixtures/tester/default.bash @@ -9,6 +9,7 @@ _cli_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 + local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -20,19 +21,29 @@ _cli_completions_filter() { # 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 - - for u in "${used[@]}"; do - if [[ "$u" == "$word" ]]; then - continue 2 - fi - done + local candidates=("$word") + if [[ "$word" == "$dynamic_prefix"* ]]; then + word="${word#"$dynamic_prefix"}" + word="${word//$'\r'/ }" + word="${word//$'\n'/ }" + read -r -a candidates <<<"$word" fi - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") + for candidate in "${candidates[@]}"; do + if ((!want_options)); then + [[ "${candidate:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$candidate" ]]; then + continue 2 + fi + done + fi + + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v candidate '%q' "$candidate" + result+=("$candidate") + done done echo "${result[*]}" From 95ea525ba36a5bfb328908ae218d7f690b2ca7e7 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Fri, 20 Feb 2026 02:33:37 +0200 Subject: [PATCH 2/8] simplify dynamic completion check --- lib/completely/pattern.rb | 6 +-- lib/completely/templates/template.erb | 34 ++++++--------- lib/completely/version.rb | 2 +- spec/approvals/cli/generated-script | 42 ++++++++----------- spec/approvals/cli/generated-script-alt | 42 ++++++++----------- spec/approvals/cli/generated-wrapped-script | 42 ++++++++----------- .../approvals/cli/test/completely-tester-1.sh | 42 ++++++++----------- .../approvals/cli/test/completely-tester-2.sh | 42 ++++++++----------- spec/approvals/cli/test/completely-tester.sh | 42 ++++++++----------- spec/approvals/completions/function | 36 +++++++--------- spec/approvals/completions/script | 36 +++++++--------- .../completions/script-complete-options | 34 ++++++--------- spec/approvals/completions/script-only-spaces | 35 ++++++---------- spec/approvals/completions/script-with-debug | 33 +++++---------- spec/fixtures/tester/default.bash | 37 +++++++--------- 15 files changed, 201 insertions(+), 304 deletions(-) diff --git a/lib/completely/pattern.rb b/lib/completely/pattern.rb index 62e954b..92333a2 100644 --- a/lib/completely/pattern.rb +++ b/lib/completely/pattern.rb @@ -1,7 +1,5 @@ module Completely class Pattern - DYNAMIC_WORD_PREFIX = '__completely_dynamic__' - attr_reader :text, :completions, :function_name def initialize(text, completions, function_name) @@ -65,9 +63,7 @@ def serialized_words end def serialize_word(word) - if dynamic_word?(word) - return %("#{DYNAMIC_WORD_PREFIX}#{escape_for_double_quotes word}") - end + return word if dynamic_word?(word) %("#{escape_for_double_quotes word}") end diff --git a/lib/completely/templates/template.erb b/lib/completely/templates/template.erb index a2c1bf1..45bb795 100644 --- a/lib/completely/templates/template.erb +++ b/lib/completely/templates/template.erb @@ -9,7 +9,6 @@ local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 - local dynamic_prefix="<%= Completely::Pattern::DYNAMIC_WORD_PREFIX %>" # words the user already typed (excluding the command itself) local used=() @@ -21,29 +20,19 @@ # Completing a non-option: drop options and already-used words. [[ "${cur:0:1}" == "-" ]] && want_options=1 for word in "${words[@]}"; do - local candidates=("$word") - if [[ "$word" == "$dynamic_prefix"* ]]; then - word="${word#"$dynamic_prefix"}" - word="${word//$'\r'/ }" - word="${word//$'\n'/ }" - read -r -a candidates <<<"$word" - fi - - for candidate in "${candidates[@]}"; do - if ((!want_options)); then - [[ "${candidate:0:1}" == "-" ]] && continue + if ((!want_options)); then + [[ "${word:0:1}" == "-" ]] && continue - for u in "${used[@]}"; do - if [[ "$u" == "$candidate" ]]; then - continue 2 - fi - done - fi + for u in "${used[@]}"; do + if [[ "$u" == "$word" ]]; then + continue 2 + fi + done + fi - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v candidate '%q' "$candidate" - result+=("$candidate") - done + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") done echo "${result[*]}" @@ -70,6 +59,7 @@ % patterns.each do |pattern| % next if pattern.empty? <%= pattern.case_string %>) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen <%= pattern.compgen %> -- "$cur") ;; diff --git a/lib/completely/version.rb b/lib/completely/version.rb index 407dc87..caa939d 100644 --- a/lib/completely/version.rb +++ b/lib/completely/version.rb @@ -1,3 +1,3 @@ module Completely - VERSION = '0.7.4' + VERSION = '0.8.0.rc1' end diff --git a/spec/approvals/cli/generated-script b/spec/approvals/cli/generated-script index 270eff7..8ceca03 100644 --- a/spec/approvals/cli/generated-script +++ b/spec/approvals/cli/generated-script @@ -9,7 +9,6 @@ _mygit_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 - local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -21,29 +20,19 @@ _mygit_completions_filter() { # Completing a non-option: drop options and already-used words. [[ "${cur:0:1}" == "-" ]] && want_options=1 for word in "${words[@]}"; do - local candidates=("$word") - if [[ "$word" == "$dynamic_prefix"* ]]; then - word="${word#"$dynamic_prefix"}" - word="${word//$'\r'/ }" - word="${word//$'\n'/ }" - read -r -a candidates <<<"$word" + if ((!want_options)); then + [[ "${word:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$word" ]]; then + continue 2 + fi + done fi - for candidate in "${candidates[@]}"; do - if ((!want_options)); then - [[ "${candidate:0:1}" == "-" ]] && continue - - for u in "${used[@]}"; do - if [[ "$u" == "$candidate" ]]; then - continue 2 - fi - done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v candidate '%q' "$candidate" - result+=("$candidate") - done + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") done echo "${result[*]}" @@ -61,22 +50,27 @@ _mygit_completions() { case "$compline" in 'status'*'--branch') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") ;; 'status'*'-b') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") ;; 'status'*) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "--help" "--verbose" "--branch" "-b")" -- "$cur") ;; 'init'*) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mygit_completions_filter "--bare")" -- "$cur") ;; *) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "-h" "-v" "--help" "--version" "init" "status")" -- "$cur") ;; diff --git a/spec/approvals/cli/generated-script-alt b/spec/approvals/cli/generated-script-alt index db0ecca..e5ac987 100644 --- a/spec/approvals/cli/generated-script-alt +++ b/spec/approvals/cli/generated-script-alt @@ -9,7 +9,6 @@ _mycomps_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 - local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -21,29 +20,19 @@ _mycomps_filter() { # Completing a non-option: drop options and already-used words. [[ "${cur:0:1}" == "-" ]] && want_options=1 for word in "${words[@]}"; do - local candidates=("$word") - if [[ "$word" == "$dynamic_prefix"* ]]; then - word="${word#"$dynamic_prefix"}" - word="${word//$'\r'/ }" - word="${word//$'\n'/ }" - read -r -a candidates <<<"$word" + if ((!want_options)); then + [[ "${word:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$word" ]]; then + continue 2 + fi + done fi - for candidate in "${candidates[@]}"; do - if ((!want_options)); then - [[ "${candidate:0:1}" == "-" ]] && continue - - for u in "${used[@]}"; do - if [[ "$u" == "$candidate" ]]; then - continue 2 - fi - done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v candidate '%q' "$candidate" - result+=("$candidate") - done + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") done echo "${result[*]}" @@ -61,22 +50,27 @@ _mycomps() { case "$compline" in 'status'*'--branch') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") ;; 'status'*'-b') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") ;; 'status'*) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter "--help" "--verbose" "--branch" "-b")" -- "$cur") ;; 'init'*) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mycomps_filter "--bare")" -- "$cur") ;; *) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter "-h" "-v" "--help" "--version" "init" "status")" -- "$cur") ;; diff --git a/spec/approvals/cli/generated-wrapped-script b/spec/approvals/cli/generated-wrapped-script index da503c5..0770945 100644 --- a/spec/approvals/cli/generated-wrapped-script +++ b/spec/approvals/cli/generated-wrapped-script @@ -10,7 +10,6 @@ give_comps() { echo $' local cur=${COMP_WORDS[COMP_CWORD]}' echo $' local result=()' echo $' local want_options=0' - echo $' local dynamic_prefix="__completely_dynamic__"' echo $'' echo $' # words the user already typed (excluding the command itself)' echo $' local used=()' @@ -22,29 +21,19 @@ give_comps() { 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 $' local candidates=("$word")' - echo $' if [[ "$word" == "$dynamic_prefix"* ]]; then' - echo $' word="${word#"$dynamic_prefix"}"' - echo $' word="${word//$\'\r\'/ }"' - echo $' word="${word//$\'\n\'/ }"' - echo $' read -r -a candidates <<<"$word"' + echo $' if ((!want_options)); then' + echo $' [[ "${word:0:1}" == "-" ]] && continue' + echo $'' + echo $' for u in "${used[@]}"; do' + echo $' if [[ "$u" == "$word" ]]; then' + echo $' continue 2' + echo $' fi' + echo $' done' echo $' fi' echo $'' - echo $' for candidate in "${candidates[@]}"; do' - echo $' if ((!want_options)); then' - echo $' [[ "${candidate:0:1}" == "-" ]] && continue' - echo $'' - echo $' for u in "${used[@]}"; do' - echo $' if [[ "$u" == "$candidate" ]]; then' - echo $' continue 2' - echo $' fi' - echo $' done' - echo $' fi' - echo $'' - echo $' # compgen -W expects shell-escaped words in one space-delimited string.' - echo $' printf -v candidate \'%q\' "$candidate"' - echo $' result+=("$candidate")' - echo $' done' + 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[*]}"' @@ -62,22 +51,27 @@ give_comps() { echo $'' echo $' case "$compline" in' echo $' \'status\'*\'--branch\')' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format=\'%(refname:short)\' 2>/dev/null)")" -- "$cur")' + echo $' # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format=\'%(refname:short)\' 2>/dev/null))" -- "$cur")' echo $' ;;' echo $'' echo $' \'status\'*\'-b\')' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format=\'%(refname:short)\' 2>/dev/null)")" -- "$cur")' + echo $' # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format=\'%(refname:short)\' 2>/dev/null))" -- "$cur")' echo $' ;;' echo $'' echo $' \'status\'*)' + echo $' # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions' echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "--help" "--verbose" "--branch" "-b")" -- "$cur")' echo $' ;;' echo $'' echo $' \'init\'*)' + echo $' # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions' echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mygit_completions_filter "--bare")" -- "$cur")' echo $' ;;' echo $'' echo $' *)' + echo $' # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions' echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "-h" "-v" "--help" "--version" "init" "status")" -- "$cur")' echo $' ;;' echo $'' diff --git a/spec/approvals/cli/test/completely-tester-1.sh b/spec/approvals/cli/test/completely-tester-1.sh index 48b87e3..eb591ef 100644 --- a/spec/approvals/cli/test/completely-tester-1.sh +++ b/spec/approvals/cli/test/completely-tester-1.sh @@ -17,7 +17,6 @@ _mygit_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 - local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -29,29 +28,19 @@ _mygit_completions_filter() { # Completing a non-option: drop options and already-used words. [[ "${cur:0:1}" == "-" ]] && want_options=1 for word in "${words[@]}"; do - local candidates=("$word") - if [[ "$word" == "$dynamic_prefix"* ]]; then - word="${word#"$dynamic_prefix"}" - word="${word//$'\r'/ }" - word="${word//$'\n'/ }" - read -r -a candidates <<<"$word" + if ((!want_options)); then + [[ "${word:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$word" ]]; then + continue 2 + fi + done fi - for candidate in "${candidates[@]}"; do - if ((!want_options)); then - [[ "${candidate:0:1}" == "-" ]] && continue - - for u in "${used[@]}"; do - if [[ "$u" == "$candidate" ]]; then - continue 2 - fi - done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v candidate '%q' "$candidate" - result+=("$candidate") - done + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") done echo "${result[*]}" @@ -69,22 +58,27 @@ _mygit_completions() { case "$compline" in 'status'*'--branch') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") ;; 'status'*'-b') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") ;; 'status'*) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "--help" "--verbose" "--branch" "-b")" -- "$cur") ;; 'init'*) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mygit_completions_filter "--bare")" -- "$cur") ;; *) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "-h" "-v" "--help" "--version" "init" "status")" -- "$cur") ;; diff --git a/spec/approvals/cli/test/completely-tester-2.sh b/spec/approvals/cli/test/completely-tester-2.sh index 978816a..2fec8fe 100644 --- a/spec/approvals/cli/test/completely-tester-2.sh +++ b/spec/approvals/cli/test/completely-tester-2.sh @@ -17,7 +17,6 @@ _mygit_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 - local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -29,29 +28,19 @@ _mygit_completions_filter() { # Completing a non-option: drop options and already-used words. [[ "${cur:0:1}" == "-" ]] && want_options=1 for word in "${words[@]}"; do - local candidates=("$word") - if [[ "$word" == "$dynamic_prefix"* ]]; then - word="${word#"$dynamic_prefix"}" - word="${word//$'\r'/ }" - word="${word//$'\n'/ }" - read -r -a candidates <<<"$word" + if ((!want_options)); then + [[ "${word:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$word" ]]; then + continue 2 + fi + done fi - for candidate in "${candidates[@]}"; do - if ((!want_options)); then - [[ "${candidate:0:1}" == "-" ]] && continue - - for u in "${used[@]}"; do - if [[ "$u" == "$candidate" ]]; then - continue 2 - fi - done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v candidate '%q' "$candidate" - result+=("$candidate") - done + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") done echo "${result[*]}" @@ -69,22 +58,27 @@ _mygit_completions() { case "$compline" in 'status'*'--branch') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") ;; 'status'*'-b') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") ;; 'status'*) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "--help" "--verbose" "--branch" "-b")" -- "$cur") ;; 'init'*) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mygit_completions_filter "--bare")" -- "$cur") ;; *) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "-h" "-v" "--help" "--version" "init" "status")" -- "$cur") ;; diff --git a/spec/approvals/cli/test/completely-tester.sh b/spec/approvals/cli/test/completely-tester.sh index 0f42b58..76c486e 100644 --- a/spec/approvals/cli/test/completely-tester.sh +++ b/spec/approvals/cli/test/completely-tester.sh @@ -17,7 +17,6 @@ _mygit_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 - local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -29,29 +28,19 @@ _mygit_completions_filter() { # Completing a non-option: drop options and already-used words. [[ "${cur:0:1}" == "-" ]] && want_options=1 for word in "${words[@]}"; do - local candidates=("$word") - if [[ "$word" == "$dynamic_prefix"* ]]; then - word="${word#"$dynamic_prefix"}" - word="${word//$'\r'/ }" - word="${word//$'\n'/ }" - read -r -a candidates <<<"$word" + if ((!want_options)); then + [[ "${word:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$word" ]]; then + continue 2 + fi + done fi - for candidate in "${candidates[@]}"; do - if ((!want_options)); then - [[ "${candidate:0:1}" == "-" ]] && continue - - for u in "${used[@]}"; do - if [[ "$u" == "$candidate" ]]; then - continue 2 - fi - done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v candidate '%q' "$candidate" - result+=("$candidate") - done + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") done echo "${result[*]}" @@ -69,22 +58,27 @@ _mygit_completions() { case "$compline" in 'status'*'--branch') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") ;; 'status'*'-b') - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "__completely_dynamic__$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") ;; 'status'*) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "--help" "--verbose" "--branch" "-b")" -- "$cur") ;; 'init'*) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mygit_completions_filter "--bare")" -- "$cur") ;; *) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "-h" "-v" "--help" "--version" "init" "status")" -- "$cur") ;; diff --git a/spec/approvals/completions/function b/spec/approvals/completions/function index c6d1d04..788f661 100644 --- a/spec/approvals/completions/function +++ b/spec/approvals/completions/function @@ -10,7 +10,6 @@ send_completions() { echo $' local cur=${COMP_WORDS[COMP_CWORD]}' echo $' local result=()' echo $' local want_options=0' - echo $' local dynamic_prefix="__completely_dynamic__"' echo $'' echo $' # words the user already typed (excluding the command itself)' echo $' local used=()' @@ -22,29 +21,19 @@ send_completions() { 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 $' local candidates=("$word")' - echo $' if [[ "$word" == "$dynamic_prefix"* ]]; then' - echo $' word="${word#"$dynamic_prefix"}"' - echo $' word="${word//$\'\r\'/ }"' - echo $' word="${word//$\'\n\'/ }"' - echo $' read -r -a candidates <<<"$word"' - echo $' fi' - echo $'' - echo $' for candidate in "${candidates[@]}"; do' - echo $' if ((!want_options)); then' - echo $' [[ "${candidate:0:1}" == "-" ]] && continue' + echo $' if ((!want_options)); then' + echo $' [[ "${word:0:1}" == "-" ]] && continue' echo $'' - echo $' for u in "${used[@]}"; do' - echo $' if [[ "$u" == "$candidate" ]]; then' - echo $' continue 2' - echo $' fi' - echo $' done' - echo $' fi' + echo $' for u in "${used[@]}"; do' + echo $' if [[ "$u" == "$word" ]]; then' + echo $' continue 2' + echo $' fi' + echo $' done' + echo $' fi' echo $'' - echo $' # compgen -W expects shell-escaped words in one space-delimited string.' - echo $' printf -v candidate \'%q\' "$candidate"' - echo $' result+=("$candidate")' - echo $' done' + 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[*]}"' @@ -62,14 +51,17 @@ send_completions() { echo $'' echo $' case "$compline" in' echo $' \'generate\'*)' + echo $' # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions' echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_completely_completions_filter "--help" "--force")" -- "$cur")' echo $' ;;' echo $'' echo $' \'init\'*)' + echo $' # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions' echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_completely_completions_filter "--help")" -- "$cur")' echo $' ;;' echo $'' echo $' *)' + echo $' # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions' echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_completely_completions_filter "--help" "--version" "init" "generate")" -- "$cur")' echo $' ;;' echo $'' diff --git a/spec/approvals/completions/script b/spec/approvals/completions/script index 06641ed..1e34cc7 100644 --- a/spec/approvals/completions/script +++ b/spec/approvals/completions/script @@ -9,7 +9,6 @@ _completely_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 - local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -21,29 +20,19 @@ _completely_completions_filter() { # Completing a non-option: drop options and already-used words. [[ "${cur:0:1}" == "-" ]] && want_options=1 for word in "${words[@]}"; do - local candidates=("$word") - if [[ "$word" == "$dynamic_prefix"* ]]; then - word="${word#"$dynamic_prefix"}" - word="${word//$'\r'/ }" - word="${word//$'\n'/ }" - read -r -a candidates <<<"$word" - fi - - for candidate in "${candidates[@]}"; do - if ((!want_options)); then - [[ "${candidate:0:1}" == "-" ]] && continue + if ((!want_options)); then + [[ "${word:0:1}" == "-" ]] && continue - for u in "${used[@]}"; do - if [[ "$u" == "$candidate" ]]; then - continue 2 - fi - done - fi + for u in "${used[@]}"; do + if [[ "$u" == "$word" ]]; then + continue 2 + fi + done + fi - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v candidate '%q' "$candidate" - result+=("$candidate") - done + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") done echo "${result[*]}" @@ -61,14 +50,17 @@ _completely_completions() { case "$compline" in 'generate'*) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_completely_completions_filter "--help" "--force")" -- "$cur") ;; 'init'*) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_completely_completions_filter "--help")" -- "$cur") ;; *) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_completely_completions_filter "--help" "--version" "init" "generate")" -- "$cur") ;; diff --git a/spec/approvals/completions/script-complete-options b/spec/approvals/completions/script-complete-options index 6ecdc99..f81438e 100644 --- a/spec/approvals/completions/script-complete-options +++ b/spec/approvals/completions/script-complete-options @@ -9,7 +9,6 @@ _mygit_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 - local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -21,29 +20,19 @@ _mygit_completions_filter() { # Completing a non-option: drop options and already-used words. [[ "${cur:0:1}" == "-" ]] && want_options=1 for word in "${words[@]}"; do - local candidates=("$word") - if [[ "$word" == "$dynamic_prefix"* ]]; then - word="${word#"$dynamic_prefix"}" - word="${word//$'\r'/ }" - word="${word//$'\n'/ }" - read -r -a candidates <<<"$word" + if ((!want_options)); then + [[ "${word:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$word" ]]; then + continue 2 + fi + done fi - for candidate in "${candidates[@]}"; do - if ((!want_options)); then - [[ "${candidate:0:1}" == "-" ]] && continue - - for u in "${used[@]}"; do - if [[ "$u" == "$candidate" ]]; then - continue 2 - fi - done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v candidate '%q' "$candidate" - result+=("$candidate") - done + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") done echo "${result[*]}" @@ -61,6 +50,7 @@ _mygit_completions() { case "$compline" in *) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "status" "commit")" -- "$cur") ;; diff --git a/spec/approvals/completions/script-only-spaces b/spec/approvals/completions/script-only-spaces index 401453f..9c7c7fa 100644 --- a/spec/approvals/completions/script-only-spaces +++ b/spec/approvals/completions/script-only-spaces @@ -9,7 +9,6 @@ _completely_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 - local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -21,29 +20,19 @@ _completely_completions_filter() { # Completing a non-option: drop options and already-used words. [[ "${cur:0:1}" == "-" ]] && want_options=1 for word in "${words[@]}"; do - local candidates=("$word") - if [[ "$word" == "$dynamic_prefix"* ]]; then - word="${word#"$dynamic_prefix"}" - word="${word//$'\r'/ }" - word="${word//$'\n'/ }" - read -r -a candidates <<<"$word" - fi - - for candidate in "${candidates[@]}"; do - if ((!want_options)); then - [[ "${candidate:0:1}" == "-" ]] && continue + if ((!want_options)); then + [[ "${word:0:1}" == "-" ]] && continue - for u in "${used[@]}"; do - if [[ "$u" == "$candidate" ]]; then - continue 2 - fi - done - fi + for u in "${used[@]}"; do + if [[ "$u" == "$word" ]]; then + continue 2 + fi + done + fi - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v candidate '%q' "$candidate" - result+=("$candidate") - done + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") done echo "${result[*]}" @@ -61,10 +50,12 @@ _completely_completions() { case "$compline" in 'generate'*) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_completely_completions_filter "--help" "--force")" -- "$cur") ;; 'init'*) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_completely_completions_filter "--help")" -- "$cur") ;; diff --git a/spec/approvals/completions/script-with-debug b/spec/approvals/completions/script-with-debug index 688a963..7a3673c 100644 --- a/spec/approvals/completions/script-with-debug +++ b/spec/approvals/completions/script-with-debug @@ -9,7 +9,6 @@ _completely_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 - local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -21,29 +20,19 @@ _completely_completions_filter() { # Completing a non-option: drop options and already-used words. [[ "${cur:0:1}" == "-" ]] && want_options=1 for word in "${words[@]}"; do - local candidates=("$word") - if [[ "$word" == "$dynamic_prefix"* ]]; then - word="${word#"$dynamic_prefix"}" - word="${word//$'\r'/ }" - word="${word//$'\n'/ }" - read -r -a candidates <<<"$word" + if ((!want_options)); then + [[ "${word:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$word" ]]; then + continue 2 + fi + done fi - for candidate in "${candidates[@]}"; do - if ((!want_options)); then - [[ "${candidate:0:1}" == "-" ]] && continue - - for u in "${used[@]}"; do - if [[ "$u" == "$candidate" ]]; then - continue 2 - fi - done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v candidate '%q' "$candidate" - result+=("$candidate") - done + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") done echo "${result[*]}" diff --git a/spec/fixtures/tester/default.bash b/spec/fixtures/tester/default.bash index f31db9d..2b843ca 100644 --- a/spec/fixtures/tester/default.bash +++ b/spec/fixtures/tester/default.bash @@ -9,7 +9,6 @@ _cli_completions_filter() { local cur=${COMP_WORDS[COMP_CWORD]} local result=() local want_options=0 - local dynamic_prefix="__completely_dynamic__" # words the user already typed (excluding the command itself) local used=() @@ -21,29 +20,19 @@ _cli_completions_filter() { # Completing a non-option: drop options and already-used words. [[ "${cur:0:1}" == "-" ]] && want_options=1 for word in "${words[@]}"; do - local candidates=("$word") - if [[ "$word" == "$dynamic_prefix"* ]]; then - word="${word#"$dynamic_prefix"}" - word="${word//$'\r'/ }" - word="${word//$'\n'/ }" - read -r -a candidates <<<"$word" + if ((!want_options)); then + [[ "${word:0:1}" == "-" ]] && continue + + for u in "${used[@]}"; do + if [[ "$u" == "$word" ]]; then + continue 2 + fi + done fi - for candidate in "${candidates[@]}"; do - if ((!want_options)); then - [[ "${candidate:0:1}" == "-" ]] && continue - - for u in "${used[@]}"; do - if [[ "$u" == "$candidate" ]]; then - continue 2 - fi - done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v candidate '%q' "$candidate" - result+=("$candidate") - done + # compgen -W expects shell-escaped words in one space-delimited string. + printf -v word '%q' "$word" + result+=("$word") done echo "${result[*]}" @@ -61,18 +50,22 @@ _cli_completions() { case "$compline" in 'command childcommand'*) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--quiet" "--verbose" "-q" "-v")" -- "$cur") ;; 'command subcommand'*) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--force" "--quiet")" -- "$cur") ;; 'command'*) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "subcommand" "childcommand")" -- "$cur") ;; *) + # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_cli_completions_filter "--help" "--version" "command" "conquer")" -- "$cur") ;; From 8945d73abd8284863bf58452916fce550a842a89 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Fri, 20 Feb 2026 02:34:34 +0200 Subject: [PATCH 3/8] version 0.8.0.rc2 --- lib/completely/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/completely/version.rb b/lib/completely/version.rb index caa939d..bbe5efd 100644 --- a/lib/completely/version.rb +++ b/lib/completely/version.rb @@ -1,3 +1,3 @@ module Completely - VERSION = '0.8.0.rc1' + VERSION = '0.8.0.rc2' end From e3ad21d507968edc3998811b3b4fa8e4c06bdb91 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Fri, 20 Feb 2026 12:43:45 +0200 Subject: [PATCH 4/8] - Rollback space delimited handling --- .github/workflows/test.yml | 2 +- AGENTS.md | 69 ---------- CHANGELOG.md | 128 ++++++------------ Dockerfile | 2 +- README.md | 2 - lib/completely/commands/generate.rb | 2 +- lib/completely/installer.rb | 47 ++++--- lib/completely/pattern.rb | 20 +-- lib/completely/templates/template.erb | 32 ++--- lib/completely/version.rb | 2 +- spec/README.md | 25 +--- spec/approvals/cli/generated-script | 44 +++--- spec/approvals/cli/generated-script-alt | 44 +++--- spec/approvals/cli/generated-wrapped-script | 44 +++--- spec/approvals/cli/install/dry | 2 +- spec/approvals/cli/install/stdin-dry | 2 +- .../approvals/cli/test/completely-tester-1.sh | 44 +++--- .../approvals/cli/test/completely-tester-2.sh | 44 +++--- spec/approvals/cli/test/completely-tester.sh | 44 +++--- spec/approvals/cli/uninstall/dry | 2 +- spec/approvals/completions/function | 38 +++--- spec/approvals/completions/script | 38 +++--- .../completions/script-complete-options | 34 ++--- spec/approvals/completions/script-only-spaces | 35 +++-- spec/approvals/completions/script-with-debug | 31 +++-- spec/completely/commands/generate_spec.rb | 10 +- spec/completely/commands/install_spec.rb | 2 - spec/completely/installer_spec.rb | 96 ++++++------- spec/completely/integration.yml | 13 +- spec/completely/pattern_spec.rb | 12 +- spec/fixtures/integration/dynamic.txt | 2 - spec/fixtures/integration/dynamic.yaml | 3 - spec/fixtures/tester/default.bash | 43 +++--- 33 files changed, 384 insertions(+), 574 deletions(-) delete mode 100644 AGENTS.md delete mode 100644 spec/fixtures/integration/dynamic.txt delete mode 100644 spec/fixtures/integration/dynamic.yaml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f8c93a0..97286e6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: - matrix: { ruby: ['3.2', '3.3', '3.4', '4.0'] } + matrix: { ruby: ['3.2', '3.3', '3.4'] } steps: - name: Checkout code diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 252ce00..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,69 +0,0 @@ -# AGENTS.md - -Guidance for coding agents working in this repository. - -## Repo Snapshot - -- Project: `completely` (Ruby gem that generates Bash completion scripts from YAML). -- Key generation code: - - `lib/completely/pattern.rb` - - `lib/completely/templates/template.erb` -- Core behavior tests: - - `spec/completely/integration_spec.rb` - - `spec/completely/commands/generate_spec.rb` - -## Working Rules - -- Keep changes minimal and localized, especially in: - - completion-word serialization (`Pattern`) - - generated script runtime behavior (`template.erb`) -- Do not change generated approvals. -- Do not run approval prompts interactively on behalf of the developer. -- If an approval spec changes, stop and ask the developer to review/approve manually. -- Prefer adding regression coverage in integration fixtures for completion behavior changes. - -## Fast Validation Loop - -Run these first after edits: - -```bash -respec tagged script_quality -respec only integration -``` - -If touching quoting/escaping or dynamic completions, also run: - -```bash -respec only pattern -respec only completions -``` - -## Formatting and Linting Notes - -- `shellcheck` and `shfmt` requirements are enforced by specs tagged `:script_quality` in `spec/completely/commands/generate_spec.rb`. -- `shfmt` uses flags: - - `shfmt -d -i 2 -ci completely.bash` -- Small whitespace differences in heredoc/redirect forms (like `<<<"$x"` vs `<<< "$x"`) can fail shfmt. - -## Approval Specs - -- Some specs use `rspec_approvals` and may prompt interactively if output changes. -- In non-interactive runs this can fail with `Errno::ENOTTY`. -- Approval decisions are always developer-owned. Agents should not approve/update snapshots. - -## Completion Semantics to Preserve - -- Literal YAML words with spaces/quotes must complete correctly. -- Dynamic `$(...)` entries must produce multiple completion candidates when command output contains multiple words. -- ``, ``, and other `<...>` entries map to `compgen -A ...` actions and should remain unaffected by `-W` serialization changes. - -## Manual Repro Pattern - -Useful local sanity check: - -```bash -cd dev -ruby -I../lib ../bin/completely test "cli " -``` - -Expected: sensible mixed output for dynamic values and quoted/spaced literals. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e9b269..0cefcdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,203 +1,161 @@ Changelog ======================================== -v0.7.4 - 2026-02-19 ----------------------------------------- - -- Fix completion escaping [`dce16c3`](https://github.com/bashly-framework/completely/commit/dce16c3) -- Refactor template filter function [`834ed78`](https://github.com/bashly-framework/completely/commit/834ed78) -- Update completions installer paths [`338d567`](https://github.com/bashly-framework/completely/commit/338d567) -- Compare [`v0.7.3..v0.7.4`](https://github.com/bashly-framework/completely/compare/v0.7.3..v0.7.4) - - -v0.7.3 - 2025-09-24 ----------------------------------------- - -- Add support for reading config from stdin and writing to stdout [`764fb36`](https://github.com/bashly-framework/completely/commit/764fb36) -- Fix repeating final completion [`0029c4e`](https://github.com/bashly-framework/completely/commit/0029c4e) -- Refactor installer with new io / string builders [`cb91679`](https://github.com/bashly-framework/completely/commit/cb91679) -- Add support for `generate --install` [`c5a877e`](https://github.com/bashly-framework/completely/commit/c5a877e) -- Compare [`v0.7.2..v0.7.3`](https://github.com/bashly-framework/completely/compare/v0.7.2..v0.7.3) - - v0.7.2 - 2025-08-04 ---------------------------------------- -- Fix JSON schema [`3cc3ef6`](https://github.com/bashly-framework/completely/commit/3cc3ef6) -- Drop support for Ruby 3.0 and 3.1 [`7ff8bd8`](https://github.com/bashly-framework/completely/commit/7ff8bd8) -- Compare [`v0.7.1..v0.7.2`](https://github.com/bashly-framework/completely/compare/v0.7.1..v0.7.2) +- Fix JSON schema +- Drop support for Ruby 3.0 and 3.1 v0.7.1 - 2025-04-04 ---------------------------------------- -- Add support for modifying the `complete` command options [`690c264`](https://github.com/bashly-framework/completely/commit/690c264) -- Compare [`v0.7.0..v0.7.1`](https://github.com/bashly-framework/completely/compare/v0.7.0..v0.7.1) +- Add support for modifying the `complete` command options v0.7.0 - 2024-11-29 ---------------------------------------- -- Update instructions and template for obtaining list of git branches [`8cdfabd`](https://github.com/bashly-framework/completely/commit/8cdfabd) -- Add support for nested configuration [`742e3cd`](https://github.com/bashly-framework/completely/commit/742e3cd) -- Add `completely init --nested` and explain nested syntax in the README [`f1e17ed`](https://github.com/bashly-framework/completely/commit/f1e17ed) -- Compare [`v0.6.3..v0.7.0`](https://github.com/bashly-framework/completely/compare/v0.6.3..v0.7.0) +- Update instructions and template for obtaining list of git branches +- Add support for nested configuration +- Add `completely init --nested` and explain nested syntax in the README v0.6.3 - 2024-07-05 ---------------------------------------- -- Allow using colon, semicolon and equal sign in completions [`a9e6a6e`](https://github.com/bashly-framework/completely/commit/a9e6a6e) -- Check output with shfmt [`3f7ae7e`](https://github.com/bashly-framework/completely/commit/3f7ae7e) -- Revert wordbreak (colon) patch [`24f9d3d`](https://github.com/bashly-framework/completely/commit/24f9d3d) -- Compare [`v0.6.2..v0.6.3`](https://github.com/bashly-framework/completely/compare/v0.6.2..v0.6.3) +- Allow using colon, semicolon and equal sign in completions +- Check output with shfmt +- Revert wordbreak (colon) patch v0.6.2 - 2024-02-08 ---------------------------------------- -- Update possible completions installation directories [`2bc93a7`](https://github.com/bashly-framework/completely/commit/2bc93a7) -- Build docker images automatically [`4d20dfd`](https://github.com/bashly-framework/completely/commit/4d20dfd) -- Compare [`v0.6.1..v0.6.2`](https://github.com/bashly-framework/completely/compare/v0.6.1..v0.6.2) +- Update possible completions installation directories +- Build docker images automatically v0.6.1 - 2023-06-23 ---------------------------------------- -- Add ability to uninstall a completion script [`67b6715`](https://github.com/bashly-framework/completely/commit/67b6715) -- Compare [`v0.6.0..v0.6.1`](https://github.com/bashly-framework/completely/compare/v0.6.0..v0.6.1) +- Add ability to uninstall a completion script v0.6.0 - 2023-06-23 ---------------------------------------- -- Refactor install command and add an Installer model [`b1341fa`](https://github.com/bashly-framework/completely/commit/b1341fa) -- Drop support for Ruby 2.7 [`151eff1`](https://github.com/bashly-framework/completely/commit/151eff1) -- Change exception classes [`3ce16ac`](https://github.com/bashly-framework/completely/commit/3ce16ac) -- Compare [`v0.5.4..v0.6.0`](https://github.com/bashly-framework/completely/compare/v0.5.4..v0.6.0) +- Refactor install command and add an Installer model +- Drop support for Ruby 2.7 +- Change exception classes v0.5.4 - 2023-04-21 ---------------------------------------- -- Add `completely install` command [`2fbd879`](https://github.com/bashly-framework/completely/commit/2fbd879) -- Compare [`v0.5.3..v0.5.4`](https://github.com/bashly-framework/completely/compare/v0.5.3..v0.5.4) +- Add `completely install` command v0.5.3 - 2023-01-31 ---------------------------------------- -- Upgrade dependencies [`211166a`](https://github.com/bashly-framework/completely/commit/211166a) -- Compare [`v0.5.2..v0.5.3`](https://github.com/bashly-framework/completely/compare/v0.5.2..v0.5.3) +- Upgrade dependencies v0.5.2 - 2022-12-02 ---------------------------------------- -- Improve test command output and allow multiple complines in one run [`e924571`](https://github.com/bashly-framework/completely/commit/e924571) -- Compare [`v0.5.1..v0.5.2`](https://github.com/bashly-framework/completely/compare/v0.5.1..v0.5.2) +- Improve test command output and allow multiple complines in one run v0.5.1 - 2022-11-28 ---------------------------------------- -- Refactor with rubocop [`42b996d`](https://github.com/bashly-framework/completely/commit/42b996d) -- Fix broken script when wildcards follow the first word [`63b77d1`](https://github.com/bashly-framework/completely/commit/63b77d1) -- Show warning when running the test command on an invalid file [`de7ede0`](https://github.com/bashly-framework/completely/commit/de7ede0) -- Compare [`v0.5.0..v0.5.1`](https://github.com/bashly-framework/completely/compare/v0.5.0..v0.5.1) +- Refactor with rubocop +- Fix broken script when wildcards follow the first word +- Show warning when running the test command on an invalid file v0.5.0 - 2022-09-04 ---------------------------------------- -- Add docker release [`39acd6e`](https://github.com/bashly-framework/completely/commit/39acd6e) -- Fix shellcheck SC2162 in the generated script [`9e703ec`](https://github.com/bashly-framework/completely/commit/9e703ec) -- Fix shellcheck SC2124 in the generated script [`2d23c51`](https://github.com/bashly-framework/completely/commit/2d23c51) -- Hide flag completion unless input ends with a hyphen [`c15d705`](https://github.com/bashly-framework/completely/commit/c15d705) -- Compare [`v0.4.3..v0.5.0`](https://github.com/bashly-framework/completely/compare/v0.4.3..v0.5.0) +- Add docker release +- Fix shellcheck SC2162 in the generated script +- Fix shellcheck SC2124 in the generated script +- Hide flag completion unless input ends with a hyphen v0.4.3 - 2022-07-14 ---------------------------------------- -- Fix file/folder completion when they contain spaces [`9dea691`](https://github.com/bashly-framework/completely/commit/9dea691) -- Compare [`v0.4.2..v0.4.3`](https://github.com/bashly-framework/completely/compare/v0.4.2..v0.4.3) +- Fix file/folder completion when they contain spaces v0.4.2 - 2022-05-27 ---------------------------------------- -- Allow keeping the test script with --keep [`20d9b15`](https://github.com/bashly-framework/completely/commit/20d9b15) -- Compare [`v0.4.1..v0.4.2`](https://github.com/bashly-framework/completely/compare/v0.4.1..v0.4.2) +- Allow keeping the test script with --keep v0.4.1 - 2022-05-21 ---------------------------------------- -- Remove support for arbitrary script test to fix zsh incompatibilities [`9e3e6d9`](https://github.com/bashly-framework/completely/commit/9e3e6d9) -- Compare [`v0.4.0..v0.4.1`](https://github.com/bashly-framework/completely/compare/v0.4.0..v0.4.1) +- Remove support for arbitrary script test to fix zsh incompatibilities v0.4.0 - 2022-05-21 ---------------------------------------- -- Improve template [`8172be2`](https://github.com/bashly-framework/completely/commit/8172be2) -- Refactor CLI commands [`1fced36`](https://github.com/bashly-framework/completely/commit/1fced36) -- Add Tester class for testing any completions script [`986f4d1`](https://github.com/bashly-framework/completely/commit/986f4d1) -- Add tester CLI command [`09e91ee`](https://github.com/bashly-framework/completely/commit/09e91ee) -- Add support for middle wildcard for --flag args completions [`8d25207`](https://github.com/bashly-framework/completely/commit/8d25207) -- Add COMPLETELY_DEBUG environment setting [`44c00a1`](https://github.com/bashly-framework/completely/commit/44c00a1) -- Allow setting the CONFIG_PATH argument via the COMPLETELY_CONFIG_PATH environment variable [`8ef65e1`](https://github.com/bashly-framework/completely/commit/8ef65e1) -- Allow setting the SCRIPT_PATH argument via the COMPLETELY_SCRIPT_PATH environment variable [`a484ff4`](https://github.com/bashly-framework/completely/commit/a484ff4) -- Compare [`v0.3.1..v0.4.0`](https://github.com/bashly-framework/completely/compare/v0.3.1..v0.4.0) +- Improve template +- Refactor CLI commands +- Add Tester class for testing any completions script +- Add tester CLI command +- Add support for middle wildcard for --flag args completions +- Add COMPLETELY_DEBUG environment setting +- Allow setting the CONFIG_PATH argument via the COMPLETELY_CONFIG_PATH environment variable +- Allow setting the SCRIPT_PATH argument via the COMPLETELY_SCRIPT_PATH environment variable v0.3.1 - 2022-02-20 ---------------------------------------- -- Fix Psych 4 errors for Ruby 3.1 [`2fb9a73`](https://github.com/bashly-framework/completely/commit/2fb9a73) -- Compare [`v0.3.0..v0.3.1`](https://github.com/bashly-framework/completely/compare/v0.3.0..v0.3.1) +- Fix Psych 4 errors for Ruby 3.1 v0.3.0 - 2022-01-28 ---------------------------------------- -- Fix generated script for zsh compatibility [`d19369b`](https://github.com/bashly-framework/completely/commit/d19369b) -- Compare [`v0.2.0..v0.3.0`](https://github.com/bashly-framework/completely/compare/v0.2.0..v0.3.0) +- Fix generated script for zsh compatibility v0.2.0 - 2021-09-03 ---------------------------------------- -- Improve generated code to support local completions [`3518434`](https://github.com/bashly-framework/completely/commit/3518434) -- Compare [`v0.1.3..v0.2.0`](https://github.com/bashly-framework/completely/compare/v0.1.3..v0.2.0) +- Improve generated code to support local completions v0.1.3 - 2021-07-21 ---------------------------------------- -- Fix function name when only spaced patterns are configured [`2e14ec2`](https://github.com/bashly-framework/completely/commit/2e14ec2) -- Compare [`v0.1.2..v0.1.3`](https://github.com/bashly-framework/completely/compare/v0.1.2..v0.1.3) +- Fix function name when only spaced patterns are configured v0.1.2 - 2021-07-20 ---------------------------------------- -- Add ability to generate a function that prints the script [`22de124`](https://github.com/bashly-framework/completely/commit/22de124) -- Compare [`v0.1.1..v0.1.2`](https://github.com/bashly-framework/completely/compare/v0.1.1..v0.1.2) +- Add ability to generate a function that prints the script v0.1.1 - 2021-07-20 ---------------------------------------- -- Fix missing VERSION error [`e6f0ac1`](https://github.com/bashly-framework/completely/commit/e6f0ac1) -- Compare [`v0.1.0..v0.1.1`](https://github.com/bashly-framework/completely/compare/v0.1.0..v0.1.1) +- Fix missing VERSION error v0.1.0 - 2021-07-20 ---------------------------------------- -- Initial version [`bcd598c`](https://github.com/bashly-framework/completely/commit/bcd598c) -- Compare [`v0.1.0`](https://github.com/bashly-framework/completely/compare/v0.1.0) +- Initial version diff --git a/Dockerfile b/Dockerfile index 554eb3d..4345097 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,6 @@ FROM dannyben/alpine-ruby:3.3.3 ENV PS1="\n\n>> completely \W \$ " WORKDIR /app -RUN gem install completely --version 0.7.4 +RUN gem install completely --version 0.7.3 ENTRYPOINT ["completely"] \ No newline at end of file diff --git a/README.md b/README.md index c35d6db..e85942d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # Completely - Bash Completions Generator -![repocard](https://repocard.dannyben.com/svg/completely.svg) - Completely is a command line utility and a Ruby library that lets you generate bash completion scripts from simple YAML configuration. diff --git a/lib/completely/commands/generate.rb b/lib/completely/commands/generate.rb index fb013ff..091bd06 100644 --- a/lib/completely/commands/generate.rb +++ b/lib/completely/commands/generate.rb @@ -60,7 +60,7 @@ def install(content) end def show(content) = puts content - + def save(content) File.write output_path, content say "Saved m`#{output_path}`" diff --git a/lib/completely/installer.rb b/lib/completely/installer.rb index f1d2978..f4eebdf 100644 --- a/lib/completely/installer.rb +++ b/lib/completely/installer.rb @@ -1,5 +1,3 @@ -require 'fileutils' - module Completely class Installer class << self @@ -40,8 +38,18 @@ def initialize(program:, script_path: nil) @script_path = script_path end + def target_directories + @target_directories ||= %W[ + /usr/share/bash-completion/completions + /usr/local/etc/bash_completion.d + #{Dir.home}/.local/share/bash-completion/completions + #{Dir.home}/.bash_completion.d + ] + end + def install_command - %W[cp #{script_path} #{target_path}] + result = root_user? ? [] : %w[sudo] + result + %W[cp #{script_path} #{target_path}] end def install_command_string @@ -49,7 +57,8 @@ def install_command_string end def uninstall_command - %W[rm -f #{target_path}] + result = root_user? ? [] : %w[sudo] + result + %w[rm -f] + target_directories.map { |dir| "#{dir}/#{program}" } end def uninstall_command_string @@ -61,12 +70,14 @@ def target_path end def install(force: false) + unless completions_path + raise InstallError, 'Cannot determine system completions directory' + end + unless script_exist? raise InstallError, "Cannot find script: m`#{script_path}`" end - FileUtils.mkdir_p completions_path - if target_exist? && !force raise InstallError, "File exists: m`#{target_path}`" end @@ -88,20 +99,22 @@ def script_exist? File.exist? script_path end - def completions_path - @completions_path ||= "#{user_completions_base_dir}/completions" + def root_user? + Process.uid.zero? end - def user_completions_base_dir - @user_completions_base_dir ||= bash_completion_user_dir || "#{data_home}/bash-completion" - end - - def bash_completion_user_dir - ENV['BASH_COMPLETION_USER_DIR']&.split(':')&.find { |entry| !entry.empty? } - end + def completions_path + @completions_path ||= begin + result = nil + target_directories.each do |target| + if Dir.exist? target + result = target + break + end + end - def data_home - ENV['XDG_DATA_HOME'] || "#{Dir.home}/.local/share" + result + end end end end diff --git a/lib/completely/pattern.rb b/lib/completely/pattern.rb index 92333a2..a276a29 100644 --- a/lib/completely/pattern.rb +++ b/lib/completely/pattern.rb @@ -54,26 +54,8 @@ def compgen def compgen! result = [] result << actions.join(' ').to_s if actions.any? - result << %[-W "$(#{function_name} #{serialized_words.join ' '})"] if words.any? + result << %[-W "$(#{function_name} "#{words.join ' '}")"] if words.any? result.any? ? result.join(' ') : nil end - - def serialized_words - @serialized_words ||= words.map { |word| serialize_word(word) } - end - - def serialize_word(word) - return word if dynamic_word?(word) - - %("#{escape_for_double_quotes word}") - end - - def dynamic_word?(word) - word.match?(/\A\$\(.+\)\z/) - 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 45bb795..b5dfbe4 100644 --- a/lib/completely/templates/template.erb +++ b/lib/completely/templates/template.erb @@ -5,10 +5,9 @@ # Modifying it manually is not recommended <%= function_name %>_filter() { - local words=("$@") + local words="$1" local cur=${COMP_WORDS[COMP_CWORD]} local result=() - local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -16,26 +15,28 @@ used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - # 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 + 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 [[ "${word:0:1}" == "-" ]] && continue + local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - continue 2 + seen=1 + break fi done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") - done + ((!seen)) && result+=("$word") + done - echo "${result[*]}" + echo "${result[*]}" + fi } <%= function_name %>() { @@ -59,7 +60,6 @@ % patterns.each do |pattern| % next if pattern.empty? <%= pattern.case_string %>) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen <%= pattern.compgen %> -- "$cur") ;; diff --git a/lib/completely/version.rb b/lib/completely/version.rb index bbe5efd..026d7c7 100644 --- a/lib/completely/version.rb +++ b/lib/completely/version.rb @@ -1,3 +1,3 @@ module Completely - VERSION = '0.8.0.rc2' + VERSION = '0.7.3' end diff --git a/spec/README.md b/spec/README.md index c00362f..25343ef 100644 --- a/spec/README.md +++ b/spec/README.md @@ -2,29 +2,19 @@ ## Running tests -You can run specs with `rspec` as usual. - -We recommend using [`respec`][2], which wraps common spec workflows: - ```bash -rspec +$ rspec # or -respec +$ run spec +# or, to run just tests in a given file +$ run spec zsh +# or, to run just specs tagged with :focus +$ run spec :focus ``` You might need to prefix the commands with `bundle exec`, depending on the way Ruby is installed. -Useful helper shortcuts: - -```bash -# script quality checks (shellcheck + shfmt generated script tests) -respec tagged script_quality - -# integration behavior suite -respec only integration -``` - ## Interactive Approvals Some tests may prompt you for an interactive approval of changes. This @@ -39,5 +29,4 @@ ZSH compatibility test is done by running the completely tester script inside a zsh container. This is all done automatically by `spec/completely/zsh_spec.rb`. -[1]: https://github.com/dannyben/rspec_approvals -[2]: https://github.com/DannyBen/respec +[1]: https://github.com/dannyben/rspec_approvals \ No newline at end of file diff --git a/spec/approvals/cli/generated-script b/spec/approvals/cli/generated-script index 8ceca03..94e256e 100644 --- a/spec/approvals/cli/generated-script +++ b/spec/approvals/cli/generated-script @@ -5,10 +5,9 @@ # Modifying it manually is not recommended _mygit_completions_filter() { - local words=("$@") + local words="$1" local cur=${COMP_WORDS[COMP_CWORD]} local result=() - local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -16,26 +15,28 @@ _mygit_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - # 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 + 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 [[ "${word:0:1}" == "-" ]] && continue + local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - continue 2 + seen=1 + break fi done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") - done + ((!seen)) && result+=("$word") + done - echo "${result[*]}" + echo "${result[*]}" + fi } _mygit_completions() { @@ -50,28 +51,23 @@ _mygit_completions() { case "$compline" in 'status'*'--branch') - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*'-b') - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - 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'*) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mygit_completions_filter "--bare")" -- "$cur") ;; *) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) 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 e5ac987..217aae4 100644 --- a/spec/approvals/cli/generated-script-alt +++ b/spec/approvals/cli/generated-script-alt @@ -5,10 +5,9 @@ # Modifying it manually is not recommended _mycomps_filter() { - local words=("$@") + local words="$1" local cur=${COMP_WORDS[COMP_CWORD]} local result=() - local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -16,26 +15,28 @@ _mycomps_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - # 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 + 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 [[ "${word:0:1}" == "-" ]] && continue + local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - continue 2 + seen=1 + break fi done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") - done + ((!seen)) && result+=("$word") + done - echo "${result[*]}" + echo "${result[*]}" + fi } _mycomps() { @@ -50,28 +51,23 @@ _mycomps() { case "$compline" in 'status'*'--branch') - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*'-b') - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mycomps_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - 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'*) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mycomps_filter "--bare")" -- "$cur") ;; *) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - 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 0770945..e37ade0 100644 --- a/spec/approvals/cli/generated-wrapped-script +++ b/spec/approvals/cli/generated-wrapped-script @@ -6,10 +6,9 @@ give_comps() { echo $'# Modifying it manually is not recommended' echo $'' echo $'_mygit_completions_filter() {' - echo $' local words=("$@")' + echo $' local words="$1"' 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=()' @@ -17,26 +16,28 @@ give_comps() { echo $' used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' echo $' fi' echo $'' - 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 $' 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 $' [[ "${word:0:1}" == "-" ]] && continue' echo $'' + echo $' local seen=0' echo $' for u in "${used[@]}"; do' echo $' if [[ "$u" == "$word" ]]; then' - echo $' continue 2' + echo $' seen=1' + echo $' break' echo $' fi' echo $' done' - echo $' fi' - echo $'' - echo $' # compgen -W expects shell-escaped words in one space-delimited string.' - echo $' printf -v word \'%q\' "$word"' - echo $' result+=("$word")' - echo $' done' + echo $' ((!seen)) && result+=("$word")' + echo $' done' echo $'' - echo $' echo "${result[*]}"' + echo $' echo "${result[*]}"' + echo $' fi' echo $'}' echo $'' echo $'_mygit_completions() {' @@ -51,28 +52,23 @@ give_comps() { echo $'' echo $' case "$compline" in' echo $' \'status\'*\'--branch\')' - echo $' # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format=\'%(refname:short)\' 2>/dev/null))" -- "$cur")' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format=\'%(refname:short)\' 2>/dev/null)")" -- "$cur")' echo $' ;;' echo $'' echo $' \'status\'*\'-b\')' - echo $' # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions' - echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format=\'%(refname:short)\' 2>/dev/null))" -- "$cur")' + echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format=\'%(refname:short)\' 2>/dev/null)")" -- "$cur")' echo $' ;;' echo $'' echo $' \'status\'*)' - echo $' # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions' - 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\'*)' - echo $' # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions' echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mygit_completions_filter "--bare")" -- "$cur")' echo $' ;;' echo $'' echo $' *)' - echo $' # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions' - 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/install/dry b/spec/approvals/cli/install/dry index 3a4ba7e..02bae48 100644 --- a/spec/approvals/cli/install/dry +++ b/spec/approvals/cli/install/dry @@ -1 +1 @@ -cp completely.bash /home/USER/.local/share/bash-completion/completions/completely-test +sudo cp completely.bash /usr/share/bash-completion/completions/completely-test diff --git a/spec/approvals/cli/install/stdin-dry b/spec/approvals/cli/install/stdin-dry index cfbfd0a..47144da 100644 --- a/spec/approvals/cli/install/stdin-dry +++ b/spec/approvals/cli/install/stdin-dry @@ -1 +1 @@ -cp /home/USER/.local/share/bash-completion/completions/completely-test +sudo cp /usr/share/bash-completion/completions/completely-test diff --git a/spec/approvals/cli/test/completely-tester-1.sh b/spec/approvals/cli/test/completely-tester-1.sh index eb591ef..50d5b48 100644 --- a/spec/approvals/cli/test/completely-tester-1.sh +++ b/spec/approvals/cli/test/completely-tester-1.sh @@ -13,10 +13,9 @@ fi # Modifying it manually is not recommended _mygit_completions_filter() { - local words=("$@") + local words="$1" local cur=${COMP_WORDS[COMP_CWORD]} local result=() - local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -24,26 +23,28 @@ _mygit_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - # 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 + 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 [[ "${word:0:1}" == "-" ]] && continue + local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - continue 2 + seen=1 + break fi done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") - done + ((!seen)) && result+=("$word") + done - echo "${result[*]}" + echo "${result[*]}" + fi } _mygit_completions() { @@ -58,28 +59,23 @@ _mygit_completions() { case "$compline" in 'status'*'--branch') - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*'-b') - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - 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'*) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mygit_completions_filter "--bare")" -- "$cur") ;; *) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) 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 2fec8fe..a0fb1e3 100644 --- a/spec/approvals/cli/test/completely-tester-2.sh +++ b/spec/approvals/cli/test/completely-tester-2.sh @@ -13,10 +13,9 @@ fi # Modifying it manually is not recommended _mygit_completions_filter() { - local words=("$@") + local words="$1" local cur=${COMP_WORDS[COMP_CWORD]} local result=() - local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -24,26 +23,28 @@ _mygit_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - # 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 + 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 [[ "${word:0:1}" == "-" ]] && continue + local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - continue 2 + seen=1 + break fi done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") - done + ((!seen)) && result+=("$word") + done - echo "${result[*]}" + echo "${result[*]}" + fi } _mygit_completions() { @@ -58,28 +59,23 @@ _mygit_completions() { case "$compline" in 'status'*'--branch') - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*'-b') - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - 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'*) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mygit_completions_filter "--bare")" -- "$cur") ;; *) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) 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 76c486e..108dbf9 100644 --- a/spec/approvals/cli/test/completely-tester.sh +++ b/spec/approvals/cli/test/completely-tester.sh @@ -13,10 +13,9 @@ fi # Modifying it manually is not recommended _mygit_completions_filter() { - local words=("$@") + local words="$1" local cur=${COMP_WORDS[COMP_CWORD]} local result=() - local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -24,26 +23,28 @@ _mygit_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - # 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 + 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 [[ "${word:0:1}" == "-" ]] && continue + local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - continue 2 + seen=1 + break fi done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") - done + ((!seen)) && result+=("$word") + done - echo "${result[*]}" + echo "${result[*]}" + fi } _mygit_completions() { @@ -58,28 +59,23 @@ _mygit_completions() { case "$compline" in 'status'*'--branch') - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*'-b') - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter $(git branch --format='%(refname:short)' 2>/dev/null))" -- "$cur") + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_mygit_completions_filter "$(git branch --format='%(refname:short)' 2>/dev/null)")" -- "$cur") ;; 'status'*) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - 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'*) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mygit_completions_filter "--bare")" -- "$cur") ;; *) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) 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/uninstall/dry b/spec/approvals/cli/uninstall/dry index c9791f4..d582874 100644 --- a/spec/approvals/cli/uninstall/dry +++ b/spec/approvals/cli/uninstall/dry @@ -1 +1 @@ -rm -f /home/USER/.local/share/bash-completion/completions/completely-test +sudo rm -f /usr/share/bash-completion/completions/completely-test /usr/local/etc/bash_completion.d/completely-test /home/vagrant/.local/share/bash-completion/completion/completely-test /home/vagrant/.bash_completion.d/completely-test diff --git a/spec/approvals/completions/function b/spec/approvals/completions/function index 788f661..1b31b34 100644 --- a/spec/approvals/completions/function +++ b/spec/approvals/completions/function @@ -6,10 +6,9 @@ send_completions() { echo $'# Modifying it manually is not recommended' echo $'' echo $'_completely_completions_filter() {' - echo $' local words=("$@")' + echo $' local words="$1"' 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=()' @@ -17,26 +16,28 @@ send_completions() { echo $' used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")' echo $' fi' echo $'' - 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 $' 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 $' [[ "${word:0:1}" == "-" ]] && continue' echo $'' + echo $' local seen=0' echo $' for u in "${used[@]}"; do' echo $' if [[ "$u" == "$word" ]]; then' - echo $' continue 2' + echo $' seen=1' + echo $' break' echo $' fi' echo $' done' - echo $' fi' - echo $'' - echo $' # compgen -W expects shell-escaped words in one space-delimited string.' - echo $' printf -v word \'%q\' "$word"' - echo $' result+=("$word")' - echo $' done' + echo $' ((!seen)) && result+=("$word")' + echo $' done' echo $'' - echo $' echo "${result[*]}"' + echo $' echo "${result[*]}"' + echo $' fi' echo $'}' echo $'' echo $'_completely_completions() {' @@ -51,18 +52,15 @@ send_completions() { echo $'' echo $' case "$compline" in' echo $' \'generate\'*)' - echo $' # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions' - 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\'*)' - echo $' # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions' echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_completely_completions_filter "--help")" -- "$cur")' echo $' ;;' echo $'' echo $' *)' - echo $' # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions' - 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 1e34cc7..cba794b 100644 --- a/spec/approvals/completions/script +++ b/spec/approvals/completions/script @@ -5,10 +5,9 @@ # Modifying it manually is not recommended _completely_completions_filter() { - local words=("$@") + local words="$1" local cur=${COMP_WORDS[COMP_CWORD]} local result=() - local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -16,26 +15,28 @@ _completely_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - # 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 + 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 [[ "${word:0:1}" == "-" ]] && continue + local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - continue 2 + seen=1 + break fi done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") - done + ((!seen)) && result+=("$word") + done - echo "${result[*]}" + echo "${result[*]}" + fi } _completely_completions() { @@ -50,18 +51,15 @@ _completely_completions() { case "$compline" in 'generate'*) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - 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'*) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_completely_completions_filter "--help")" -- "$cur") ;; *) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) 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 f81438e..a0dff27 100644 --- a/spec/approvals/completions/script-complete-options +++ b/spec/approvals/completions/script-complete-options @@ -5,10 +5,9 @@ # Modifying it manually is not recommended _mygit_completions_filter() { - local words=("$@") + local words="$1" local cur=${COMP_WORDS[COMP_CWORD]} local result=() - local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -16,26 +15,28 @@ _mygit_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - # 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 + 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 [[ "${word:0:1}" == "-" ]] && continue + local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - continue 2 + seen=1 + break fi done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") - done + ((!seen)) && result+=("$word") + done - echo "${result[*]}" + echo "${result[*]}" + fi } _mygit_completions() { @@ -50,8 +51,7 @@ _mygit_completions() { case "$compline" in *) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - 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 9c7c7fa..44695a6 100644 --- a/spec/approvals/completions/script-only-spaces +++ b/spec/approvals/completions/script-only-spaces @@ -5,10 +5,9 @@ # Modifying it manually is not recommended _completely_completions_filter() { - local words=("$@") + local words="$1" local cur=${COMP_WORDS[COMP_CWORD]} local result=() - local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -16,26 +15,28 @@ _completely_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - # 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 + 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 [[ "${word:0:1}" == "-" ]] && continue + local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - continue 2 + seen=1 + break fi done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") - done + ((!seen)) && result+=("$word") + done - echo "${result[*]}" + echo "${result[*]}" + fi } _completely_completions() { @@ -50,12 +51,10 @@ _completely_completions() { case "$compline" in 'generate'*) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - 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'*) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(_completely_completions_filter "--help")" -- "$cur") ;; diff --git a/spec/approvals/completions/script-with-debug b/spec/approvals/completions/script-with-debug index 7a3673c..afc0053 100644 --- a/spec/approvals/completions/script-with-debug +++ b/spec/approvals/completions/script-with-debug @@ -5,10 +5,9 @@ # Modifying it manually is not recommended _completely_completions_filter() { - local words=("$@") + local words="$1" local cur=${COMP_WORDS[COMP_CWORD]} local result=() - local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -16,26 +15,28 @@ _completely_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - # 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 + 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 [[ "${word:0:1}" == "-" ]] && continue + local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - continue 2 + seen=1 + break fi done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") - done + ((!seen)) && result+=("$word") + done - echo "${result[*]}" + echo "${result[*]}" + fi } _completely_completions() { diff --git a/spec/completely/commands/generate_spec.rb b/spec/completely/commands/generate_spec.rb index 65eee8e..fb6a198 100644 --- a/spec/completely/commands/generate_spec.rb +++ b/spec/completely/commands/generate_spec.rb @@ -22,12 +22,12 @@ expect(File.read 'completely.bash').to match_approval('cli/generated-script') end - it 'generates a shellcheck compliant script', :script_quality do + it 'generates a shellcheck compliant script' do expect { subject.execute %w[generate] }.to output_approval('cli/generate/no-args') expect(`shellcheck completely.bash 2>&1`).to be_empty end - it 'generates a shfmt compliant script', :script_quality do + it 'generates a shfmt compliant script' do expect { subject.execute %w[generate] }.to output_approval('cli/generate/no-args') expect(`shfmt -d -i 2 -ci completely.bash 2>&1`).to be_empty end @@ -119,16 +119,16 @@ context 'with --install PROGRAM' do let(:mock_installer) do instance_double Installer, - install: true, + install: true, install_command_string: 'stubbed install_command_string', - target_path: 'stubbed target_path' + target_path: 'stubbed target_path' end it 'passes the generated script to the installer' do allow(Installer).to receive(:from_string) .with( program: 'mycli', - string: a_string_matching(/bash completions script/) + string: a_string_matching(/bash completions script/) ).and_return(mock_installer) expect(mock_installer).to receive(:install) diff --git a/spec/completely/commands/install_spec.rb b/spec/completely/commands/install_spec.rb index 6e2a93f..014b476 100644 --- a/spec/completely/commands/install_spec.rb +++ b/spec/completely/commands/install_spec.rb @@ -52,7 +52,6 @@ expect { subject.execute %w[install completely-test --dry] } .to output_approval('cli/install/dry') - .except(%r[/home/([^/]+)], '/home/USER') end end @@ -65,7 +64,6 @@ expect { subject.execute %w[install completely-test - --dry] } .to output_approval('cli/install/stdin-dry') .except(/cp [^\s]*completely-[^\s]*/, 'cp ') - .except(%r[/home/([^/]+)], '/home/USER') end end diff --git a/spec/completely/installer_spec.rb b/spec/completely/installer_spec.rb index bf0a67b..f74f6fc 100644 --- a/spec/completely/installer_spec.rb +++ b/spec/completely/installer_spec.rb @@ -4,9 +4,14 @@ let(:leeway) { RUBY_VERSION < '3.2.0' ? 0 : 3 } let(:program) { 'completely-test' } let(:script_path) { 'completions.bash' } - let(:target_path) { "#{Dir.home}/.local/share/bash-completion/completions/#{program}" } - let(:install_command) { %W[cp #{subject.script_path} #{subject.target_path}] } - let(:uninstall_command) { %W[rm -f #{subject.target_path}] } + let(:targets) { subject.target_directories.map { |dir| "#{dir}/#{program}" } } + let(:install_command) do + %W[sudo cp #{subject.script_path} #{subject.target_path}] + end + + let(:uninstall_command) do + %w[sudo rm -f] + targets + end describe '::from_io' do subject { described_class.from_io program:, io: } @@ -28,58 +33,33 @@ end end - describe '#target_path' do - it 'returns a user-level target path' do - expect(subject.target_path).to eq target_path - end - - context 'when BASH_COMPLETION_USER_DIR is set' do - around do |example| - original = ENV['BASH_COMPLETION_USER_DIR'] - ENV['BASH_COMPLETION_USER_DIR'] = '/tmp/completely-user-dir' - example.run - ensure - ENV['BASH_COMPLETION_USER_DIR'] = original - end - - it 'uses BASH_COMPLETION_USER_DIR/completions' do - expect(subject.target_path).to eq '/tmp/completely-user-dir/completions/completely-test' - end + describe '#target_directories' do + it 'returns an array of potential completion directories' do + expect(subject.target_directories).to be_an Array + expect(subject.target_directories.size).to eq 4 end + end - context 'when XDG_DATA_HOME is set' do - around do |example| - original = ENV['XDG_DATA_HOME'] - ENV['XDG_DATA_HOME'] = '/tmp/completely-xdg' - example.run - ensure - ENV['XDG_DATA_HOME'] = original - end - - it 'uses XDG_DATA_HOME/bash-completion/completions' do - expect(subject.target_path).to eq '/tmp/completely-xdg/bash-completion/completions/completely-test' - end - end - - context 'when BASH_COMPLETION_USER_DIR has multiple entries' do - around do |example| - original = ENV['BASH_COMPLETION_USER_DIR'] - ENV['BASH_COMPLETION_USER_DIR'] = ':/tmp/completely-first:/tmp/completely-second' - example.run - ensure - ENV['BASH_COMPLETION_USER_DIR'] = original - end - - it 'uses the first non-empty entry' do - expect(subject.target_path).to eq '/tmp/completely-first/completions/completely-test' - end + describe '#target_path' do + it 'returns the first matching path' do + expect(subject.target_path) + .to eq '/usr/share/bash-completion/completions/completely-test' end end describe '#install_command' do it 'returns a copy command as an array' do expect(subject.install_command) - .to eq %W[cp completions.bash #{target_path}] + .to eq %w[sudo cp completions.bash /usr/share/bash-completion/completions/completely-test] + end + + context 'when the user is root' do + it 'returns the command without sudo' do + allow(subject).to receive(:root_user?).and_return true + + expect(subject.install_command) + .to eq %w[cp completions.bash /usr/share/bash-completion/completions/completely-test] + end end end @@ -91,7 +71,15 @@ describe '#uninstall_command' do it 'returns an rm command as an array' do - expect(subject.uninstall_command).to eq %W[rm -f #{target_path}] + expect(subject.uninstall_command).to eq %w[sudo rm -f] + targets + end + + context 'when the user is root' do + it 'returns the command without sudo' do + allow(subject).to receive(:root_user?).and_return true + + expect(subject.uninstall_command).to eq %w[rm -f] + targets + end end end @@ -107,7 +95,15 @@ before do allow(subject).to receive_messages(script_path: existing_file, target_path: missing_file) - allow(FileUtils).to receive(:mkdir_p) + end + + context 'when the completions_path cannot be found' do + it 'raises an error' do + allow(subject).to receive(:completions_path).and_return nil + + expect { subject.install }.to raise_approval('installer/install-no-dir') + .diff(leeway) + end end context 'when the script cannot be found' do @@ -132,7 +128,6 @@ it 'proceeds to install' do allow(subject).to receive(:target_path).and_return existing_file - expect(FileUtils).to receive(:mkdir_p) expect(subject).to receive(:system).with(*install_command) subject.install force: true @@ -143,7 +138,6 @@ it 'proceeds to install' do allow(subject).to receive(:target_path).and_return missing_file - expect(FileUtils).to receive(:mkdir_p) expect(subject).to receive(:system).with(*install_command) subject.install diff --git a/spec/completely/integration.yml b/spec/completely/integration.yml index 05f3b53..eb90815 100644 --- a/spec/completely/integration.yml +++ b/spec/completely/integration.yml @@ -9,7 +9,7 @@ ftp: expected: [download] - compline: "ftp download " - expected: [another-dir, dir with spaces, dummy-dir, dynamic.txt, dynamic.yaml, file with spaces.txt, ftp.yaml, gradual.yaml, wildcard.yaml] + expected: [another-dir, dir with spaces, dummy-dir, file with spaces.txt, ftp.yaml, gradual.yaml, wildcard.yaml] - compline: "ftp download -" expected: [--help, --override] @@ -29,16 +29,6 @@ ftp: - compline: "/anything/goes/ftp list -" expected: [--help, --short] -dynamic: -- compline: "dynamic " - expected: [foo bar, hello, help] - -- compline: "dynamic h" - expected: [hello, help] - -- compline: "dynamic f" - expected: [foo bar] - gradual: - compline: "cli " expected: [command, conquer] @@ -103,3 +93,4 @@ wildcard: - compline: "wildcard download --contest " expected: [everything, nothing] + diff --git a/spec/completely/pattern_spec.rb b/spec/completely/pattern_spec.rb index 93952a4..912ed3e 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,15 +120,7 @@ let(:completions) { %w[--message --help] } it 'omits the -A arguments' do - 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")"' + expect(subject.compgen).to eq '-W "$(_filter "--message --help")"' end end diff --git a/spec/fixtures/integration/dynamic.txt b/spec/fixtures/integration/dynamic.txt deleted file mode 100644 index da2146a..0000000 --- a/spec/fixtures/integration/dynamic.txt +++ /dev/null @@ -1,2 +0,0 @@ -hello -help diff --git a/spec/fixtures/integration/dynamic.yaml b/spec/fixtures/integration/dynamic.yaml deleted file mode 100644 index c677ab1..0000000 --- a/spec/fixtures/integration/dynamic.yaml +++ /dev/null @@ -1,3 +0,0 @@ -dynamic: -- $(cat dynamic.txt) -- foo bar diff --git a/spec/fixtures/tester/default.bash b/spec/fixtures/tester/default.bash index 2b843ca..a5a878f 100644 --- a/spec/fixtures/tester/default.bash +++ b/spec/fixtures/tester/default.bash @@ -5,10 +5,9 @@ # Modifying it manually is not recommended _cli_completions_filter() { - local words=("$@") + local words="$1" local cur=${COMP_WORDS[COMP_CWORD]} local result=() - local want_options=0 # words the user already typed (excluding the command itself) local used=() @@ -16,26 +15,28 @@ _cli_completions_filter() { used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") fi - # 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 + 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 [[ "${word:0:1}" == "-" ]] && continue + local seen=0 for u in "${used[@]}"; do if [[ "$u" == "$word" ]]; then - continue 2 + seen=1 + break fi done - fi - - # compgen -W expects shell-escaped words in one space-delimited string. - printf -v word '%q' "$word" - result+=("$word") - done + ((!seen)) && result+=("$word") + done - echo "${result[*]}" + echo "${result[*]}" + fi } _cli_completions() { @@ -50,23 +51,19 @@ _cli_completions() { case "$compline" in 'command childcommand'*) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - 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'*) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - 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'*) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - 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") ;; *) - # shellcheck disable=SC2046 # intentional splitting for dynamic $(...) completions - 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 74a5303f238fbee1ab91fbda59e2b2dcbd54134e Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Fri, 20 Feb 2026 12:56:27 +0200 Subject: [PATCH 5/8] apply non-core commits --- .github/workflows/test.yml | 2 +- AGENTS.md | 5 +++++ README.md | 2 ++ spec/README.md | 25 ++++++++++++++++------ spec/completely/commands/install_spec.rb | 2 ++ spec/completely/commands/uninstall_spec.rb | 4 +++- 6 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 AGENTS.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 97286e6..f8c93a0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: - matrix: { ruby: ['3.2', '3.3', '3.4'] } + matrix: { ruby: ['3.2', '3.3', '3.4', '4.0'] } steps: - name: Checkout code diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0a57aa9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ +# AGENTS.md + +## Notes + +- `CHANGELOG.md` is generated using `git changelog`; do not edit it manually. diff --git a/README.md b/README.md index e85942d..c35d6db 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Completely - Bash Completions Generator +![repocard](https://repocard.dannyben.com/svg/completely.svg) + Completely is a command line utility and a Ruby library that lets you generate bash completion scripts from simple YAML configuration. diff --git a/spec/README.md b/spec/README.md index 25343ef..c00362f 100644 --- a/spec/README.md +++ b/spec/README.md @@ -2,19 +2,29 @@ ## Running tests +You can run specs with `rspec` as usual. + +We recommend using [`respec`][2], which wraps common spec workflows: + ```bash -$ rspec +rspec # or -$ run spec -# or, to run just tests in a given file -$ run spec zsh -# or, to run just specs tagged with :focus -$ run spec :focus +respec ``` You might need to prefix the commands with `bundle exec`, depending on the way Ruby is installed. +Useful helper shortcuts: + +```bash +# script quality checks (shellcheck + shfmt generated script tests) +respec tagged script_quality + +# integration behavior suite +respec only integration +``` + ## Interactive Approvals Some tests may prompt you for an interactive approval of changes. This @@ -29,4 +39,5 @@ ZSH compatibility test is done by running the completely tester script inside a zsh container. This is all done automatically by `spec/completely/zsh_spec.rb`. -[1]: https://github.com/dannyben/rspec_approvals \ No newline at end of file +[1]: https://github.com/dannyben/rspec_approvals +[2]: https://github.com/DannyBen/respec diff --git a/spec/completely/commands/install_spec.rb b/spec/completely/commands/install_spec.rb index 014b476..6e2a93f 100644 --- a/spec/completely/commands/install_spec.rb +++ b/spec/completely/commands/install_spec.rb @@ -52,6 +52,7 @@ expect { subject.execute %w[install completely-test --dry] } .to output_approval('cli/install/dry') + .except(%r[/home/([^/]+)], '/home/USER') end end @@ -64,6 +65,7 @@ expect { subject.execute %w[install completely-test - --dry] } .to output_approval('cli/install/stdin-dry') .except(/cp [^\s]*completely-[^\s]*/, 'cp ') + .except(%r[/home/([^/]+)], '/home/USER') end end diff --git a/spec/completely/commands/uninstall_spec.rb b/spec/completely/commands/uninstall_spec.rb index ec34739..191e27a 100644 --- a/spec/completely/commands/uninstall_spec.rb +++ b/spec/completely/commands/uninstall_spec.rb @@ -38,7 +38,9 @@ expect(mock_installer).not_to receive(:uninstall) expect { subject.execute %w[uninstall completely-test --dry] } - .to output_approval('cli/uninstall/dry').diff(20) + .to output_approval('cli/uninstall/dry') + .except(%r[/home/([^/]+)], '/home/USER') + .diff(20) end end From f8928cc147fec39ffb2326bc4b1f6cad2d6ba80f Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 18 Feb 2026 22:55:09 +0200 Subject: [PATCH 6/8] - Update completions installer paths --- lib/completely/commands/generate.rb | 2 +- lib/completely/installer.rb | 47 ++++------- spec/approvals/cli/install/dry | 2 +- spec/approvals/cli/install/stdin-dry | 2 +- spec/approvals/cli/uninstall/dry | 2 +- spec/completely/commands/generate_spec.rb | 6 +- spec/completely/installer_spec.rb | 96 ++++++++++++----------- 7 files changed, 75 insertions(+), 82 deletions(-) diff --git a/lib/completely/commands/generate.rb b/lib/completely/commands/generate.rb index 091bd06..fb013ff 100644 --- a/lib/completely/commands/generate.rb +++ b/lib/completely/commands/generate.rb @@ -60,7 +60,7 @@ def install(content) end def show(content) = puts content - + def save(content) File.write output_path, content say "Saved m`#{output_path}`" diff --git a/lib/completely/installer.rb b/lib/completely/installer.rb index f4eebdf..f1d2978 100644 --- a/lib/completely/installer.rb +++ b/lib/completely/installer.rb @@ -1,3 +1,5 @@ +require 'fileutils' + module Completely class Installer class << self @@ -38,18 +40,8 @@ def initialize(program:, script_path: nil) @script_path = script_path end - def target_directories - @target_directories ||= %W[ - /usr/share/bash-completion/completions - /usr/local/etc/bash_completion.d - #{Dir.home}/.local/share/bash-completion/completions - #{Dir.home}/.bash_completion.d - ] - end - def install_command - result = root_user? ? [] : %w[sudo] - result + %W[cp #{script_path} #{target_path}] + %W[cp #{script_path} #{target_path}] end def install_command_string @@ -57,8 +49,7 @@ def install_command_string end def uninstall_command - result = root_user? ? [] : %w[sudo] - result + %w[rm -f] + target_directories.map { |dir| "#{dir}/#{program}" } + %W[rm -f #{target_path}] end def uninstall_command_string @@ -70,14 +61,12 @@ def target_path end def install(force: false) - unless completions_path - raise InstallError, 'Cannot determine system completions directory' - end - unless script_exist? raise InstallError, "Cannot find script: m`#{script_path}`" end + FileUtils.mkdir_p completions_path + if target_exist? && !force raise InstallError, "File exists: m`#{target_path}`" end @@ -99,22 +88,20 @@ def script_exist? File.exist? script_path end - def root_user? - Process.uid.zero? + def completions_path + @completions_path ||= "#{user_completions_base_dir}/completions" end - def completions_path - @completions_path ||= begin - result = nil - target_directories.each do |target| - if Dir.exist? target - result = target - break - end - end + def user_completions_base_dir + @user_completions_base_dir ||= bash_completion_user_dir || "#{data_home}/bash-completion" + end - result - end + def bash_completion_user_dir + ENV['BASH_COMPLETION_USER_DIR']&.split(':')&.find { |entry| !entry.empty? } + end + + def data_home + ENV['XDG_DATA_HOME'] || "#{Dir.home}/.local/share" end end end diff --git a/spec/approvals/cli/install/dry b/spec/approvals/cli/install/dry index 02bae48..61fd597 100644 --- a/spec/approvals/cli/install/dry +++ b/spec/approvals/cli/install/dry @@ -1 +1 @@ -sudo cp completely.bash /usr/share/bash-completion/completions/completely-test +cp completely.bash /home/vagrant/.local/share/bash-completion/completions/completely-test diff --git a/spec/approvals/cli/install/stdin-dry b/spec/approvals/cli/install/stdin-dry index 47144da..cf74e1e 100644 --- a/spec/approvals/cli/install/stdin-dry +++ b/spec/approvals/cli/install/stdin-dry @@ -1 +1 @@ -sudo cp /usr/share/bash-completion/completions/completely-test +cp /home/vagrant/.local/share/bash-completion/completions/completely-test diff --git a/spec/approvals/cli/uninstall/dry b/spec/approvals/cli/uninstall/dry index d582874..088c7c9 100644 --- a/spec/approvals/cli/uninstall/dry +++ b/spec/approvals/cli/uninstall/dry @@ -1 +1 @@ -sudo rm -f /usr/share/bash-completion/completions/completely-test /usr/local/etc/bash_completion.d/completely-test /home/vagrant/.local/share/bash-completion/completion/completely-test /home/vagrant/.bash_completion.d/completely-test +rm -f /home/vagrant/.local/share/bash-completion/completions/completely-test diff --git a/spec/completely/commands/generate_spec.rb b/spec/completely/commands/generate_spec.rb index fb6a198..9d152ea 100644 --- a/spec/completely/commands/generate_spec.rb +++ b/spec/completely/commands/generate_spec.rb @@ -119,16 +119,16 @@ context 'with --install PROGRAM' do let(:mock_installer) do instance_double Installer, - install: true, + install: true, install_command_string: 'stubbed install_command_string', - target_path: 'stubbed target_path' + target_path: 'stubbed target_path' end it 'passes the generated script to the installer' do allow(Installer).to receive(:from_string) .with( program: 'mycli', - string: a_string_matching(/bash completions script/) + string: a_string_matching(/bash completions script/) ).and_return(mock_installer) expect(mock_installer).to receive(:install) diff --git a/spec/completely/installer_spec.rb b/spec/completely/installer_spec.rb index f74f6fc..bf0a67b 100644 --- a/spec/completely/installer_spec.rb +++ b/spec/completely/installer_spec.rb @@ -4,14 +4,9 @@ let(:leeway) { RUBY_VERSION < '3.2.0' ? 0 : 3 } let(:program) { 'completely-test' } let(:script_path) { 'completions.bash' } - let(:targets) { subject.target_directories.map { |dir| "#{dir}/#{program}" } } - let(:install_command) do - %W[sudo cp #{subject.script_path} #{subject.target_path}] - end - - let(:uninstall_command) do - %w[sudo rm -f] + targets - end + let(:target_path) { "#{Dir.home}/.local/share/bash-completion/completions/#{program}" } + let(:install_command) { %W[cp #{subject.script_path} #{subject.target_path}] } + let(:uninstall_command) { %W[rm -f #{subject.target_path}] } describe '::from_io' do subject { described_class.from_io program:, io: } @@ -33,36 +28,61 @@ end end - describe '#target_directories' do - it 'returns an array of potential completion directories' do - expect(subject.target_directories).to be_an Array - expect(subject.target_directories.size).to eq 4 + describe '#target_path' do + it 'returns a user-level target path' do + expect(subject.target_path).to eq target_path end - end - describe '#target_path' do - it 'returns the first matching path' do - expect(subject.target_path) - .to eq '/usr/share/bash-completion/completions/completely-test' + context 'when BASH_COMPLETION_USER_DIR is set' do + around do |example| + original = ENV['BASH_COMPLETION_USER_DIR'] + ENV['BASH_COMPLETION_USER_DIR'] = '/tmp/completely-user-dir' + example.run + ensure + ENV['BASH_COMPLETION_USER_DIR'] = original + end + + it 'uses BASH_COMPLETION_USER_DIR/completions' do + expect(subject.target_path).to eq '/tmp/completely-user-dir/completions/completely-test' + end end - end - describe '#install_command' do - it 'returns a copy command as an array' do - expect(subject.install_command) - .to eq %w[sudo cp completions.bash /usr/share/bash-completion/completions/completely-test] + context 'when XDG_DATA_HOME is set' do + around do |example| + original = ENV['XDG_DATA_HOME'] + ENV['XDG_DATA_HOME'] = '/tmp/completely-xdg' + example.run + ensure + ENV['XDG_DATA_HOME'] = original + end + + it 'uses XDG_DATA_HOME/bash-completion/completions' do + expect(subject.target_path).to eq '/tmp/completely-xdg/bash-completion/completions/completely-test' + end end - context 'when the user is root' do - it 'returns the command without sudo' do - allow(subject).to receive(:root_user?).and_return true + context 'when BASH_COMPLETION_USER_DIR has multiple entries' do + around do |example| + original = ENV['BASH_COMPLETION_USER_DIR'] + ENV['BASH_COMPLETION_USER_DIR'] = ':/tmp/completely-first:/tmp/completely-second' + example.run + ensure + ENV['BASH_COMPLETION_USER_DIR'] = original + end - expect(subject.install_command) - .to eq %w[cp completions.bash /usr/share/bash-completion/completions/completely-test] + it 'uses the first non-empty entry' do + expect(subject.target_path).to eq '/tmp/completely-first/completions/completely-test' end end end + describe '#install_command' do + it 'returns a copy command as an array' do + expect(subject.install_command) + .to eq %W[cp completions.bash #{target_path}] + end + end + describe '#install_command_string' do it 'returns the install command as a string' do expect(subject.install_command_string).to eq subject.install_command.join(' ') @@ -71,15 +91,7 @@ describe '#uninstall_command' do it 'returns an rm command as an array' do - expect(subject.uninstall_command).to eq %w[sudo rm -f] + targets - end - - context 'when the user is root' do - it 'returns the command without sudo' do - allow(subject).to receive(:root_user?).and_return true - - expect(subject.uninstall_command).to eq %w[rm -f] + targets - end + expect(subject.uninstall_command).to eq %W[rm -f #{target_path}] end end @@ -95,15 +107,7 @@ before do allow(subject).to receive_messages(script_path: existing_file, target_path: missing_file) - end - - context 'when the completions_path cannot be found' do - it 'raises an error' do - allow(subject).to receive(:completions_path).and_return nil - - expect { subject.install }.to raise_approval('installer/install-no-dir') - .diff(leeway) - end + allow(FileUtils).to receive(:mkdir_p) end context 'when the script cannot be found' do @@ -128,6 +132,7 @@ it 'proceeds to install' do allow(subject).to receive(:target_path).and_return existing_file + expect(FileUtils).to receive(:mkdir_p) expect(subject).to receive(:system).with(*install_command) subject.install force: true @@ -138,6 +143,7 @@ it 'proceeds to install' do allow(subject).to receive(:target_path).and_return missing_file + expect(FileUtils).to receive(:mkdir_p) expect(subject).to receive(:system).with(*install_command) subject.install From d5f7552c5a0a2db6c65e1a88f3caa03fb69697ed Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Fri, 20 Feb 2026 13:11:08 +0200 Subject: [PATCH 7/8] rubocop --- README.md | 7 +++++++ spec/approvals/cli/install/dry | 2 +- spec/approvals/cli/install/stdin-dry | 2 +- spec/completely/commands/install_spec.rb | 4 ++-- spec/completely/commands/uninstall_spec.rb | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c35d6db..6203e99 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,13 @@ mygit: The `2> /dev/null` is used so that if the command is executed in a directory without a git repository, it will still behave as expected. +### Completion scope and limitations + +- Completion words are treated as whitespace-delimited tokens. +- Literal completion phrases that contain spaces are not supported as a single completion item. +- Quotes and other special shell characters in literal completion words are not escaped automatically. +- Dynamic `$(...)` completion commands should output plain whitespace-delimited words. + ### Suggesting flag arguments Adding a `*` wildcard in the middle of a pattern can be useful for suggesting diff --git a/spec/approvals/cli/install/dry b/spec/approvals/cli/install/dry index 61fd597..3a4ba7e 100644 --- a/spec/approvals/cli/install/dry +++ b/spec/approvals/cli/install/dry @@ -1 +1 @@ -cp completely.bash /home/vagrant/.local/share/bash-completion/completions/completely-test +cp completely.bash /home/USER/.local/share/bash-completion/completions/completely-test diff --git a/spec/approvals/cli/install/stdin-dry b/spec/approvals/cli/install/stdin-dry index cf74e1e..cfbfd0a 100644 --- a/spec/approvals/cli/install/stdin-dry +++ b/spec/approvals/cli/install/stdin-dry @@ -1 +1 @@ -cp /home/vagrant/.local/share/bash-completion/completions/completely-test +cp /home/USER/.local/share/bash-completion/completions/completely-test diff --git a/spec/completely/commands/install_spec.rb b/spec/completely/commands/install_spec.rb index 6e2a93f..b1351a7 100644 --- a/spec/completely/commands/install_spec.rb +++ b/spec/completely/commands/install_spec.rb @@ -52,7 +52,7 @@ expect { subject.execute %w[install completely-test --dry] } .to output_approval('cli/install/dry') - .except(%r[/home/([^/]+)], '/home/USER') + .except(%r{/home/([^/]+)}, '/home/USER') end end @@ -65,7 +65,7 @@ expect { subject.execute %w[install completely-test - --dry] } .to output_approval('cli/install/stdin-dry') .except(/cp [^\s]*completely-[^\s]*/, 'cp ') - .except(%r[/home/([^/]+)], '/home/USER') + .except(%r{/home/([^/]+)}, '/home/USER') end end diff --git a/spec/completely/commands/uninstall_spec.rb b/spec/completely/commands/uninstall_spec.rb index 191e27a..930bc95 100644 --- a/spec/completely/commands/uninstall_spec.rb +++ b/spec/completely/commands/uninstall_spec.rb @@ -39,7 +39,7 @@ expect { subject.execute %w[uninstall completely-test --dry] } .to output_approval('cli/uninstall/dry') - .except(%r[/home/([^/]+)], '/home/USER') + .except(%r{/home/([^/]+)}, '/home/USER') .diff(20) end end From afe2c522f971ae17edb879b4f81ce532ede885e6 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Fri, 20 Feb 2026 13:24:51 +0200 Subject: [PATCH 8/8] - Support filenames with spaces when or are used --- lib/completely/pattern.rb | 4 ++++ lib/completely/templates/template.erb | 3 +++ spec/approvals/cli/generated-script | 1 + spec/approvals/cli/generated-script-alt | 1 + spec/approvals/cli/generated-wrapped-script | 1 + spec/approvals/cli/test/completely-tester-1.sh | 1 + spec/approvals/cli/test/completely-tester-2.sh | 1 + spec/approvals/cli/test/completely-tester.sh | 1 + spec/approvals/completions/function | 1 + spec/approvals/completions/script | 1 + spec/approvals/completions/script-only-spaces | 1 + spec/completely/pattern_spec.rb | 14 ++++++++++++++ 12 files changed, 30 insertions(+) diff --git a/lib/completely/pattern.rb b/lib/completely/pattern.rb index a276a29..5bd0609 100644 --- a/lib/completely/pattern.rb +++ b/lib/completely/pattern.rb @@ -49,6 +49,10 @@ def compgen @compgen ||= compgen! end + def filename_action? + actions.include?('-A file') || actions.include?('-A directory') + end + private def compgen! diff --git a/lib/completely/templates/template.erb b/lib/completely/templates/template.erb index b5dfbe4..3110fd3 100644 --- a/lib/completely/templates/template.erb +++ b/lib/completely/templates/template.erb @@ -60,6 +60,9 @@ % patterns.each do |pattern| % next if pattern.empty? <%= pattern.case_string %>) +% if pattern.filename_action? + compopt -o filenames 2>/dev/null +% end while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen <%= pattern.compgen %> -- "$cur") ;; diff --git a/spec/approvals/cli/generated-script b/spec/approvals/cli/generated-script index 94e256e..ded53a7 100644 --- a/spec/approvals/cli/generated-script +++ b/spec/approvals/cli/generated-script @@ -63,6 +63,7 @@ _mygit_completions() { ;; 'init'*) + compopt -o filenames 2>/dev/null while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mygit_completions_filter "--bare")" -- "$cur") ;; diff --git a/spec/approvals/cli/generated-script-alt b/spec/approvals/cli/generated-script-alt index 217aae4..3856f04 100644 --- a/spec/approvals/cli/generated-script-alt +++ b/spec/approvals/cli/generated-script-alt @@ -63,6 +63,7 @@ _mycomps() { ;; 'init'*) + compopt -o filenames 2>/dev/null while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mycomps_filter "--bare")" -- "$cur") ;; diff --git a/spec/approvals/cli/generated-wrapped-script b/spec/approvals/cli/generated-wrapped-script index e37ade0..32613f4 100644 --- a/spec/approvals/cli/generated-wrapped-script +++ b/spec/approvals/cli/generated-wrapped-script @@ -64,6 +64,7 @@ give_comps() { echo $' ;;' echo $'' echo $' \'init\'*)' + echo $' compopt -o filenames 2>/dev/null' echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mygit_completions_filter "--bare")" -- "$cur")' echo $' ;;' echo $'' diff --git a/spec/approvals/cli/test/completely-tester-1.sh b/spec/approvals/cli/test/completely-tester-1.sh index 50d5b48..5deacf8 100644 --- a/spec/approvals/cli/test/completely-tester-1.sh +++ b/spec/approvals/cli/test/completely-tester-1.sh @@ -71,6 +71,7 @@ _mygit_completions() { ;; 'init'*) + compopt -o filenames 2>/dev/null while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mygit_completions_filter "--bare")" -- "$cur") ;; diff --git a/spec/approvals/cli/test/completely-tester-2.sh b/spec/approvals/cli/test/completely-tester-2.sh index a0fb1e3..ec78932 100644 --- a/spec/approvals/cli/test/completely-tester-2.sh +++ b/spec/approvals/cli/test/completely-tester-2.sh @@ -71,6 +71,7 @@ _mygit_completions() { ;; 'init'*) + compopt -o filenames 2>/dev/null while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mygit_completions_filter "--bare")" -- "$cur") ;; diff --git a/spec/approvals/cli/test/completely-tester.sh b/spec/approvals/cli/test/completely-tester.sh index 108dbf9..499ca18 100644 --- a/spec/approvals/cli/test/completely-tester.sh +++ b/spec/approvals/cli/test/completely-tester.sh @@ -71,6 +71,7 @@ _mygit_completions() { ;; 'init'*) + compopt -o filenames 2>/dev/null while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_mygit_completions_filter "--bare")" -- "$cur") ;; diff --git a/spec/approvals/completions/function b/spec/approvals/completions/function index 1b31b34..3bd1845 100644 --- a/spec/approvals/completions/function +++ b/spec/approvals/completions/function @@ -52,6 +52,7 @@ send_completions() { echo $'' echo $' case "$compline" in' echo $' \'generate\'*)' + echo $' compopt -o filenames 2>/dev/null' echo $' while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_completely_completions_filter "--help --force")" -- "$cur")' echo $' ;;' echo $'' diff --git a/spec/approvals/completions/script b/spec/approvals/completions/script index cba794b..5d1a3aa 100644 --- a/spec/approvals/completions/script +++ b/spec/approvals/completions/script @@ -51,6 +51,7 @@ _completely_completions() { case "$compline" in 'generate'*) + compopt -o filenames 2>/dev/null while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_completely_completions_filter "--help --force")" -- "$cur") ;; diff --git a/spec/approvals/completions/script-only-spaces b/spec/approvals/completions/script-only-spaces index 44695a6..6e845b2 100644 --- a/spec/approvals/completions/script-only-spaces +++ b/spec/approvals/completions/script-only-spaces @@ -51,6 +51,7 @@ _completely_completions() { case "$compline" in 'generate'*) + compopt -o filenames 2>/dev/null while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -W "$(_completely_completions_filter "--help --force")" -- "$cur") ;; diff --git a/spec/completely/pattern_spec.rb b/spec/completely/pattern_spec.rb index 912ed3e..05f10e6 100644 --- a/spec/completely/pattern_spec.rb +++ b/spec/completely/pattern_spec.rb @@ -37,6 +37,20 @@ end end + describe '#filename_action?' do + it 'returns true when file or directory actions exist' do + expect(subject.filename_action?).to be true + end + + context 'when file and directory actions are not present' do + let(:completions) { %w[--message --help ] } + + it 'returns false' do + expect(subject.filename_action?).to be false + end + end + end + describe '#prefix' do it 'returns the first word of the pattern' do expect(subject.prefix).to eq 'git'