mirror of https://github.com/dexidp/dex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
336 lines
9.6 KiB
336 lines
9.6 KiB
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) |
|
}
|
|
|