From 37bd6e8b54b139f2a8c7686b23d4182edbb8a5bc Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Sun, 29 Mar 2020 21:48:37 -0400 Subject: [PATCH] Zsh completion V2 using Go completion The current Zsh completion support is not well aligned with Bash completion and requires to have special code for Zsh, and different code for Bash. This commit introduces a V2 version which is based on Custom Go Completions and aims to standardize completion definition and behaviour across the different shells (Bash, Zsh, Fish). Signed-off-by: Marc Khouzam --- zsh_completions.md | 43 +++++++++++-- zsh_completions_v2.go | 145 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 zsh_completions_v2.go diff --git a/zsh_completions.md b/zsh_completions.md index df9c2eac9..7cbb9efbb 100644 --- a/zsh_completions.md +++ b/zsh_completions.md @@ -4,7 +4,41 @@ Cobra supports native Zsh completion generated from the root `cobra.Command`. The generated completion script should be put somewhere in your `$fpath` named `_`. -### What's Supported +Cobra now provides a V2 version for Zsh completion. The V2 version addresses +some limitations of the first version, which had some incompatibilities +between Zsh-completion and Bash-completion. Furthermore, the V2 version +supports custom completions implemented using `ValidArgsFunction` and +`RegisterFlagCompletionFunc()`. + +To take advantage the V2 version you should use the `command.GenZshCompletionV2()` +or `command.GenZshCompletionFileV2()` functions. You must provide these functions +with a parameter indicating if the completions should be annotated with a +description; Cobra will provide the description automatically, based on usage +information. You can choose to make this option configurable by your users. + +The original Zsh completion (`command.GenZshCompletion()` and +`command.GenZshCompletionFile()`) is retained for backwards-compatibility. + +### Limitations + +* Custom completions implemented in Bash scripting are not supported. You should +instead use `ValidArgsFunction` and `RegisterFlagCompletionFunc()` which are supported +across all shells (bash, zsh, fish). +* Bash-completion annotations for flags are not currently supported: + * The family of functions `MarkFlag...()` and `MarkPersistentFlag...()` which correspond to the below annotations + * `BashCompCustom` (which has been superseded by `RegisterFlagCompletionFunc()`) + * `BashCompFilenameExt` (filtering by file extension) + * `BashCompOneRequiredFlag` (required flags) + * `BashCompSubdirsInDir` (filtering by directory) +* The Zsh-specific functions are not supported (as they are not standard across different shells): + * `MarkZshCompPositionalArgumentWords` (which is superseded by `ValidArgs`) + * `MarkZshCompPositionalArgumentFile` (filtering of arguments by file extension) + +### Legacy version + +The below information pertains to the legacy Zsh-completion support. + +#### What's Supported * Completion for all non-hidden subcommands using their `.Short` description. * Completion for all non-hidden flags using the following rules: @@ -31,9 +65,6 @@ The generated completion script should be put somewhere in your `$fpath` named completion options for 1st argument. * Argument completions only offered for commands with no subcommands. -### What's not yet Supported +#### What's not Supported -* Custom completion scripts are not supported yet (We should probably create zsh - specific one, doesn't make sense to re-use the bash one as the functions will - be different). -* Whatever other feature you're looking for and doesn't exist :) +* Custom completion scripts are not supported diff --git a/zsh_completions_v2.go b/zsh_completions_v2.go new file mode 100644 index 000000000..d1c17a7c3 --- /dev/null +++ b/zsh_completions_v2.go @@ -0,0 +1,145 @@ +package cobra + +import ( + "bytes" + "fmt" + "io" + "os" +) + +func genZshCompV2(buf *bytes.Buffer, name string, includeDesc bool) { + compCmd := ShellCompRequestCmd + if !includeDesc { + compCmd = ShellCompNoDescRequestCmd + } + buf.WriteString(fmt.Sprintf(`#compdef __%[1]s %[1]s + +# zsh completion for %-36[1]s -*- shell-script -*- + +__%[1]s_debug() +{ + local file="$BASH_COMP_DEBUG_FILE" + if [[ -n ${file} ]]; then + echo "$*" >> "${file}" + fi +} + +_%[1]s() +{ + local lastParam lastChar flagPrefix requestComp out directive compCount comp lastComp + local -a completions + + __%[1]s_debug "\n========= starting completion logic ==========" + __%[1]s_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" + + lastParam=${words[-1]} + lastChar=${lastParam[-1]} + __%[1]s_debug "lastParam: ${lastParam}, lastChar: ${lastChar}" + + # For zsh, when completing a flag with an = (e.g., %[1]s -n=) + # completions must be prefixed with the flag + setopt local_options BASH_REMATCH + if [[ "${lastParam}" =~ '-.*=' ]]; then + # We are dealing with a flag with an = + flagPrefix=${BASH_REMATCH} + fi + + # Prepare the command to obtain completions + requestComp="${words[1]} %[2]s ${words[2,-1]}" + if [ "${lastChar}" = "" ]; then + # If the last parameter is complete (there is a space following it) + # We add an extra empty parameter so we can indicate this to the go completion code. + __%[1]s_debug "Adding extra empty parameter" + requestComp="${requestComp} \"\"" + fi + + __%[1]s_debug "About to call: eval ${requestComp}" + + # Use eval to handle any environment variables and such + out=$(eval ${requestComp} 2>/dev/null) + __%[1]s_debug "completion output: ${out}" + + # Extract the directive integer following a : from the last line + if [ "${out[-2]}" = : ]; then + directive=${out[-1]} + # Remove the directive (that means the last 3 chars as we include the : and the newline) + out=${out[1,-4]} + else + # There is no directive specified. Leave $out as is. + __%[1]s_debug "No directive found. Setting do default" + directive=0 + fi + + __%[1]s_debug "directive: ${directive}" + __%[1]s_debug "completions: ${out}" + __%[1]s_debug "flagPrefix: ${flagPrefix}" + + if [ $((directive & %[3]d)) -ne 0 ]; then + __%[1]s_debug "Completion received error. Ignoring completions." + return + fi + + compCount=0 + while IFS='\n' read -r comp; do + if [ -n "$comp" ]; then + # If requested, completions are returned with a description. + # The description is preceded by a TAB character. + # For zsh's _describe, we need to use a : instead of a TAB. + # We first need to escape any : as part of the completion itself. + comp=${comp//:/\\:} + + local tab=$(printf '\t') + comp=${comp//$tab/:} + + ((compCount++)) + if [ -n "$flagPrefix" ]; then + __%[1]s_debug "Adding completion: ${flagPrefix}${comp}" + completions+="${flagPrefix}${comp}" + else + __%[1]s_debug "Adding completion: ${comp}" + completions+=${comp} + fi + lastComp=$comp + fi + done < <(printf "%%s\n" "${out[@]}") + + if [ ${compCount} -eq 0 ]; then + if [ $((directive & %[5]d)) -ne 0 ]; then + __%[1]s_debug "deactivating file completion" + else + # Perform file completion + __%[1]s_debug "activating file completion" + _arguments '*:filename:_files' + fi + elif [ $((directive & %[4]d)) -ne 0 ] && [ ${compCount} -eq 1 ]; then + __%[1]s_debug "Activating nospace." + # We can use compadd here as there is no description when + # there is only one completion. + compadd -S '' "${lastComp}" + else + _describe "completions" completions + fi +} + +compdef _%[1]s %[1]s +`, name, compCmd, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp)) +} + +// GenZshCompletionV2 generates the zsh completion V2 file and writes to the passed writer. +func (c *Command) GenZshCompletionV2(w io.Writer, includeDesc bool) error { + buf := new(bytes.Buffer) + genZshCompV2(buf, c.Name(), includeDesc) + _, err := buf.WriteTo(w) + return err +} + +// GenZshCompletionFileV2 generates the zsh completion V2 file. +func (c *Command) GenZshCompletionFileV2(filename string, includeDesc bool) error { + outFile, err := os.Create(filename) + if err != nil { + return err + } + defer outFile.Close() + + return c.GenZshCompletionV2(outFile, includeDesc) +}