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 31, 2023
1 parent 7583a83 commit 22f481e
Show file tree
Hide file tree
Showing 10 changed files with 283 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
72 changes: 72 additions & 0 deletions cli/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package cli

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

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

//go:embed completion-scripts/aws-vault.*
var completionScripts embed.FS

type CompletionCommandInput struct {
Shell string
}

var completionScriptPrinter io.Writer = os.Stdout

func ConfigureCompletionCommand(app *kingpin.Application) {
var input CompletionCommandInput

supportedShells, err := completionSupportedShells()
if err != nil {
panic(err)
}

cmd := app.Command(
"completion",
"Output shell completion script. To be used with `eval $(aws-vault completion SHELL)`.",
)

shellArgHelp := fmt.Sprintf("Shell to get completion script for [ %s ]", strings.Join(supportedShells, " "))
cmd.Arg("shell", shellArgHelp).
Required().
Envar("SHELL").
HintOptions(supportedShells...).
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)

completionScript, err := completionScripts.ReadFile(fmt.Sprintf("completion-scripts/aws-vault.%s", shell))
if err != nil {
return fmt.Errorf("unknown shell: %s", input.Shell)
}

_, err = fmt.Fprint(completionScriptPrinter, string(completionScript))
if err != nil {
return fmt.Errorf("failed to print completion script: %w", err)
}

return nil
})
}

// completionSupportedShells returns a list of shells with available completions.
// The list is generated from the embedded completion scripts.
func completionSupportedShells() ([]string, error) {
scripts, err := completionScripts.ReadDir("completion-scripts")
if err != nil {
return nil, fmt.Errorf("failed to read completion scripts: %w", err)
}
var shells []string
for _, f := range scripts {
shells = append(shells, strings.Split(path.Ext(f.Name()), ".")[1])
}
return shells, nil
}
138 changes: 138 additions & 0 deletions cli/completion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package cli

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

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

type shellTestDataItem struct {
shellName string
shellPath string
completionScript string
}

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

shellsAndCompletionScripts, err := getShellsAndCompletionScripts()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(shellsAndCompletionScripts) == 0 {
t.Fatal("no shells found")
}

invalidShellTestDataItem := shellTestDataItem{
shellName: "invalid",
shellPath: "/bin/invalid",
}
for _, tt := range shellsAndCompletionScripts {
if tt.shellName == invalidShellTestDataItem.shellName {
t.Fatalf("invalidShellTestDataItem.shellName (%s) is actually a valid shell name", invalidShellTestDataItem.shellName)
}
}

// Test shell argument
t.Run("arg", func(t *testing.T) {
for _, tt := range shellsAndCompletionScripts {
t.Run(tt.shellName, func(t *testing.T) {
var buf bytes.Buffer
completionScriptPrinter = &buf

_, err := app.Parse([]string{"completion", tt.shellName})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

got := buf.String()
if got != tt.completionScript {
t.Errorf("got %q; want %q", got, tt.completionScript)
}
})
}

t.Run(invalidShellTestDataItem.shellName, func(t *testing.T) {
var buf bytes.Buffer
app.UsageWriter(&buf)

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

if err == nil {
t.Fatal("expected error, but didn't get one")
}

want := fmt.Sprintf("unknown shell: %s", invalidShellTestDataItem.shellName)
if err.Error() != want {
t.Errorf("got error(%q); want error(%q)", err.Error(), want)
}
})
})

// Test $SHELL envar
t.Run("envar", func(t *testing.T) {
for _, tt := range shellsAndCompletionScripts {
t.Run(tt.shellName, func(t *testing.T) {
var buf bytes.Buffer
completionScriptPrinter = &buf

os.Setenv("SHELL", tt.shellPath)

_, err := app.Parse([]string{"completion"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

got := buf.String()
if got != tt.completionScript {
t.Errorf("got %q; want %q", got, tt.completionScript)
}
})
}

t.Run(invalidShellTestDataItem.shellName, func(t *testing.T) {
var buf bytes.Buffer
app.UsageWriter(&buf)

os.Setenv("SHELL", invalidShellTestDataItem.shellPath)

_, err := app.Parse([]string{"completion"})
if err == nil {
t.Fatal("expected error, but didn't get one")
}

want := fmt.Sprintf("unknown shell: %s", invalidShellTestDataItem.shellPath)
if err.Error() != want {
t.Errorf("got error(%q); want error(%q)", err.Error(), want)
}
})
})
}

func getShellsAndCompletionScripts() ([]shellTestDataItem, error) {
shells, err := completionSupportedShells()
if err != nil {
return nil, err
}

var shellsAndValues []shellTestDataItem
for _, shell := range shells {
completionScript, err := completionScripts.ReadFile(fmt.Sprintf("completion-scripts/aws-vault.%s", shell))
if err != nil {
return nil, err
}
shellsAndValues = append(
shellsAndValues,
shellTestDataItem{
shellName: shell,
shellPath: fmt.Sprintf("/bin/%s", shell),
completionScript: string(completionScript),
},
)
}
return shellsAndValues, nil
}
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 22f481e

Please sign in to comment.