| package cobra |
| |
| import ( |
| "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) |
| if err != nil { |
| return err |
| } |
| defer outFile.Close() |
| |
| return c.GenZshCompletion(outFile) |
| } |
| |
| // 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 { |
| 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()) |
| } |
| |
| // 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) |
| } |
| |
| // 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, |
| } |
| 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) |
| } |
| } |
| sort.Strings(result) |
| return result, nil |
| } |
| |
| 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) |
| } |
| } |
| |
| func (c *Command) zshcompArgsAnnotationnIsDuplicatePosition(annotation zshCompArgsAnnotation, position int) bool { |
| _, dup := annotation[position] |
| return dup |
| } |
| |
| 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 |
| } |
| |
| func zshCompGenFuncName(c *Command) string { |
| if c.HasParent() { |
| return zshCompGenFuncName(c.Parent()) + "_" + c.Name() |
| } |
| 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 zshCompGenFlagEntryForSingleOptionFlag(f *pflag.Flag) string { |
| var option, multiMark, extras string |
| |
| if zshCompFlagCouldBeSpecifiedMoreThenOnce(f) { |
| multiMark = "*" |
| } |
| |
| option = "--" + f.Name |
| if option == "--" { |
| option = "-" + f.Shorthand |
| } |
| extras = zshCompGenFlagEntryExtras(f) |
| |
| return fmt.Sprintf(`'%s%s[%s]%s'`, multiMark, option, zshCompQuoteFlagDescription(f.Usage), extras) |
| } |
| |
| func zshCompGenFlagEntryForMultiOptionFlag(f *pflag.Flag) string { |
| var options, parenMultiMark, curlyMultiMark, extras string |
| |
| if zshCompFlagCouldBeSpecifiedMoreThenOnce(f) { |
| parenMultiMark = "*" |
| curlyMultiMark = "\\*" |
| } |
| |
| 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 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) |
| } |
| } |
| } |
| |
| return extras |
| } |
| |
| 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) |
| } |