diff --git a/USAGE.md b/USAGE.md index e6fe5e9de..9b1bc586e 100644 --- a/USAGE.md +++ b/USAGE.md @@ -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 diff --git a/cli/completion-scripts/aws-vault.bash b/cli/completion-scripts/aws-vault.bash new file mode 100644 index 000000000..ee5617908 --- /dev/null +++ b/cli/completion-scripts/aws-vault.bash @@ -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 diff --git a/cli/completion-scripts/aws-vault.fish b/cli/completion-scripts/aws-vault.fish new file mode 100644 index 000000000..1c6423d30 --- /dev/null +++ b/cli/completion-scripts/aws-vault.fish @@ -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 diff --git a/cli/completion-scripts/aws-vault.zsh b/cli/completion-scripts/aws-vault.zsh new file mode 100644 index 000000000..3cbbdba75 --- /dev/null +++ b/cli/completion-scripts/aws-vault.zsh @@ -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 diff --git a/cli/completion.go b/cli/completion.go new file mode 100644 index 000000000..096d0362a --- /dev/null +++ b/cli/completion.go @@ -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 + }) +} diff --git a/cli/completion_test.go b/cli/completion_test.go new file mode 100644 index 000000000..1934c5258 --- /dev/null +++ b/cli/completion_test.go @@ -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") + } + }) +} diff --git a/contrib/completions/bash/aws-vault.bash b/contrib/completions/bash/aws-vault.bash deleted file mode 100644 index ee5617908..000000000 --- a/contrib/completions/bash/aws-vault.bash +++ /dev/null @@ -1,17 +0,0 @@ -_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 diff --git a/contrib/completions/bash/aws-vault.bash b/contrib/completions/bash/aws-vault.bash new file mode 120000 index 000000000..0fafe0316 --- /dev/null +++ b/contrib/completions/bash/aws-vault.bash @@ -0,0 +1 @@ +../../../cli/completion-scripts/aws-vault.bash \ No newline at end of file diff --git a/contrib/completions/fish/aws-vault.fish b/contrib/completions/fish/aws-vault.fish deleted file mode 100644 index 1c6423d30..000000000 --- a/contrib/completions/fish/aws-vault.fish +++ /dev/null @@ -1,24 +0,0 @@ -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 diff --git a/contrib/completions/fish/aws-vault.fish b/contrib/completions/fish/aws-vault.fish new file mode 120000 index 000000000..047ff3d7d --- /dev/null +++ b/contrib/completions/fish/aws-vault.fish @@ -0,0 +1 @@ +../../../cli/completion-scripts/aws-vault.fish \ No newline at end of file diff --git a/contrib/completions/zsh/aws-vault.zsh b/contrib/completions/zsh/aws-vault.zsh deleted file mode 100644 index 3cbbdba75..000000000 --- a/contrib/completions/zsh/aws-vault.zsh +++ /dev/null @@ -1,24 +0,0 @@ -#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 diff --git a/contrib/completions/zsh/aws-vault.zsh b/contrib/completions/zsh/aws-vault.zsh new file mode 120000 index 000000000..4047a8a98 --- /dev/null +++ b/contrib/completions/zsh/aws-vault.zsh @@ -0,0 +1 @@ +../../../cli/completion-scripts/aws-vault.zsh \ No newline at end of file diff --git a/main.go b/main.go index fc6b1f708..c9960ca85 100644 --- a/main.go +++ b/main.go @@ -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:])) }