Skip to content

Commit

Permalink
Add a command to generate completion scripts
Browse files Browse the repository at this point in the history
Rather than directing people to download a file, which could end up out
of sync if the upstream version changes, instead bundle the scripts into
the binary and add a command to output them.

`//go:embed` requires the files to be in a descendant directory of the
file containing the directive, so move them into a subdirectory, and
symlink them back to the original locations so that people can still use
the legacy method
  • Loading branch information
whi-tw committed May 24, 2023
1 parent 7583a83 commit d301824
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 71 deletions.
10 changes: 4 additions & 6 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -683,12 +683,10 @@ Further config:

## Shell completion

You can generate shell completions for
- bash: `eval "$(curl -fs https://raw.githubusercontent.com/99designs/aws-vault/master/contrib/completions/bash/aws-vault.bash)"`
- zsh: `eval "$(curl -fs https://raw.githubusercontent.com/99designs/aws-vault/master/contrib/completions/zsh/aws-vault.zsh)"`
- fish: `eval "$(curl -fs https://raw.githubusercontent.com/99designs/aws-vault/master/contrib/completions/fish/aws-vault.fish)"`

Find the completion scripts at [contrib/completions](contrib/completions).
You can generate shell completions for `bash`, `zsh` and `fish`:
- bash: `eval "$(aws-vault completion bash)"`
- zsh: `eval "$(aws-vault completion zsh)"`
- fish: `eval "$(aws-vault completion fish)"`


## Desktop apps
Expand Down
17 changes: 17 additions & 0 deletions cli/completion-scripts/aws-vault.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
_aws-vault_bash_autocomplete() {
local i cur prev opts base

for (( i=1; i < COMP_CWORD; i++ )); do
if [[ ${COMP_WORDS[i]} == -- ]]; then
_command_offset $i+1
return
fi
done

COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
opts=$( ${COMP_WORDS[0]} --completion-bash "${COMP_WORDS[@]:1:$COMP_CWORD}" )
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
}
complete -F _aws-vault_bash_autocomplete -o default aws-vault
24 changes: 24 additions & 0 deletions cli/completion-scripts/aws-vault.fish
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
if status --is-interactive
complete -ec aws-vault

# switch based on seeing a `--`
complete -c aws-vault -n 'not __fish_aws_vault_is_commandline' -xa '(__fish_aws_vault_complete_arg)'
complete -c aws-vault -n '__fish_aws_vault_is_commandline' -xa '(__fish_aws_vault_complete_commandline)'

function __fish_aws_vault_is_commandline
string match -q -r '^--$' -- (commandline -opc)
end

function __fish_aws_vault_complete_arg
set -l parts (commandline -opc)
set -e parts[1]

aws-vault --completion-bash $parts
end

function __fish_aws_vault_complete_commandline
set -l parts (string split --max 1 '--' -- (commandline -pc))

complete "-C$parts[2]"
end
end
24 changes: 24 additions & 0 deletions cli/completion-scripts/aws-vault.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#compdef aws-vault

_aws-vault() {
local i
for (( i=2; i < CURRENT; i++ )); do
if [[ ${words[i]} == -- ]]; then
shift $i words
(( CURRENT -= i ))
_normal
return
fi
done

local matches=($(${words[1]} --completion-bash ${(@)words[2,$CURRENT]}))
compadd -a matches

if [[ $compstate[nmatches] -eq 0 && $words[$CURRENT] != -* ]]; then
_files
fi
}

if [[ "$(basename -- ${(%):-%x})" != "_aws-vault" ]]; then
compdef _aws-vault aws-vault
fi
54 changes: 54 additions & 0 deletions cli/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package cli

import (
_ "embed"
"fmt"
"io"
"os"
"path"

"github.com/alecthomas/kingpin/v2"
)

//go:embed completion-scripts/aws-vault.bash
var bashCompletionScript string

//go:embed completion-scripts/aws-vault.zsh
var zshCompletionScript string

//go:embed completion-scripts/aws-vault.fish
var fishCompletionScript string

type CompletionCommandInput struct {
Shell string
}

var completionWriter io.Writer = os.Stdout

func ConfigureCompletionCommand(app *kingpin.Application) {
input := CompletionCommandInput{}

cmd := app.Command("completion", "Output shell completion code for bash / zsh / fish.")

cmd.Arg("shell", "Shell type: bash | zsh | fish").
Required().
Envar("SHELL").
HintOptions("bash", "zsh", "fish").
StringVar(&input.Shell)

cmd.Action(func(c *kingpin.ParseContext) error {
shell := path.Base(input.Shell) // strip any path (useful for $SHELL, doesn't hurt for other cases)

switch shell {
case "bash":
fmt.Fprint(completionWriter, bashCompletionScript)
case "zsh":
fmt.Fprint(completionWriter, zshCompletionScript)
case "fish":
fmt.Fprint(completionWriter, fishCompletionScript)
default:
return fmt.Errorf("unknown shell: %s", input.Shell)
}
return nil
})
}
84 changes: 84 additions & 0 deletions cli/completion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package cli

import (
"bytes"
"fmt"
"os"
"testing"

"github.com/alecthomas/kingpin/v2"
)

func TestConfigureCompletionCommand(t *testing.T) {
app := kingpin.New("test", "")
ConfigureCompletionCommand(app)

tests := []struct {
shell string
want string
}{
{"bash", bashCompletionScript},
{"zsh", zshCompletionScript},
{"fish", fishCompletionScript},
}

for _, tt := range tests {
t.Run(tt.shell, func(t *testing.T) {
var buf bytes.Buffer
completionWriter = &buf
_, err := app.Parse([]string{"completion", tt.shell})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

got := buf.String()
if got != tt.want {
t.Errorf("Parse([]string{\"completion\", %q}) = %q, want %q", tt.shell, got, tt.want)
}
})
}

// Test $SHELL envar

envarTests := []struct {
value string
want string
}{
{"/bin/bash", bashCompletionScript},
{"/bin/zsh", zshCompletionScript},
{"/bin/fish", fishCompletionScript},
}

for _, tt := range envarTests {
t.Run(fmt.Sprintf("$SHELL=%s", tt.value), func(t *testing.T) {
var buf bytes.Buffer
completionWriter = &buf
os.Setenv("SHELL", tt.value)
_, err := app.Parse([]string{"completion"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

got := buf.String()
if got != tt.want {
t.Errorf("SHELL=%q Parse([]string{\"completion\"}) = %q, want %q", tt.value, got, tt.want)
}
})
}

// Test unknown shell
t.Run("unknown shell", func(t *testing.T) {
var buf bytes.Buffer
app.UsageWriter(&buf)

_, err := app.Parse([]string{"completion", "foo"})

if err == nil {
t.Fatal("expected error, but got nil")
}

if err.Error() != "unknown shell: foo" {
t.Errorf("ConfigureCompletionCommand(%q) = %q, want %q", "foo", err.Error(), "unknown shell: foo")
}
})
}
17 changes: 0 additions & 17 deletions contrib/completions/bash/aws-vault.bash

This file was deleted.

1 change: 1 addition & 0 deletions contrib/completions/bash/aws-vault.bash
24 changes: 0 additions & 24 deletions contrib/completions/fish/aws-vault.fish

This file was deleted.

1 change: 1 addition & 0 deletions contrib/completions/fish/aws-vault.fish
24 changes: 0 additions & 24 deletions contrib/completions/zsh/aws-vault.zsh

This file was deleted.

1 change: 1 addition & 0 deletions contrib/completions/zsh/aws-vault.zsh
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func main() {
cli.ConfigureClearCommand(app, a)
cli.ConfigureLoginCommand(app, a)
cli.ConfigureProxyCommand(app)
cli.ConfigureCompletionCommand(app)

kingpin.MustParse(app.Parse(os.Args[1:]))
}

0 comments on commit d301824

Please sign in to comment.