vendor/github.com/spf13/cobra/zsh_completions.go
changeset 251 1c52a0eeb952
parent 242 2a9ec03fe5a1
child 256 6d9efbef00a9
--- a/vendor/github.com/spf13/cobra/zsh_completions.go	Wed Sep 18 19:17:42 2019 +0200
+++ b/vendor/github.com/spf13/cobra/zsh_completions.go	Sun Feb 16 18:54:01 2020 +0100
@@ -1,13 +1,102 @@
 package cobra
 
 import (
-	"bytes"
+	"encoding/json"
 	"fmt"
 	"io"
 	"os"
+	"sort"
 	"strings"
+	"text/template"
+
+	"github.com/spf13/pflag"
 )
 
+const (
+	zshCompArgumentAnnotation   = "cobra_annotations_zsh_completion_argument_annotation"
+	zshCompArgumentFilenameComp = "cobra_annotations_zsh_completion_argument_file_completion"
+	zshCompArgumentWordComp     = "cobra_annotations_zsh_completion_argument_word_completion"
+	zshCompDirname              = "cobra_annotations_zsh_dirname"
+)
+
+var (
+	zshCompFuncMap = template.FuncMap{
+		"genZshFuncName":              zshCompGenFuncName,
+		"extractFlags":                zshCompExtractFlag,
+		"genFlagEntryForZshArguments": zshCompGenFlagEntryForArguments,
+		"extractArgsCompletions":      zshCompExtractArgumentCompletionHintsForRendering,
+	}
+	zshCompletionText = `
+{{/* should accept Command (that contains subcommands) as parameter */}}
+{{define "argumentsC" -}}
+{{ $cmdPath := genZshFuncName .}}
+function {{$cmdPath}} {
+  local -a commands
+
+  _arguments -C \{{- range extractFlags .}}
+    {{genFlagEntryForZshArguments .}} \{{- end}}
+    "1: :->cmnds" \
+    "*::arg:->args"
+
+  case $state in
+  cmnds)
+    commands=({{range .Commands}}{{if not .Hidden}}
+      "{{.Name}}:{{.Short}}"{{end}}{{end}}
+    )
+    _describe "command" commands
+    ;;
+  esac
+
+  case "$words[1]" in {{- range .Commands}}{{if not .Hidden}}
+  {{.Name}})
+    {{$cmdPath}}_{{.Name}}
+    ;;{{end}}{{end}}
+  esac
+}
+{{range .Commands}}{{if not .Hidden}}
+{{template "selectCmdTemplate" .}}
+{{- end}}{{end}}
+{{- end}}
+
+{{/* should accept Command without subcommands as parameter */}}
+{{define "arguments" -}}
+function {{genZshFuncName .}} {
+{{"  _arguments"}}{{range extractFlags .}} \
+    {{genFlagEntryForZshArguments . -}}
+{{end}}{{range extractArgsCompletions .}} \
+    {{.}}{{end}}
+}
+{{end}}
+
+{{/* dispatcher for commands with or without subcommands */}}
+{{define "selectCmdTemplate" -}}
+{{if .Hidden}}{{/* ignore hidden*/}}{{else -}}
+{{if .Commands}}{{template "argumentsC" .}}{{else}}{{template "arguments" .}}{{end}}
+{{- end}}
+{{- end}}
+
+{{/* template entry point */}}
+{{define "Main" -}}
+#compdef _{{.Name}} {{.Name}}
+
+{{template "selectCmdTemplate" .}}
+{{end}}
+`
+)
+
+// zshCompArgsAnnotation is used to encode/decode zsh completion for
+// arguments to/from Command.Annotations.
+type zshCompArgsAnnotation map[int]zshCompArgHint
+
+type zshCompArgHint struct {
+	// Indicates the type of the completion to use. One of:
+	// zshCompArgumentFilenameComp or zshCompArgumentWordComp
+	Tipe string `json:"type"`
+
+	// A value for the type above (globs for file completion or words)
+	Options []string `json:"options"`
+}
+
 // GenZshCompletionFile generates zsh completion file.
 func (c *Command) GenZshCompletionFile(filename string) error {
 	outFile, err := os.Create(filename)
@@ -19,108 +108,229 @@
 	return c.GenZshCompletion(outFile)
 }
 
-// GenZshCompletion generates a zsh completion file and writes to the passed writer.
+// GenZshCompletion generates a zsh completion file and writes to the passed
+// writer. The completion always run on the root command regardless of the
+// command it was called from.
 func (c *Command) GenZshCompletion(w io.Writer) error {
-	buf := new(bytes.Buffer)
-
-	writeHeader(buf, c)
-	maxDepth := maxDepth(c)
-	writeLevelMapping(buf, maxDepth)
-	writeLevelCases(buf, maxDepth, c)
-
-	_, err := buf.WriteTo(w)
-	return err
+	tmpl, err := template.New("Main").Funcs(zshCompFuncMap).Parse(zshCompletionText)
+	if err != nil {
+		return fmt.Errorf("error creating zsh completion template: %v", err)
+	}
+	return tmpl.Execute(w, c.Root())
 }
 
-func writeHeader(w io.Writer, cmd *Command) {
-	fmt.Fprintf(w, "#compdef %s\n\n", cmd.Name())
+// MarkZshCompPositionalArgumentFile marks the specified argument (first
+// argument is 1) as completed by file selection. patterns (e.g. "*.txt") are
+// optional - if not provided the completion will search for all files.
+func (c *Command) MarkZshCompPositionalArgumentFile(argPosition int, patterns ...string) error {
+	if argPosition < 1 {
+		return fmt.Errorf("Invalid argument position (%d)", argPosition)
+	}
+	annotation, err := c.zshCompGetArgsAnnotations()
+	if err != nil {
+		return err
+	}
+	if c.zshcompArgsAnnotationnIsDuplicatePosition(annotation, argPosition) {
+		return fmt.Errorf("Duplicate annotation for positional argument at index %d", argPosition)
+	}
+	annotation[argPosition] = zshCompArgHint{
+		Tipe:    zshCompArgumentFilenameComp,
+		Options: patterns,
+	}
+	return c.zshCompSetArgsAnnotations(annotation)
 }
 
-func maxDepth(c *Command) int {
-	if len(c.Commands()) == 0 {
-		return 0
+// MarkZshCompPositionalArgumentWords marks the specified positional argument
+// (first argument is 1) as completed by the provided words. At east one word
+// must be provided, spaces within words will be offered completion with
+// "word\ word".
+func (c *Command) MarkZshCompPositionalArgumentWords(argPosition int, words ...string) error {
+	if argPosition < 1 {
+		return fmt.Errorf("Invalid argument position (%d)", argPosition)
+	}
+	if len(words) == 0 {
+		return fmt.Errorf("Trying to set empty word list for positional argument %d", argPosition)
+	}
+	annotation, err := c.zshCompGetArgsAnnotations()
+	if err != nil {
+		return err
+	}
+	if c.zshcompArgsAnnotationnIsDuplicatePosition(annotation, argPosition) {
+		return fmt.Errorf("Duplicate annotation for positional argument at index %d", argPosition)
+	}
+	annotation[argPosition] = zshCompArgHint{
+		Tipe:    zshCompArgumentWordComp,
+		Options: words,
 	}
-	maxDepthSub := 0
-	for _, s := range c.Commands() {
-		subDepth := maxDepth(s)
-		if subDepth > maxDepthSub {
-			maxDepthSub = subDepth
+	return c.zshCompSetArgsAnnotations(annotation)
+}
+
+func zshCompExtractArgumentCompletionHintsForRendering(c *Command) ([]string, error) {
+	var result []string
+	annotation, err := c.zshCompGetArgsAnnotations()
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range annotation {
+		s, err := zshCompRenderZshCompArgHint(k, v)
+		if err != nil {
+			return nil, err
+		}
+		result = append(result, s)
+	}
+	if len(c.ValidArgs) > 0 {
+		if _, positionOneExists := annotation[1]; !positionOneExists {
+			s, err := zshCompRenderZshCompArgHint(1, zshCompArgHint{
+				Tipe:    zshCompArgumentWordComp,
+				Options: c.ValidArgs,
+			})
+			if err != nil {
+				return nil, err
+			}
+			result = append(result, s)
 		}
 	}
-	return 1 + maxDepthSub
+	sort.Strings(result)
+	return result, nil
 }
 
-func writeLevelMapping(w io.Writer, numLevels int) {
-	fmt.Fprintln(w, `_arguments \`)
-	for i := 1; i <= numLevels; i++ {
-		fmt.Fprintf(w, `  '%d: :->level%d' \`, i, i)
-		fmt.Fprintln(w)
+func zshCompRenderZshCompArgHint(i int, z zshCompArgHint) (string, error) {
+	switch t := z.Tipe; t {
+	case zshCompArgumentFilenameComp:
+		var globs []string
+		for _, g := range z.Options {
+			globs = append(globs, fmt.Sprintf(`-g "%s"`, g))
+		}
+		return fmt.Sprintf(`'%d: :_files %s'`, i, strings.Join(globs, " ")), nil
+	case zshCompArgumentWordComp:
+		var words []string
+		for _, w := range z.Options {
+			words = append(words, fmt.Sprintf("%q", w))
+		}
+		return fmt.Sprintf(`'%d: :(%s)'`, i, strings.Join(words, " ")), nil
+	default:
+		return "", fmt.Errorf("Invalid zsh argument completion annotation: %s", t)
 	}
-	fmt.Fprintf(w, `  '%d: :%s'`, numLevels+1, "_files")
-	fmt.Fprintln(w)
+}
+
+func (c *Command) zshcompArgsAnnotationnIsDuplicatePosition(annotation zshCompArgsAnnotation, position int) bool {
+	_, dup := annotation[position]
+	return dup
 }
 
-func writeLevelCases(w io.Writer, maxDepth int, root *Command) {
-	fmt.Fprintln(w, "case $state in")
-	defer fmt.Fprintln(w, "esac")
+func (c *Command) zshCompGetArgsAnnotations() (zshCompArgsAnnotation, error) {
+	annotation := make(zshCompArgsAnnotation)
+	annotationString, ok := c.Annotations[zshCompArgumentAnnotation]
+	if !ok {
+		return annotation, nil
+	}
+	err := json.Unmarshal([]byte(annotationString), &annotation)
+	if err != nil {
+		return annotation, fmt.Errorf("Error unmarshaling zsh argument annotation: %v", err)
+	}
+	return annotation, nil
+}
+
+func (c *Command) zshCompSetArgsAnnotations(annotation zshCompArgsAnnotation) error {
+	jsn, err := json.Marshal(annotation)
+	if err != nil {
+		return fmt.Errorf("Error marshaling zsh argument annotation: %v", err)
+	}
+	if c.Annotations == nil {
+		c.Annotations = make(map[string]string)
+	}
+	c.Annotations[zshCompArgumentAnnotation] = string(jsn)
+	return nil
+}
 
-	for i := 1; i <= maxDepth; i++ {
-		fmt.Fprintf(w, "  level%d)\n", i)
-		writeLevel(w, root, i)
-		fmt.Fprintln(w, "  ;;")
+func zshCompGenFuncName(c *Command) string {
+	if c.HasParent() {
+		return zshCompGenFuncName(c.Parent()) + "_" + c.Name()
 	}
-	fmt.Fprintln(w, "  *)")
-	fmt.Fprintln(w, "    _arguments '*: :_files'")
-	fmt.Fprintln(w, "  ;;")
+	return "_" + c.Name()
+}
+
+func zshCompExtractFlag(c *Command) []*pflag.Flag {
+	var flags []*pflag.Flag
+	c.LocalFlags().VisitAll(func(f *pflag.Flag) {
+		if !f.Hidden {
+			flags = append(flags, f)
+		}
+	})
+	c.InheritedFlags().VisitAll(func(f *pflag.Flag) {
+		if !f.Hidden {
+			flags = append(flags, f)
+		}
+	})
+	return flags
+}
+
+// zshCompGenFlagEntryForArguments returns an entry that matches _arguments
+// zsh-completion parameters. It's too complicated to generate in a template.
+func zshCompGenFlagEntryForArguments(f *pflag.Flag) string {
+	if f.Name == "" || f.Shorthand == "" {
+		return zshCompGenFlagEntryForSingleOptionFlag(f)
+	}
+	return zshCompGenFlagEntryForMultiOptionFlag(f)
 }
 
-func writeLevel(w io.Writer, root *Command, i int) {
-	fmt.Fprintf(w, "    case $words[%d] in\n", i)
-	defer fmt.Fprintln(w, "    esac")
+func zshCompGenFlagEntryForSingleOptionFlag(f *pflag.Flag) string {
+	var option, multiMark, extras string
 
-	commands := filterByLevel(root, i)
-	byParent := groupByParent(commands)
+	if zshCompFlagCouldBeSpecifiedMoreThenOnce(f) {
+		multiMark = "*"
+	}
 
-	for p, c := range byParent {
-		names := names(c)
-		fmt.Fprintf(w, "      %s)\n", p)
-		fmt.Fprintf(w, "        _arguments '%d: :(%s)'\n", i, strings.Join(names, " "))
-		fmt.Fprintln(w, "      ;;")
+	option = "--" + f.Name
+	if option == "--" {
+		option = "-" + f.Shorthand
 	}
-	fmt.Fprintln(w, "      *)")
-	fmt.Fprintln(w, "        _arguments '*: :_files'")
-	fmt.Fprintln(w, "      ;;")
+	extras = zshCompGenFlagEntryExtras(f)
 
+	return fmt.Sprintf(`'%s%s[%s]%s'`, multiMark, option, zshCompQuoteFlagDescription(f.Usage), extras)
 }
 
-func filterByLevel(c *Command, l int) []*Command {
-	cs := make([]*Command, 0)
-	if l == 0 {
-		cs = append(cs, c)
-		return cs
+func zshCompGenFlagEntryForMultiOptionFlag(f *pflag.Flag) string {
+	var options, parenMultiMark, curlyMultiMark, extras string
+
+	if zshCompFlagCouldBeSpecifiedMoreThenOnce(f) {
+		parenMultiMark = "*"
+		curlyMultiMark = "\\*"
 	}
-	for _, s := range c.Commands() {
-		cs = append(cs, filterByLevel(s, l-1)...)
-	}
-	return cs
+
+	options = fmt.Sprintf(`'(%s-%s %s--%s)'{%s-%s,%s--%s}`,
+		parenMultiMark, f.Shorthand, parenMultiMark, f.Name, curlyMultiMark, f.Shorthand, curlyMultiMark, f.Name)
+	extras = zshCompGenFlagEntryExtras(f)
+
+	return fmt.Sprintf(`%s'[%s]%s'`, options, zshCompQuoteFlagDescription(f.Usage), extras)
 }
 
-func groupByParent(commands []*Command) map[string][]*Command {
-	m := make(map[string][]*Command)
-	for _, c := range commands {
-		parent := c.Parent()
-		if parent == nil {
-			continue
+func zshCompGenFlagEntryExtras(f *pflag.Flag) string {
+	if f.NoOptDefVal != "" {
+		return ""
+	}
+
+	extras := ":" // allow options for flag (even without assistance)
+	for key, values := range f.Annotations {
+		switch key {
+		case zshCompDirname:
+			extras = fmt.Sprintf(":filename:_files -g %q", values[0])
+		case BashCompFilenameExt:
+			extras = ":filename:_files"
+			for _, pattern := range values {
+				extras = extras + fmt.Sprintf(` -g "%s"`, pattern)
+			}
 		}
-		m[parent.Name()] = append(m[parent.Name()], c)
 	}
-	return m
+
+	return extras
 }
 
-func names(commands []*Command) []string {
-	ns := make([]string, len(commands))
-	for i, c := range commands {
-		ns[i] = c.Name()
-	}
-	return ns
+func zshCompFlagCouldBeSpecifiedMoreThenOnce(f *pflag.Flag) bool {
+	return strings.Contains(f.Value.Type(), "Slice") ||
+		strings.Contains(f.Value.Type(), "Array")
 }
+
+func zshCompQuoteFlagDescription(s string) string {
+	return strings.Replace(s, "'", `'\''`, -1)
+}