Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Install OS dependencies
run: sudo apt-get -y install mandoc
run: sudo apt-get -y install mandoc zsh

# Rush needed for easy installation of stuff
- name: Install rush
Expand Down
8 changes: 6 additions & 2 deletions examples/completions-advanced/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ dynamic external commands and internal functions, file and directory sources,
and the `no-space` option.

Runtime completions are enabled in `settings.yml`. Users can load the generated
Bash wrapper with:
wrapper for their shell with:

```bash
# Bash
source <(cli completions)

# Zsh
source <(cli completions zsh)
```

<!-- include: settings.yml src/completions_command.sh src/lib/completions.sh -->
Expand All @@ -28,7 +32,7 @@ commands:
args:
- name: shell
help: Shell to generate completions for
allowed: [bash]
allowed: [bash, zsh]
default: bash

- name: deploy
Expand Down
2 changes: 1 addition & 1 deletion examples/completions-advanced/src/bashly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ commands:
args:
- name: shell
help: Shell to generate completions for
allowed: [bash]
allowed: [bash, zsh]
default: bash

- name: deploy
Expand Down
9 changes: 6 additions & 3 deletions examples/completions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
Demonstrates how to expose the generated `send_completions` function through
an application command. Runtime completions are enabled in `settings.yml`.

Users can load the Bash wrapper with:
Users can load the wrapper for their shell with:

```bash
# Bash
source <(cli completions)

# Zsh
source <(cli completions zsh)
```

This example was generated with:
Expand Down Expand Up @@ -37,7 +41,7 @@ commands:
args:
- name: shell
help: Shell to generate completions for
allowed: [bash]
allowed: [bash, zsh]
default: bash

- name: download
Expand Down Expand Up @@ -102,4 +106,3 @@ download
````



2 changes: 1 addition & 1 deletion examples/completions/src/bashly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ commands:
args:
- name: shell
help: Shell to generate completions for
allowed: [bash]
allowed: [bash, zsh]
default: bash

- name: download
Expand Down
2 changes: 2 additions & 0 deletions lib/bashly/views/command/completion_script.gtx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
>
> case "$completion_shell" in
> bash) send_completions_bash ;;
> zsh) send_completions_zsh ;;
> *)
> printf 'unsupported shell: %s\n' "$completion_shell" >&2
> return 1
Expand All @@ -13,3 +14,4 @@
> }
>
= render :completion_script_bash
= render :completion_script_zsh
58 changes: 58 additions & 0 deletions lib/bashly/views/command/completion_script_zsh.gtx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
= view_marker

> send_completions_zsh() {
> cat <<'BASHLY_COMPLETIONS'
> #compdef {{ name }}
>
> _{{ name.to_underscore }}_completions() {
> local completion_command="${words[1]}"
> local completion_current="${words[CURRENT]:-}"
> local completion_output
> local completion_directive
> local -a completion_words=()
> local -a completion_response=()
> local -a completion_options=()
> local -a completion_add_args=()
>
> if (( CURRENT > 2 )); then
> completion_words=("${words[2,$((CURRENT - 1))]}")
> fi
>
> completion_output="$(
> "$completion_command" __complete \
> "${completion_words[@]}" \
> "$completion_current"
> )"
> completion_response=("${(@f)completion_output}")
>
> completion_directive="${completion_response[-1]:-}"
> if [[ $completion_directive == :options=* ]]; then
> completion_response[-1]=()
> completion_options=("${(@s:,:)${completion_directive#:options=}}")
> fi
>
> local completion_option
> local completion_files=false
> local completion_directories=false
> for completion_option in "${completion_options[@]}"; do
> case "$completion_option" in
> files) completion_files=true ;;
> directories) completion_directories=true ;;
> no-space) completion_add_args=(-S '') ;;
> esac
> done
>
> if (( ${#completion_response[@]} > 0 )); then
> compadd "${completion_add_args[@]}" -- "${completion_response[@]}"
> fi
>
> if [[ $completion_files == true ]]; then
> _files "${completion_add_args[@]}"
> elif [[ $completion_directories == true ]]; then
> _files -/ "${completion_add_args[@]}"
> fi
> }
>
> compdef _{{ name.to_underscore }}_completions {{ name }}
> BASHLY_COMPLETIONS
> }
4 changes: 2 additions & 2 deletions spec/bashly/integration/completion_script_bash_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ def complete_with_bash(*words, trace_options: false)

it 'rejects unsupported shells' do
stdout, stderr, status = Open3.capture3(
'bash', '-c', "source #{cli}; send_completions zsh"
'bash', '-c', "source #{cli}; send_completions fish"
)

expect(status).not_to be_success
expect(stdout).to be_empty
expect(stderr).to eq "unsupported shell: zsh\n"
expect(stderr).to eq "unsupported shell: fish\n"
end
end

Expand Down
140 changes: 140 additions & 0 deletions spec/bashly/integration/completion_script_zsh_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
describe 'Zsh completion script', :slow do
def complete_with_zsh(*words, trace_files: false)
completion_words = words.map { |word| Shellwords.shellescape word }.join ' '
file_trace = if trace_files
<<~ZSH
_files() {
if [[ "$1" == "-/" ]]; then
shift
compadd "$@" -- apricot
else
compadd "$@" -- apple.txt apricot
fi
}
ZSH
end

Open3.capture3(
'zsh', '-f', '-c', <<~ZSH
cd ./spec/tmp
compdef() { true }
eval "$(bash -c 'source ./cli; send_completions zsh')"
typeset -A completion_test_seen=()
compadd() {
local no_space=false
if [[ "$1" == "-S" ]]; then
[[ -z "$2" ]] && no_space=true
shift 2
fi
[[ "$1" == "--" ]] && shift
[[ $no_space == true ]] && print 'option no-space'
local candidate
for candidate in "$@"; do
if [[ -z "${completion_test_seen[$candidate]:-}" ]]; then
printf 'candidate %s\n' "$candidate"
completion_test_seen[$candidate]=1
fi
done
}
#{file_trace}
words=(./cli #{completion_words})
CURRENT=#{words.length + 1}
_cli_completions
ZSH
)
end

context 'generation' do
let(:cli) { File.expand_path 'spec/tmp/cli' }

before(:context) do
Settings.enable_completions = 'always'
reset_tmp_dir
FileUtils.cp_r Dir['spec/fixtures/completions/core/*'], 'spec/tmp'
Commands::Generate.new.execute %w[generate --quiet]
end

after(:context) do
Settings.enable_completions = 'never'
end

it 'prints Zsh completions' do
stdout, stderr, status = Open3.capture3(
'bash', '-c', "source #{cli}; send_completions zsh"
)

expect(status).to be_success
expect(stderr).to be_empty
expect(stdout).to start_with "#compdef cli\n"
expect(stdout).to include '_cli_completions() {'
expect(stdout).to end_with "compdef _cli_completions cli\n"
end

it 'completes commands' do
stdout, stderr, status = complete_with_zsh 'd'

expect(status).to be_success
expect(stderr).to be_empty
expect(stdout.lines(chomp: true)).to eq ['candidate deploy']
end

it 'preserves an empty current word' do
stdout, stderr, status = complete_with_zsh 'server', ''

expect(status).to be_success
expect(stderr).to be_empty
expect(stdout.lines(chomp: true)).to include 'candidate start'
end
end

context 'configured options' do
before(:context) do
Settings.enable_completions = 'always'
reset_tmp_dir
FileUtils.cp_r Dir['spec/fixtures/completions/configured/*'], 'spec/tmp'
Commands::Generate.new.execute %w[generate --quiet]
end

after(:context) do
Settings.enable_completions = 'never'
end

it 'preserves literal candidates' do
stdout, stderr, status = complete_with_zsh 'static', '', trace_files: true

expect(status).to be_success
expect(stderr).to be_empty
expect(stdout.lines(chomp: true)).to include(
'candidate two words', 'candidate $literal', 'candidate :options=files'
)
end

it 'adds files when requested' do
stdout, stderr, status = complete_with_zsh 'files', 'a', trace_files: true

expect(status).to be_success
expect(stderr).to be_empty
expect(stdout.lines(chomp: true)).to contain_exactly(
'candidate apple.txt', 'candidate apricot'
)
end

it 'adds only directories when requested' do
stdout, stderr, status = complete_with_zsh 'directories', 'a', trace_files: true

expect(status).to be_success
expect(stderr).to be_empty
expect(stdout.lines(chomp: true)).to eq ['candidate apricot']
end

it 'applies no-space to candidates and files' do
stdout, stderr, status = complete_with_zsh 'combined', 'a', trace_files: true

expect(status).to be_success
expect(stderr).to be_empty
expect(stdout.lines(chomp: true)).to contain_exactly(
'option no-space', 'candidate apple.txt', 'candidate apricot'
)
end
end
end