Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support dynamic completion #133

Merged
merged 4 commits into from Dec 1, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 8 additions & 3 deletions README.md
Expand Up @@ -32,13 +32,18 @@ See the [examples](examples) folder
## Shell autocompletion
Source the completion script in your shell commands file:

Bash
Bash Linux:

```echo 'source <(kaf completion bash)' >> ~/.bashrc```
```kaf completion bash > /etc/bash_completion.d/kaf```

Bash MacOS:

```kaf completion bash > /usr/local/etc/bash_completion.d/kaf```

Zsh

```echo 'source <(kaf completion zsh)' >> ~/.zshrc```
```kaf completion zsh > "${fpath[1]}/_kaf"```

Powershell

```Invoke-Expression (@(kaf completion powershell) -replace " ''\)$"," ' ')" -join "`n")```
221 changes: 25 additions & 196 deletions cmd/kaf/completion.go
@@ -1,9 +1,6 @@
package main

import (
"io"
"text/template"

"github.com/spf13/cobra"
)

Expand All @@ -12,10 +9,30 @@ func init() {
}

var completionCmd = &cobra.Command{
Use: "completion [SHELL]",
Short: "Generate bash completion script for bash or zsh",
Args: cobra.ExactValidArgs(1),
ValidArgs: []string{"bash", "zsh", "powershell"},
Use: "completion [SHELL]",
Short: "Generate completion script for bash, zsh or powershell",
Long: `To load completions:

Bash:

$ source <(kaf completion bash)

# To load completions for each session, execute once:
Linux:
$ kaf completion bash > /etc/bash_completion.d/kaf
MacOS:
$ kaf completion bash > /usr/local/etc/bash_completion.d/kaf

Zsh:

# To load completions for each session, execute once:
$ kaf completion zsh > "${fpath[1]}/_kaf"

# You will need to start a new shell for this setup to take effect.
`,
DisableFlagsInUseLine: true,
Args: cobra.ExactValidArgs(1),
ValidArgs: []string{"bash", "zsh", "powershell"},
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
Expand All @@ -24,7 +41,7 @@ var completionCmd = &cobra.Command{
errorExit("Failed to generate bash completion: %w", err)
}
case "zsh":
if err := genZshCompletion(outWriter); err != nil {
if err := rootCmd.GenZshCompletion(outWriter); err != nil {
errorExit("Failed to generate zsh completion: %w", err)
}
case "powershell":
Expand All @@ -35,191 +52,3 @@ var completionCmd = &cobra.Command{
}
},
}

// GenZshCompletion generates a zsh completion and writes to the passed writer.
// NOTE: Native zsh autocompletion is currently broken in cobra package.
// This is workaround taken from https://github.com/spf13/cobra/pull/828
// For more info see https://github.com/spf13/cobra/issues/107#issuecomment-482143494
func genZshCompletion(w io.Writer) error {
tpl := template.Must(template.New("head").Parse(zshHead))
template.Must(tpl.New("tail").Parse(zshTail))

if err := tpl.ExecuteTemplate(w, "head", rootCmd); err != nil {
return err
}
if err := rootCmd.GenBashCompletion(w); err != nil {
return err
}
return tpl.ExecuteTemplate(w, "tail", rootCmd)
}

const (
bashCompletion = `
__kaf_config_use_cluster() {
if out=$( ./kaf config get-clusters --no-headers ); then
COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
fi
}

__kaf_topics() {
if out=$( ./kaf topics --no-headers 2>/dev/null | awk '{print $1}' ); then
COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
fi
}

__kaf_groups() {
if out=$( ./kaf groups --no-headers 2>/dev/null | awk '{print $1}' ); then
COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
fi
}

__kaf_custom_func() {
case ${last_command} in
kaf_config_use-cluster)
__kaf_config_use_cluster
;;
kaf_consume | kaf_produce | kaf_topic_add-config | kaf_topic_delete | kaf_topic_describe)
__kaf_topics
;;
kaf_group_commit | kaf_group_delete | kaf_group_describe)
__kaf_groups
;;
*)
;;
esac
}
`

zshHead = `#compdef {{.Name}}
__cobra_bash_source() {
alias shopt=':'
alias _expand=_bash_expand
alias _complete=_bash_comp
emulate -L sh
setopt kshglob noshglob braceexpand
source "$@"
}
__cobra_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" = "__cobra_compopt" ]; then
echo builtin
return 0
fi
fi
type "$@"
}
__cobra_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
}
__cobra_compopt() {
true # don't do anything. Not supported by bashcompinit in zsh
}
__cobra_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
}
__cobra_get_comp_words_by_ref() {
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[${COMP_CWORD}-1]}"
words=("${COMP_WORDS[@]}")
cword=("${COMP_CWORD[@]}")
}
__cobra_filedir() {
local RET OLD_IFS w qw
__cobra_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="," __cobra_debug "RET=${RET[@]} len=${#RET[@]}"
for w in ${RET[@]}; do
if [[ ! "${w}" = "${cur}"* ]]; then
continue
fi
if eval "[[ \"\${w}\" = *.$1 || -d \"\${w}\" ]]"; then
qw="$(__cobra_quote "${w}")"
if [ -d "${w}" ]; then
COMPREPLY+=("${qw}/")
else
COMPREPLY+=("${qw}")
fi
fi
done
}
__cobra_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
__{{.Name}}_convert_bash_to_zsh() {
sed \
-e 's/declare -F/whence -w/' \
-e 's/_get_comp_words_by_ref "\$@"/_get_comp_words_by_ref "\$*"/' \
-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}/__cobra_filedir/g" \
-e "s/${LWORD}_get_comp_words_by_ref${RWORD}/__cobra_get_comp_words_by_ref/g" \
-e "s/${LWORD}__ltrim_colon_completions${RWORD}/__cobra_ltrim_colon_completions/g" \
-e "s/${LWORD}compgen${RWORD}/__cobra_compgen/g" \
-e "s/${LWORD}compopt${RWORD}/__cobra_compopt/g" \
-e "s/${LWORD}declare${RWORD}/builtin declare/g" \
-e "s/\\\$(type${RWORD}/\$(__cobra_type/g" \
<<'BASH_COMPLETION_EOF'
`

zshTail = `
BASH_COMPLETION_EOF
}
__cobra_bash_source <(__{{.Name}}_convert_bash_to_zsh)
_complete {{.Name}} 2>/dev/null
`
)
22 changes: 16 additions & 6 deletions cmd/kaf/config.go
Expand Up @@ -48,10 +48,19 @@ var configCurrentContext = &cobra.Command{
},
}

func validConfigArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
clusterList := make([]string, 0, len(cfg.Clusters))
for _, cluster := range cfg.Clusters {
clusterList = append(clusterList, cluster.Name)
}
return clusterList, cobra.ShellCompDirectiveNoFileComp
}

var configUseCmd = &cobra.Command{
Use: "use-cluster [NAME]",
Short: "Sets the current cluster in the configuration",
Args: cobra.ExactArgs(1),
Use: "use-cluster [NAME]",
Short: "Sets the current cluster in the configuration",
Args: cobra.ExactArgs(1),
ValidArgsFunction: validConfigArgs,
Run: func(cmd *cobra.Command, args []string) {
name := args[0]
if err := cfg.SetCurrentCluster(name); err != nil {
Expand Down Expand Up @@ -179,9 +188,10 @@ var configAddClusterCmd = &cobra.Command{
}

var configRemoveClusterCmd = &cobra.Command{
Use: "remove-cluster [NAME]",
Short: "remove cluster",
Args: cobra.ExactArgs(1),
Use: "remove-cluster [NAME]",
Short: "remove cluster",
Args: cobra.ExactArgs(1),
ValidArgsFunction: validConfigArgs,
Run: func(cmd *cobra.Command, args []string) {
name := args[0]

Expand Down
28 changes: 22 additions & 6 deletions cmd/kaf/group.go
Expand Up @@ -58,9 +58,10 @@ var groupsCmd = &cobra.Command{
}

var groupDeleteCmd = &cobra.Command{
Use: "delete",
Short: "Delete group",
Args: cobra.MaximumNArgs(1),
Use: "delete",
Short: "Delete group",
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: validGroupArgs,
Run: func(cmd *cobra.Command, args []string) {
admin := getClusterAdmin()
var group string
Expand Down Expand Up @@ -276,10 +277,25 @@ var groupLsCmd = &cobra.Command{
},
}

func validGroupArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
admin := getClusterAdmin()

groups, err := admin.ListConsumerGroups()
if err != nil {
errorExit("Unable to list consumer groups: %v\n", err)
}
groupList := make([]string, 0, len(groups))
for grp := range groups {
groupList = append(groupList, grp)
}
return groupList, cobra.ShellCompDirectiveNoFileComp
}

var groupDescribeCmd = &cobra.Command{
Use: "describe",
Short: "Describe consumer group",
Args: cobra.ExactArgs(1),
Use: "describe",
Short: "Describe consumer group",
Args: cobra.ExactArgs(1),
ValidArgsFunction: validGroupArgs,
Run: func(cmd *cobra.Command, args []string) {
// TODO List: This API can be used to find the current groups managed by a broker. To get a list of all groups in the cluster, you must send ListGroup to all brokers.
// same goes probably for topics
Expand Down
5 changes: 2 additions & 3 deletions cmd/kaf/kaf.go
Expand Up @@ -117,9 +117,8 @@ var (
)

var rootCmd = &cobra.Command{
Use: "kaf",
Short: "Kafka Command Line utility for cluster management",
BashCompletionFunction: bashCompletion,
Use: "kaf",
Short: "Kafka Command Line utility for cluster management",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
outWriter = cmd.OutOrStdout()
errWriter = cmd.ErrOrStderr()
Expand Down
2 changes: 1 addition & 1 deletion cmd/kaf/scram_client.go
Expand Up @@ -33,4 +33,4 @@ func (x *XDGSCRAMClient) Step(challenge string) (response string, err error) {

func (x *XDGSCRAMClient) Done() bool {
return x.ClientConversation.Done()
}
}