Skip to content

Commit

Permalink
Zsh completion V2 using Go completion
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
marckhouzam committed Apr 14, 2020
1 parent 8c638d3 commit 93e8ceb
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 2 deletions.
37 changes: 35 additions & 2 deletions zsh_completions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,40 @@ Cobra supports native Zsh completion generated from the root `cobra.Command`.
The generated completion script should be put somewhere in your `$fpath` named
`_<YOUR COMMAND>`.

### What's Supported
Cobra now provides a V2 version for Zsh completion. The V2 version addresses
some limitations of the first version, which caused some incompatibilities
between Zsh-completion and Bash-completion. Furthermore, the V2 version
supports custom completions implemented using the `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()` or
`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` (no filtering by file extension)
* `BashCompOneRequiredFlag` (no required flags)
* `BashCompSubdirsInDir` (no 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` (no 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:
Expand All @@ -31,7 +64,7 @@ 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 yet 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
Expand Down
146 changes: 146 additions & 0 deletions zsh_completions_v2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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=<TAB>)
# 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 : as 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 not 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."
else
compCount=0
while IFS='\n' read -r comp; do
if [ -n "$comp" ]; then
((compCount++))
if [ -n "$flagPrefix" ]; then
# We use compadd here so that we can hide the flagPrefix from the list
# of choices. We can use compadd because there is no description in this case.
__%[1]s_debug "Calling: compadd -p ${flagPrefix} ${comp}"
compadd -p ${flagPrefix} ${comp}
else
# 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/:}
__%[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
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)
}

0 comments on commit 93e8ceb

Please sign in to comment.