Skip to content
This repository has been archived by the owner on May 18, 2021. It is now read-only.

Provide autocompletion in bash and zsh #117

Merged
merged 2 commits into from
Feb 7, 2019
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
242 changes: 242 additions & 0 deletions cmd/autocompletion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package cmd

import (
"bufio"
"bytes"
"os"

"github.com/spf13/cobra"
)

func init() {
autocompleteCmd.AddCommand(bashAutocompleteCmd)
autocompleteCmd.AddCommand(zshAutocompleteCmd)

RootCmd.AddCommand(autocompleteCmd)
}

var autocompleteCmd = &cobra.Command{
Use: "completion",
Short: "Output shell completion code for the given shell (bash or zsh)",
Long: `
Output shell completion code for bash or zsh
This command prints shell code which must be evaluated to provide interactive
completion of aws-okta commands.

Bash
$ source <(aws-okta completion bash)
Copy link
Contributor

@nickatsegment nickatsegment Feb 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if instead of subcommands, we could do a parameter defaulting to auto (the current shell).

  • bash -c 'aws-okta completion' --> bash
  • bash -c aws-okta completion bash'` --> bash
  • zsh -c 'aws-okta completion' --> zsh

Seems a little bit nicer, but not at all a blocker.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how you can check this, any suggestion?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of just parsing $SHELL. I think that's pretty standard.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$SHELL doesn't work if you change between shells, only for login shell. Maybe we can iterate it in another pr

will load the aws-okta completion code for bash. Note that this depends on the
bash-completion framework. It must be sourced before sourcing the aws-okta
completion, e.g. on macOS:
$ brew install bash-completion
$ source $(brew --prefix)/etc/bash_completion
$ source <(aws-okta completion bash)
(or, if you want to preserve completion within new terminal sessions)
$ echo 'source <(aws-okta completion bash)' >> ~/.bashrc

Zsh
$ source <(aws-okta completion zsh)
(or, if you want to preserve completion within new terminal sessions)
$ echo 'source <(aws-okta completion zsh)' >> ~/.zshrc`,
}

var bashAutocompleteCmd = &cobra.Command{
Use: "bash",
Short: "Output shell completion code for bash",
Long: `
Output shell completion code for bash.
This command prints shell code which must be evaluated to provide interactive
completion of aws-okta commands.
$ source <(aws-okta completion bash)
will load the aws-okta completion code for bash. Note that this depends on the
bash-completion framework. It must be sourced before sourcing the aws-okta
completion, e.g. on macOS:
$ brew install bash-completion
$ source $(brew --prefix)/etc/bash_completion
$ source <(aws-okta completion bash)
(or, if you want to preserve completion within new terminal sessions)
$ echo 'source <(aws-okta completion bash)' >> ~/.bashrc`,
RunE: runCompletionBash,
}

var zshAutocompleteCmd = &cobra.Command{
Use: "zsh",
Short: "Output shell completion code for zsh",
Long: `
Output shell completion code for zsh.
This command prints shell code which must be evaluated to provide interactive
completion of aws-okta commands.
$ source <(aws-okta completion zsh)
(or, if you want to preserve completion within new terminal sessions)
$ echo 'source <(aws-okta completion zsh)' >> ~/.zshrc
zsh completions are only supported in versions of zsh >= 5.2`,
RunE: runCompletionZsh,
}

func runCompletionBash(cmd *cobra.Command, args []string) error {
out := bufio.NewWriter(os.Stdout)
defer out.Flush()
return RootCmd.GenBashCompletion(out)
}

// Copyright 2016 The Kubernetes Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
func runCompletionZsh(cmd *cobra.Command, args []string) error {
out := bufio.NewWriter(os.Stdout)
defer out.Flush()
zshInitialization := `
__aws-okta_bash_source() {
nickatsegment marked this conversation as resolved.
Show resolved Hide resolved
alias shopt=':'
alias _expand=_bash_expand
alias _complete=_bash_comp
emulate -L sh
setopt kshglob noshglob braceexpand
source "$@"
}
__aws-okta_type() {
# -t is not supported by zsh
if [ "$1" == "-t" ]; then
shift
# fake Bash 4 to disable "complete -o nospace". Instead
# "compopt +-o nospace" is used in the code to toggle trailing
# spaces. We don't support that, but leave trailing spaces on
# all the time
if [ "$1" = "__aws-okta_compopt" ]; then
echo builtin
return 0
fi
fi
type "$@"
}
__aws-okta_compgen() {
local completions w
completions=( $(compgen "$@") ) || return $?
# filter by given word as prefix
while [[ "$1" = -* && "$1" != -- ]]; do
shift
shift
done
if [[ "$1" == -- ]]; then
shift
fi
for w in "${completions[@]}"; do
if [[ "${w}" = "$1"* ]]; then
echo "${w}"
fi
done
}
__aws-okta_compopt() {
true # don't do anything. Not supported by bashcompinit in zsh
}
__aws-okta_declare() {
if [ "$1" == "-F" ]; then
whence -w "$@"
else
builtin declare "$@"
fi
}
__aws-okta_ltrim_colon_completions()
{
if [[ "$1" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then
# Remove colon-word prefix from COMPREPLY items
local colon_word=${1%${1##*:}}
local i=${#COMPREPLY[*]}
while [[ $((--i)) -ge 0 ]]; do
COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"}
done
fi
}
__aws-okta_get_comp_words_by_ref() {
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[${COMP_CWORD}-1]}"
words=("${COMP_WORDS[@]}")
cword=("${COMP_CWORD[@]}")
}
__aws-okta_filedir() {
local RET OLD_IFS w qw
__debug "_filedir $@ cur=$cur"
if [[ "$1" = \~* ]]; then
# somehow does not work. Maybe, zsh does not call this at all
eval echo "$1"
return 0
fi
OLD_IFS="$IFS"
IFS=$'\n'
if [ "$1" = "-d" ]; then
shift
RET=( $(compgen -d) )
else
RET=( $(compgen -f) )
fi
IFS="$OLD_IFS"
IFS="," __debug "RET=${RET[@]} len=${#RET[@]}"
for w in ${RET[@]}; do
if [[ ! "${w}" = "${cur}"* ]]; then
continue
fi
if eval "[[ \"\${w}\" = *.$1 || -d \"\${w}\" ]]"; then
qw="$(__aws-okta_quote "${w}")"
if [ -d "${w}" ]; then
COMPREPLY+=("${qw}/")
else
COMPREPLY+=("${qw}")
fi
fi
done
}
__aws-okta_quote() {
if [[ $1 == \'* || $1 == \"* ]]; then
# Leave out first character
printf %q "${1:1}"
else
printf %q "$1"
fi
}
autoload -U +X bashcompinit && bashcompinit
# use word boundary patterns for BSD or GNU sed
LWORD='[[:<:]]'
RWORD='[[:>:]]'
if sed --help 2>&1 | grep -q GNU; then
LWORD='\<'
RWORD='\>'
fi
__aws-okta_convert_bash_to_zsh() {
sed \
-e 's/declare -F/whence -w/' \
-e 's/local \([a-zA-Z0-9_]*\)=/local \1; \1=/' \
-e 's/flags+=("\(--.*\)=")/flags+=("\1"); two_word_flags+=("\1")/' \
-e 's/must_have_one_flag+=("\(--.*\)=")/must_have_one_flag+=("\1")/' \
-e "s/${LWORD}_filedir${RWORD}/__aws-okta_filedir/g" \
-e "s/${LWORD}_get_comp_words_by_ref${RWORD}/__aws-okta_get_comp_words_by_ref/g" \
-e "s/${LWORD}__ltrim_colon_completions${RWORD}/__aws-okta_ltrim_colon_completions/g" \
-e "s/${LWORD}compgen${RWORD}/__aws-okta_compgen/g" \
-e "s/${LWORD}compopt${RWORD}/__aws-okta_compopt/g" \
-e "s/${LWORD}declare${RWORD}/__aws-okta_declare/g" \
-e "s/\\\$(type${RWORD}/\$(__aws-okta_type/g" \
<<'BASH_COMPLETION_EOF'
`
out.Write([]byte(zshInitialization))

buf := new(bytes.Buffer)
RootCmd.GenBashCompletion(buf)
out.Write(buf.Bytes())

zshTail := `
BASH_COMPLETION_EOF
}
__aws-okta_bash_source <(__aws-okta_convert_bash_to_zsh)
`
out.Write([]byte(zshTail))
return nil
}
20 changes: 16 additions & 4 deletions cmd/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package cmd

import (
"fmt"

log "github.com/sirupsen/logrus"

"os"
"os/exec"
"os/signal"
Expand All @@ -20,12 +23,21 @@ var (
assumeRoleTTL time.Duration
)

func mustListProfiles() lib.Profiles {
profiles, err := listProfiles()
if err != nil {
log.Panicf("Failed to list profiles: %v", err)
}
return profiles
}

// execCmd represents the exec command
var execCmd = &cobra.Command{
Use: "exec <profile> -- <command>",
Short: "exec will run the command specified with aws credentials set in the environment",
RunE: execRun,
PreRun: execPre,
Use: "exec <profile> -- <command>",
Short: "exec will run the command specified with aws credentials set in the environment",
RunE: execRun,
PreRun: execPre,
ValidArgs: listProfileNames(mustListProfiles()),
}

func init() {
Expand Down
21 changes: 18 additions & 3 deletions cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,21 @@ func init() {
RootCmd.AddCommand(listCmd)
}

func listRun(cmd *cobra.Command, args []string) error {
func listProfiles() (lib.Profiles, error) {
config, err := lib.NewConfigFromEnv()
if err != nil {
return err
return nil, err
}

profiles, err := config.Parse()
if err != nil {
return err
return nil, err
}

return profiles, nil
}

func listProfileNames(profiles lib.Profiles) []string {
// Let's sort this list of profiles so we can have some more deterministic output:
var profileNames []string

Expand All @@ -42,6 +46,17 @@ func listRun(cmd *cobra.Command, args []string) error {

sort.Strings(profileNames)

return profileNames
}

func listRun(cmd *cobra.Command, args []string) error {
profiles, err := listProfiles()
if err != nil {
return err
}

profileNames := listProfileNames(profiles)

w := new(tabwriter.Writer)
w.Init(os.Stdout, 0, 8, 2, '\t', 0)
fmt.Fprintln(w, "PROFILE\tARN\tSOURCE_ROLE\t")
Expand Down