From dbbc48a1c31259bf56d54a82709392930a3cae24 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 25 Jul 2022 14:11:54 -0400 Subject: [PATCH] feat: implement zsh & bash completion scripts --- completion/bash.go | 165 +++++++---- completion/command.go | 47 +++- completion/shell.go | 637 ------------------------------------------ completion/zsh.go | 361 +++++++++--------------- gum.go | 1 + 5 files changed, 287 insertions(+), 924 deletions(-) delete mode 100644 completion/shell.go diff --git a/completion/bash.go b/completion/bash.go index f187bcb..63d7ac6 100644 --- a/completion/bash.go +++ b/completion/bash.go @@ -1,3 +1,4 @@ +// TODO implement our own gum bash completion package completion import ( @@ -12,13 +13,84 @@ import ( type Bash struct{} -func (b Bash) BeforeApply(app *kong.Kong) error { - return GenBashCompletion(app.Model.Node, app.Stdout) +// Run generates bash shell completion. +func (b Bash) Run(ctx *kong.Context) error { + buf := new(bytes.Buffer) + writePreamble(buf, ctx.Model.Name) + b.gen(buf, ctx.Model.Node) + writePostscript(buf, ctx.Model.Name) + + _, err := fmt.Fprint(ctx.Stdout, buf.String()) + return err +} + +// ShellCompDirective is a bit map representing the different behaviors the shell +// can be instructed to have once completions have been provided. +type ShellCompDirective int + +const ( + // ShellCompDirectiveError indicates an error occurred and completions should be ignored. + ShellCompDirectiveError ShellCompDirective = 1 << iota + + // ShellCompDirectiveNoSpace indicates that the shell should not add a space + // after the completion even if there is a single completion provided. + ShellCompDirectiveNoSpace + + // ShellCompDirectiveNoFileComp indicates that the shell should not provide + // file completion even when no completion is provided. + ShellCompDirectiveNoFileComp + + // ShellCompDirectiveFilterFileExt indicates that the provided completions + // should be used as file extension filters. + // For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename() + // is a shortcut to using this directive explicitly. The BashCompFilenameExt + // annotation can also be used to obtain the same behavior for flags. + ShellCompDirectiveFilterFileExt + + // ShellCompDirectiveFilterDirs indicates that only directory names should + // be provided in file completion. To request directory names within another + // directory, the returned completions should specify the directory within + // which to search. The BashCompSubdirsInDir annotation can be used to + // obtain the same behavior but only for flags. + ShellCompDirectiveFilterDirs + + // =========================================================================== + + // All directives using iota should be above this one. + // For internal use. + shellCompDirectiveMaxValue + + // ShellCompDirectiveDefault indicates to let the shell perform its default + // behavior after completions have been provided. + // This one must be last to avoid messing up the iota count. + ShellCompDirectiveDefault ShellCompDirective = 0 +) + +// Annotations for Bash completion. +const ( + // ShellCompNoDescRequestCmd is the name of the hidden command that is used to request + // completion results without their description. It is used by the shell completion scripts. + ShellCompNoDescRequestCmd = "completion completeNoDesc" + BashCompFilenameExt = "kong_annotation_bash_completion_filename_extensions" + BashCompCustom = "kong_annotation_bash_completion_custom" + BashCompOneRequiredFlag = "kong_annotation_bash_completion_one_required_flag" + BashCompSubdirsInDir = "kong_annotation_bash_completion_subdirs_in_dir" + + activeHelpEnvVarSuffix = "_ACTIVE_HELP" +) + +// activeHelpEnvVar returns the name of the program-specific ActiveHelp environment +// variable. It has the format _ACTIVE_HELP where is the name of the +// root command in upper case, with all - replaced by _. +func activeHelpEnvVar(name string) string { + // This format should not be changed: users will be using it explicitly. + activeHelpEnvVar := strings.ToUpper(fmt.Sprintf("%s%s", name, activeHelpEnvVarSuffix)) + return strings.ReplaceAll(activeHelpEnvVar, "-", "_") } func writePreamble(buf io.StringWriter, name string) { - WriteStringAndCheck(buf, fmt.Sprintf("# bash completion for %-36s -*- shell-script -*-\n", name)) - WriteStringAndCheck(buf, fmt.Sprintf(` + writeString(buf, fmt.Sprintf("# bash completion for %-36s -*- shell-script -*-\n", name)) + writeString(buf, fmt.Sprintf(` __%[1]s_debug() { if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then @@ -386,8 +458,8 @@ __%[1]s_handle_word() func writePostscript(buf io.StringWriter, name string) { name = strings.ReplaceAll(name, ":", "__") - WriteStringAndCheck(buf, fmt.Sprintf("__start_%s()\n", name)) - WriteStringAndCheck(buf, fmt.Sprintf(`{ + writeString(buf, fmt.Sprintf("__start_%s()\n", name)) + writeString(buf, fmt.Sprintf(`{ local cur prev words cword split declare -A flaghash 2>/dev/null || : declare -A aliashash 2>/dev/null || : @@ -417,33 +489,33 @@ func writePostscript(buf io.StringWriter, name string) { } `, name)) - WriteStringAndCheck(buf, fmt.Sprintf(`if [[ $(type -t compopt) = "builtin" ]]; then + writeString(buf, fmt.Sprintf(`if [[ $(type -t compopt) = "builtin" ]]; then complete -o default -F __start_%s %s else complete -o default -o nospace -F __start_%s %s fi `, name, name, name, name)) - WriteStringAndCheck(buf, "# ex: ts=4 sw=4 et filetype=sh\n") + writeString(buf, "# ex: ts=4 sw=4 et filetype=sh\n") } func writeCommands(buf io.StringWriter, cmd *kong.Node) { - WriteStringAndCheck(buf, " commands=()\n") + writeString(buf, " commands=()\n") for _, c := range cmd.Children { if c == nil || c.Hidden { continue } - WriteStringAndCheck(buf, fmt.Sprintf(" commands+=(%q)\n", c.Name)) + writeString(buf, fmt.Sprintf(" commands+=(%q)\n", c.Name)) writeCmdAliases(buf, c) } - WriteStringAndCheck(buf, "\n") + writeString(buf, "\n") } func writeFlagHandler(buf io.StringWriter, name string, annotations map[string][]string, cmd *kong.Node) { for key, value := range annotations { switch key { case BashCompFilenameExt: - WriteStringAndCheck(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name)) + writeString(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name)) var ext string if len(value) > 0 { @@ -451,18 +523,18 @@ func writeFlagHandler(buf io.StringWriter, name string, annotations map[string][ } else { ext = "_filedir" } - WriteStringAndCheck(buf, fmt.Sprintf(" flags_completion+=(%q)\n", ext)) + writeString(buf, fmt.Sprintf(" flags_completion+=(%q)\n", ext)) case BashCompCustom: - WriteStringAndCheck(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name)) + writeString(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name)) if len(value) > 0 { handlers := strings.Join(value, "; ") - WriteStringAndCheck(buf, fmt.Sprintf(" flags_completion+=(%q)\n", handlers)) + writeString(buf, fmt.Sprintf(" flags_completion+=(%q)\n", handlers)) } else { - WriteStringAndCheck(buf, " flags_completion+=(:)\n") + writeString(buf, " flags_completion+=(:)\n") } case BashCompSubdirsInDir: - WriteStringAndCheck(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name)) + writeString(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name)) var ext string if len(value) == 1 { @@ -470,7 +542,7 @@ func writeFlagHandler(buf io.StringWriter, name string, annotations map[string][ } else { ext = "_filedir -d" } - WriteStringAndCheck(buf, fmt.Sprintf(" flags_completion+=(%q)\n", ext)) + writeString(buf, fmt.Sprintf(" flags_completion+=(%q)\n", ext)) } } } @@ -484,7 +556,7 @@ func writeShortFlag(buf io.StringWriter, flag *kong.Flag, cmd *kong.Node) { format += "two_word_" } format += "flags+=(\"-%s" + cbn - WriteStringAndCheck(buf, fmt.Sprintf(format, name)) + writeString(buf, fmt.Sprintf(format, name)) writeFlagHandler(buf, "-"+name, map[string][]string{}, cmd) } @@ -495,10 +567,10 @@ func writeFlag(buf io.StringWriter, flag *kong.Flag, cmd *kong.Node) { format += "=" } format += cbn - WriteStringAndCheck(buf, fmt.Sprintf(format, name)) + writeString(buf, fmt.Sprintf(format, name)) if len(flag.DefaultValue.String()) == 0 { format = " two_word_flags+=(\"--%s" + cbn - WriteStringAndCheck(buf, fmt.Sprintf(format, name)) + writeString(buf, fmt.Sprintf(format, name)) } writeFlagHandler(buf, "--"+name, map[string][]string{}, cmd) } @@ -509,14 +581,14 @@ func writeLocalNonPersistentFlag(buf io.StringWriter, flag *kong.Flag) { if len(flag.DefaultValue.String()) == 0 { format += " local_nonpersistent_flags+=(\"--%[1]s=" + cbn } - WriteStringAndCheck(buf, fmt.Sprintf(format, name)) + writeString(buf, fmt.Sprintf(format, name)) if flag.Short > 0 { - WriteStringAndCheck(buf, fmt.Sprintf(" local_nonpersistent_flags+=(\"-%c\")\n", flag.Short)) + writeString(buf, fmt.Sprintf(" local_nonpersistent_flags+=(\"-%c\")\n", flag.Short)) } } func writeFlags(buf io.StringWriter, cmd *kong.Node) { - WriteStringAndCheck(buf, ` flags=() + writeString(buf, ` flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() @@ -534,7 +606,7 @@ func writeFlags(buf io.StringWriter, cmd *kong.Node) { } } - WriteStringAndCheck(buf, "\n") + writeString(buf, "\n") } func writeCmdAliases(buf io.StringWriter, cmd *kong.Node) { @@ -544,57 +616,46 @@ func writeCmdAliases(buf io.StringWriter, cmd *kong.Node) { sort.Strings(cmd.Aliases) - WriteStringAndCheck(buf, fmt.Sprint(` if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then`, "\n")) + writeString(buf, fmt.Sprint(` if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then`, "\n")) for _, value := range cmd.Aliases { - WriteStringAndCheck(buf, fmt.Sprintf(" command_aliases+=(%q)\n", value)) - WriteStringAndCheck(buf, fmt.Sprintf(" aliashash[%q]=%q\n", value, cmd.Name)) + writeString(buf, fmt.Sprintf(" command_aliases+=(%q)\n", value)) + writeString(buf, fmt.Sprintf(" aliashash[%q]=%q\n", value, cmd.Name)) } - WriteStringAndCheck(buf, ` fi`) - WriteStringAndCheck(buf, "\n") + writeString(buf, ` fi`) + writeString(buf, "\n") } func writeArgAliases(buf io.StringWriter, cmd *kong.Node) { - WriteStringAndCheck(buf, " noun_aliases=()\n") + writeString(buf, " noun_aliases=()\n") sort.Strings(cmd.Aliases) for _, value := range cmd.Aliases { - WriteStringAndCheck(buf, fmt.Sprintf(" noun_aliases+=(%q)\n", value)) + writeString(buf, fmt.Sprintf(" noun_aliases+=(%q)\n", value)) } } -func gen(buf io.StringWriter, cmd *kong.Node) { +func (b Bash) gen(buf io.StringWriter, cmd *kong.Node) { for _, c := range cmd.Children { if c == nil || c.Hidden { continue } - gen(buf, c) + b.gen(buf, c) } commandName := cmd.FullPath() commandName = strings.ReplaceAll(commandName, " ", "_") commandName = strings.ReplaceAll(commandName, ":", "__") if cmd.Parent == nil { - WriteStringAndCheck(buf, fmt.Sprintf("_%s_root_command()\n{\n", commandName)) + writeString(buf, fmt.Sprintf("_%s_root_command()\n{\n", commandName)) } else { - WriteStringAndCheck(buf, fmt.Sprintf("_%s()\n{\n", commandName)) + writeString(buf, fmt.Sprintf("_%s()\n{\n", commandName)) } - WriteStringAndCheck(buf, fmt.Sprintf(" last_command=%q\n", commandName)) - WriteStringAndCheck(buf, "\n") - WriteStringAndCheck(buf, " command_aliases=()\n") - WriteStringAndCheck(buf, "\n") + writeString(buf, fmt.Sprintf(" last_command=%q\n", commandName)) + writeString(buf, "\n") + writeString(buf, " command_aliases=()\n") + writeString(buf, "\n") writeCommands(buf, cmd) writeFlags(buf, cmd) writeArgAliases(buf, cmd) - WriteStringAndCheck(buf, "}\n\n") -} - -// GenBashCompletion generates bash completion file and writes to the passed writer. -func GenBashCompletion(c *kong.Node, w io.Writer) error { - buf := new(bytes.Buffer) - writePreamble(buf, c.Name) - gen(buf, c) - writePostscript(buf, c.Name) - - _, err := buf.WriteTo(w) - return err + writeString(buf, "}\n\n") } diff --git a/completion/command.go b/completion/command.go index b98547a..01f724a 100644 --- a/completion/command.go +++ b/completion/command.go @@ -1,8 +1,49 @@ package completion +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/alecthomas/kong" +) + type Completion struct { - Complete Complete `cmd:"" hidden:"" help:"Request shell completion"` - Bash Bash `cmd:"" help:"Generate the autocompletion script for bash"` - Zsh Zsh `cmd:"" help:"Generate the autocompletion script for zsh"` + Bash Bash `cmd:"" help:"Generate the autocompletion script for bash"` + Zsh Zsh `cmd:"" help:"Generate the autocompletion script for zsh"` // Fish Fish `cmd:"" help:"Generate the autocompletion script for fish"` } + +func commandName(cmd *kong.Node) string { + commandName := cmd.FullPath() + commandName = strings.ReplaceAll(commandName, " ", "_") + commandName = strings.ReplaceAll(commandName, ":", "__") + return commandName +} + +func hasCommands(cmd *kong.Node) bool { + for _, c := range cmd.Children { + if !c.Hidden { + return true + } + } + return false +} + +func isArgument(cmd *kong.Node) bool { + return cmd.Type == kong.ArgumentNode +} + +// writeString writes a string into a buffer, and checks if the error is not nil. +func writeString(b io.StringWriter, s string) { + _, err := b.WriteString(s) + if err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(1) + } +} + +func nonCompletableFlag(flag *kong.Flag) bool { + return flag.Hidden +} diff --git a/completion/shell.go b/completion/shell.go deleted file mode 100644 index 855e5ae..0000000 --- a/completion/shell.go +++ /dev/null @@ -1,637 +0,0 @@ -package completion - -import ( - "fmt" - "io" - "os" - "strings" - "sync" - - "github.com/alecthomas/kong" -) - -type Complete struct { - Arg []string `arg:"" passthrough:""` -} - -type flagCompError struct { - subCommand string - flagName string -} - -func (e *flagCompError) Error() string { - return "Subcommand '" + e.subCommand + "' does not support flag '" + e.flagName + "'" -} - -// ShellCompDirective is a bit map representing the different behaviors the shell -// can be instructed to have once completions have been provided. -type ShellCompDirective int - -const ( - // ShellCompDirectiveError indicates an error occurred and completions should be ignored. - ShellCompDirectiveError ShellCompDirective = 1 << iota - - // ShellCompDirectiveNoSpace indicates that the shell should not add a space - // after the completion even if there is a single completion provided. - ShellCompDirectiveNoSpace - - // ShellCompDirectiveNoFileComp indicates that the shell should not provide - // file completion even when no completion is provided. - ShellCompDirectiveNoFileComp - - // ShellCompDirectiveFilterFileExt indicates that the provided completions - // should be used as file extension filters. - // For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename() - // is a shortcut to using this directive explicitly. The BashCompFilenameExt - // annotation can also be used to obtain the same behavior for flags. - ShellCompDirectiveFilterFileExt - - // ShellCompDirectiveFilterDirs indicates that only directory names should - // be provided in file completion. To request directory names within another - // directory, the returned completions should specify the directory within - // which to search. The BashCompSubdirsInDir annotation can be used to - // obtain the same behavior but only for flags. - ShellCompDirectiveFilterDirs - - // =========================================================================== - - // All directives using iota should be above this one. - // For internal use. - shellCompDirectiveMaxValue - - // ShellCompDirectiveDefault indicates to let the shell perform its default - // behavior after completions have been provided. - // This one must be last to avoid messing up the iota count. - ShellCompDirectiveDefault ShellCompDirective = 0 -) - -// Annotations for Bash completion. -const ( - // ShellCompRequestCmd is the name of the hidden command that is used to request - // completion results from the program. It is used by the shell completion scripts. - ShellCompRequestCmd = "completion complete" - // ShellCompNoDescRequestCmd is the name of the hidden command that is used to request - // completion results without their description. It is used by the shell completion scripts. - ShellCompNoDescRequestCmd = "completion completeNoDesc" - BashCompFilenameExt = "kong_annotation_bash_completion_filename_extensions" - BashCompCustom = "kong_annotation_bash_completion_custom" - BashCompOneRequiredFlag = "kong_annotation_bash_completion_one_required_flag" - BashCompSubdirsInDir = "kong_annotation_bash_completion_subdirs_in_dir" -) - -// Global map of flag completion functions. Make sure to use flagCompletionMutex before you try to read and write from it. -var flagCompletionFunctions = map[*kong.Flag]func(cmd *kong.Node, args []string, toComplete string) ([]string, ShellCompDirective){} - -// lock for reading and writing from flagCompletionFunctions -var flagCompletionMutex = &sync.RWMutex{} - -const ( - activeHelpMarker = "_activeHelp_ " - // The below values should not be changed: programs will be using them explicitly - // in their user documentation, and users will be using them explicitly. - activeHelpEnvVarSuffix = "_ACTIVE_HELP" - activeHelpGlobalEnvVar = "KONG_ACTIVE_HELP" - activeHelpGlobalDisable = "0" -) - -// activeHelpEnvVar returns the name of the program-specific ActiveHelp environment -// variable. It has the format _ACTIVE_HELP where is the name of the -// root command in upper case, with all - replaced by _. -func activeHelpEnvVar(name string) string { - // This format should not be changed: users will be using it explicitly. - activeHelpEnvVar := strings.ToUpper(fmt.Sprintf("%s%s", name, activeHelpEnvVarSuffix)) - return strings.ReplaceAll(activeHelpEnvVar, "-", "_") -} - -// WriteStringAndCheck writes a string into a buffer, and checks if the error is not nil. -func WriteStringAndCheck(b io.StringWriter, s string) { - _, err := b.WriteString(s) - if err != nil { - fmt.Fprintln(os.Stderr, "Error:", err) - os.Exit(1) - } -} - -// Returns a string listing the different directive enabled in the specified parameter -func (d ShellCompDirective) string() string { - var directives []string - if d&ShellCompDirectiveError != 0 { - directives = append(directives, "ShellCompDirectiveError") - } - if d&ShellCompDirectiveNoSpace != 0 { - directives = append(directives, "ShellCompDirectiveNoSpace") - } - if d&ShellCompDirectiveNoFileComp != 0 { - directives = append(directives, "ShellCompDirectiveNoFileComp") - } - if d&ShellCompDirectiveFilterFileExt != 0 { - directives = append(directives, "ShellCompDirectiveFilterFileExt") - } - if d&ShellCompDirectiveFilterDirs != 0 { - directives = append(directives, "ShellCompDirectiveFilterDirs") - } - if len(directives) == 0 { - directives = append(directives, "ShellCompDirectiveDefault") - } - - if d >= shellCompDirectiveMaxValue { - return fmt.Sprintf("ERROR: unexpected ShellCompDirective value: %d", d) - } - return strings.Join(directives, ", ") -} - -func (l Complete) BeforeApply(ctx *kong.Context) error { - _, completions, directive, err := getCompletions(ctx.Model.Node, ctx.Args[2:]) - if err != nil { - CompErrorln(err.Error()) - // Keep going for multiple reasons: - // 1- There could be some valid completions even though there was an error - // 2- Even without completions, we need to print the directive - } - - noDescriptions := false // (cmd.CalledAs() == ShellCompNoDescRequestCmd) - for _, comp := range completions { - // if GetActiveHelpConfig(finalCmd) == activeHelpGlobalDisable { - // // Remove all activeHelp entries in this case - // if strings.HasPrefix(comp, activeHelpMarker) { - // continue - // } - // } - if noDescriptions { - // Remove any description that may be included following a tab character. - comp = strings.Split(comp, "\t")[0] - } - - // Make sure we only write the first line to the output. - // This is needed if a description contains a linebreak. - // Otherwise the shell scripts will interpret the other lines as new flags - // and could therefore provide a wrong completion. - comp = strings.Split(comp, "\n")[0] - - // Finally trim the completion. This is especially important to get rid - // of a trailing tab when there are no description following it. - // For example, a sub-command without a description should not be completed - // with a tab at the end (or else zsh will show a -- following it - // although there is no description). - comp = strings.TrimSpace(comp) - - // Print each possible completion to stdout for the completion script to consume. - fmt.Fprintln(ctx.Stdout, comp) - } - - // As the last printout, print the completion directive for the completion script to parse. - // The directive integer must be that last character following a single colon (:). - // The completion script expects : - fmt.Fprintf(ctx.Stdout, ":%d\n", directive) - - // Print some helpful info to stderr for the user to understand. - // Output from stderr must be ignored by the completion script. - fmt.Fprintf(ctx.Stderr, "Completion ended with directive: %s\n", directive.string()) - - return nil -} - -func parseFlags(c *kong.Node, args []string) (int, error) { - flags := make([]*kong.Flag, 0) - for _, f := range c.Flags { - if f.Hidden || f.Tag.Optional || f.Tag.Ignored { - continue - } - flags = append(flags, f) - } - return len(flags), nil -} - -func traverse(c *kong.Node, args []string) (*kong.Node, []string, error) { - if len(args) == 0 { - return c, args, nil - } - - // Find the sub-command to complete. - for _, c := range c.Children { - if c.Name == args[0] { - return traverse(c, args[1:]) - } - } - - // If we didn't find a sub-command, we are at the end of the path. - // We can complete the sub-command name. - return c, args, nil -} - -func getCompletions(c *kong.Node, args []string) (*kong.Node, []string, ShellCompDirective, error) { - // The last argument, which is not completely typed by the user, - // should not be part of the list of arguments - CompDebugln(c.Name, false) - CompDebugln(fmt.Sprint(args), false) - toComplete := args[len(args)-1] - trimmedArgs := args[:len(args)-1] - CompDebugln(fmt.Sprintf("toComplete: %v", toComplete), false) - CompDebugln(fmt.Sprintf("trimmedArgs: %v", trimmedArgs), false) - - var finalCmd *kong.Node - var finalArgs []string - var err error - - finalCmd, finalArgs, err = traverse(c, trimmedArgs) - if err != nil { - // Unable to find the real command. E.g., someInvalidCmd - return c, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Unable to find a command for arguments: %v", trimmedArgs) - } - CompDebugln(fmt.Sprintf("finalCmd: %v", finalCmd.Name), false) - CompDebugln(fmt.Sprintf("finalArgs: %v", finalArgs), false) - // Check if we are doing flag value completion before parsing the flags. - // This is important because if we are completing a flag value, we need to also - // remove the flag name argument from the list of finalArgs or else the parsing - // could fail due to an invalid value (incomplete) for the flag. - flag, finalArgs, toComplete, flagErr := checkIfFlagCompletion(finalCmd, finalArgs, toComplete) - - // Check if interspersed is false or -- was set on a previous arg. - // This works by counting the arguments. Normally -- is not counted as arg but - // if -- was already set or interspersed is false and there is already one arg then - // the extra added -- is counted as arg. - flagCompletion := true - newArgCount, _ := parseFlags(finalCmd, append(finalArgs, "--")) //len(finalCmd.Flags) //finalCmd.Flags().NArg() - - // Parse the flags early so we can check if required flags are set - realArgCount, err := parseFlags(finalCmd, finalArgs) - if err != nil { - return finalCmd, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error()) - } - - if newArgCount > realArgCount { - // don't do flag completion (see above) - flagCompletion = false - } - // Error while attempting to parse flags - if flagErr != nil { - // If error type is flagCompError and we don't want flagCompletion we should ignore the error - if _, ok := flagErr.(*flagCompError); !(ok && !flagCompletion) { - return finalCmd, []string{}, ShellCompDirectiveDefault, flagErr - } - } - - // We only remove the flags from the arguments if DisableFlagParsing is not set. - // This is important for commands which have requested to do their own flag completion. - // if !finalCmd.DisableFlagParsing { - // finalArgs = finalCmd.Flags().Args() - // } - - if flag != nil && flagCompletion { - // Check if we are completing a flag value subject to annotations - // if validExts, present := flag.Annotations[BashCompFilenameExt]; present { - // if len(validExts) != 0 { - // // File completion filtered by extensions - // return finalCmd, validExts, ShellCompDirectiveFilterFileExt, nil - // } - - // The annotation requests simple file completion. There is no reason to do - // that since it is the default behavior anyway. Let's ignore this annotation - // in case the program also registered a completion function for this flag. - // Even though it is a mistake on the program's side, let's be nice when we can. - // } - - // if subDir, present := flag.Annotations[BashCompSubdirsInDir]; present { - // if len(subDir) == 1 { - // // Directory completion from within a directory - // return finalCmd, subDir, ShellCompDirectiveFilterDirs, nil - // } - // // Directory completion - // return finalCmd, []string{}, ShellCompDirectiveFilterDirs, nil - // } - } - - var completions []string - var directive ShellCompDirective - - // Enforce flag groups before doing flag completions - // finalCmd.enforceFlagGroupsForCompletion() - - // Note that we want to perform flagname completion even if finalCmd.DisableFlagParsing==true; - // doing this allows for completion of persistent flag names even for commands that disable flag parsing. - // - // When doing completion of a flag name, as soon as an argument starts with - // a '-' we know it is a flag. We cannot use isFlagArg() here as it requires - // the flag name to be complete - if flag == nil && len(toComplete) > 0 && toComplete[0] == '-' && !strings.Contains(toComplete, "=") && flagCompletion { - CompDebugln("Flag name completion", false) - // First check for required flags - completions = completeRequireFlags(finalCmd, toComplete) - - // If we have not found any required flags, only then can we show regular flags - if len(completions) == 0 { - for _, flag := range finalCmd.Flags { - completions = append(completions, getFlagNameCompletions(flag, toComplete)...) - } - } - - directive = ShellCompDirectiveNoFileComp - if len(completions) == 1 && strings.HasSuffix(completions[0], "=") { - // If there is a single completion, the shell usually adds a space - // after the completion. We don't want that if the flag ends with an = - directive = ShellCompDirectiveNoSpace - } - - // if !finalCmd.DisableFlagParsing { - // // If DisableFlagParsing==false, we have completed the flags as known by Cobra; - // // we can return what we found. - // // If DisableFlagParsing==true, Cobra may not be aware of all flags, so we - // // let the logic continue to see if ValidArgsFunction needs to be called. - // return finalCmd, completions, directive, nil - // } - } else { - directive = ShellCompDirectiveDefault - // if flag == nil { - foundLocalNonPersistentFlag := false - // If TraverseChildren is true on the root command we don't check for - // local flags because we can use a local flag on a parent command - // if !finalCmd.Root().TraverseChildren { - if finalCmd.Parent != nil && len(finalCmd.Parent.Children) == 0 { - // Check if there are any local, non-persistent flags on the command-line - // localNonPersistentFlags := finalCmd.LocalNonPersistentFlags() - // finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) { - // if localNonPersistentFlags.Lookup(flag.Name) != nil && flag.Changed { - // foundLocalNonPersistentFlag = true - // } - // }) - } - - // Complete subcommand names, including the help command - if len(finalArgs) == 0 && !foundLocalNonPersistentFlag { - // We only complete sub-commands if: - // - there are no arguments on the command-line and - // - there are no local, non-persistent flags on the command-line or TraverseChildren is true - for _, subCmd := range finalCmd.Children { - // if subCmd.IsAvailableCommand() || subCmd == finalCmd.helpCommand { - if !subCmd.Hidden { - if strings.HasPrefix(subCmd.Name, toComplete) { - completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name, subCmd.Help)) - } - directive = ShellCompDirectiveNoFileComp - } - } - } - - // Complete required flags even without the '-' prefix - completions = append(completions, completeRequireFlags(finalCmd, toComplete)...) - - // Always complete ValidArgs, even if we are completing a subcommand name. - // This is for commands that have both subcommands and ValidArgs. - // if len(finalCmd.ValidArgs) > 0 { - // if len(finalArgs) == 0 { - // // ValidArgs are only for the first argument - // for _, validArg := range finalCmd.ValidArgs { - // if strings.HasPrefix(validArg, toComplete) { - // completions = append(completions, validArg) - // } - // } - // directive = ShellCompDirectiveNoFileComp - - // // If no completions were found within commands or ValidArgs, - // // see if there are any ArgAliases that should be completed. - // if len(completions) == 0 { - // for _, argAlias := range finalCmd.ArgAliases { - // if strings.HasPrefix(argAlias, toComplete) { - // completions = append(completions, argAlias) - // } - // } - // } - // } - - // // If there are ValidArgs specified (even if they don't match), we stop completion. - // // Only one of ValidArgs or ValidArgsFunction can be used for a single command. - // return finalCmd, completions, directive, nil - // } - - // Let the logic continue so as to add any ValidArgsFunction completions, - // even if we already found sub-commands. - // This is for commands that have subcommands but also specify a ValidArgsFunction. - // } - } - - // Find the completion function for the flag or command - var completionFn func(cmd *kong.Node, args []string, toComplete string) ([]string, ShellCompDirective) - if flag != nil && flagCompletion { - flagCompletionMutex.RLock() - completionFn = flagCompletionFunctions[flag] - flagCompletionMutex.RUnlock() - } else { - completionFn = nil //finalCmd.ValidArgsFunction - } - if completionFn != nil { - // Go custom completion defined for this flag or command. - // Call the registered completion function to get the completions. - var comps []string - comps, directive = completionFn(finalCmd, finalArgs, toComplete) - completions = append(completions, comps...) - } - - return finalCmd, completions, directive, nil -} - -func completeRequireFlags(finalCmd *kong.Node, toComplete string) []string { - var completions []string - - // We cannot use finalCmd.Flags() because we may not have called ParsedFlags() for commands - // that have set DisableFlagParsing; it is ParseFlags() that merges the inherited and - // non-inherited flags. - // finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) { - // doCompleteRequiredFlags(flag) - // }) - // finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) { - // doCompleteRequiredFlags(flag) - // }) - for _, f := range finalCmd.Flags { - if f.Required { - CompDebugln(fmt.Sprintf("flag: %s help: %s", f.Name, f.Help), false) - completions = append(completions, getFlagNameCompletions(f, toComplete)...) - } - } - - return completions -} - -func getFlagNameCompletions(flag *kong.Flag, toComplete string) []string { - if nonCompletableFlag(flag) { - return []string{} - } - - var completions []string - flagName := "--" + flag.Name - if strings.HasPrefix(flagName, toComplete) { - // Flag without the = - flagHelp := flag.Help - completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flagHelp)) - - // Why suggest both long forms: --flag and --flag= ? - // This forces the user to *always* have to type either an = or a space after the flag name. - // Let's be nice and avoid making users have to do that. - // Since boolean flags and shortname flags don't show the = form, let's go that route and never show it. - // The = form will still work, we just won't suggest it. - // This also makes the list of suggested flags shorter as we avoid all the = forms. - // - // if len(flag.Default) == 0 { - // // Flag requires a value, so it can be suffixed with = - // flagName += "=" - // completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Help)) - // } - } - - flagName = "-" + fmt.Sprintf("%c", flag.Short) - if flag.Short != 0 && strings.HasPrefix(flagName, toComplete) { - completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Help)) - } - - return completions -} - -func isFlagArg(arg string) bool { - return ((len(arg) >= 3 && arg[1] == '-') || - (len(arg) >= 2 && arg[0] == '-' && arg[1] != '-')) -} - -func checkIfFlagCompletion(finalCmd *kong.Node, args []string, lastArg string) (*kong.Flag, []string, string, error) { - var flagName string - trimmedArgs := args - flagWithEqual := false - orgLastArg := lastArg - - // When doing completion of a flag name, as soon as an argument starts with - // a '-' we know it is a flag. We cannot use isFlagArg() here as that function - // requires the flag name to be complete - if len(lastArg) > 0 && lastArg[0] == '-' { - if index := strings.Index(lastArg, "="); index >= 0 { - // Flag with an = - if strings.HasPrefix(lastArg[:index], "--") { - // Flag has full name - flagName = lastArg[2:index] - } else { - // Flag is shorthand - // We have to get the last shorthand flag name - // e.g. `-asd` => d to provide the correct completion - // https://github.com/spf13/cobra/issues/1257 - flagName = lastArg[index-1 : index] - } - lastArg = lastArg[index+1:] - flagWithEqual = true - } else { - // Normal flag completion - return nil, args, lastArg, nil - } - } - - if len(flagName) == 0 { - if len(args) > 0 { - prevArg := args[len(args)-1] - if isFlagArg(prevArg) { - // Only consider the case where the flag does not contain an =. - // If the flag contains an = it means it has already been fully processed, - // so we don't need to deal with it here. - if index := strings.Index(prevArg, "="); index < 0 { - if strings.HasPrefix(prevArg, "--") { - // Flag has full name - flagName = prevArg[2:] - } else { - // Flag is shorthand - // We have to get the last shorthand flag name - // e.g. `-asd` => d to provide the correct completion - // https://github.com/spf13/cobra/issues/1257 - flagName = prevArg[len(prevArg)-1:] - } - // Remove the uncompleted flag or else there could be an error created - // for an invalid value for that flag - trimmedArgs = args[:len(args)-1] - } - } - } - } - - if len(flagName) == 0 { - // Not doing flag completion - return nil, trimmedArgs, lastArg, nil - } - - flag := findFlag(finalCmd, flagName) - if flag == nil { - // Flag not supported by this command, the interspersed option might be set so return the original args - return nil, args, orgLastArg, &flagCompError{subCommand: finalCmd.Name, flagName: flagName} - } - - if !flagWithEqual { - if len(flag.Default) != 0 { - // We had assumed dealing with a two-word flag but the flag is a boolean flag. - // In that case, there is no value following it, so we are not really doing flag completion. - // Reset everything to do noun completion. - trimmedArgs = args - flag = nil - } - } - - return flag, trimmedArgs, lastArg, nil -} - -func shorthandLookup(flags []*kong.Flag, name string) *kong.Flag { - for _, flag := range flags { - if flag.Name == name { - return flag - } - } - return nil -} - -func findFlag(cmd *kong.Node, name string) *kong.Flag { - flagSet := cmd.Flags - for _, flag := range flagSet { - if flag.Name == name { - return flag - } - } - return nil -} - -func nonCompletableFlag(flag *kong.Flag) bool { - return flag.Hidden -} - -// CompDebug prints the specified string to the same file as where the -// completion script prints its logs. -// Note that completion printouts should never be on stdout as they would -// be wrongly interpreted as actual completion choices by the completion script. -func CompDebug(msg string, printToStdErr bool) { - msg = fmt.Sprintf("[Debug] %s", msg) - - // Such logs are only printed when the user has set the environment - // variable BASH_COMP_DEBUG_FILE to the path of some file to be used. - if path := os.Getenv("BASH_COMP_DEBUG_FILE"); path != "" { - f, err := os.OpenFile(path, - os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err == nil { - defer f.Close() - WriteStringAndCheck(f, msg) - } - } - - if printToStdErr { - // Must print to stderr for this not to be read by the completion script. - fmt.Fprint(os.Stderr, msg) - } -} - -// CompDebugln prints the specified string with a newline at the end -// to the same file as where the completion script prints its logs. -// Such logs are only printed when the user has set the environment -// variable BASH_COMP_DEBUG_FILE to the path of some file to be used. -func CompDebugln(msg string, printToStdErr bool) { - CompDebug(fmt.Sprintf("%s\n", msg), printToStdErr) -} - -// CompError prints the specified completion message to stderr. -func CompError(msg string) { - msg = fmt.Sprintf("[Error] %s", msg) - CompDebug(msg, true) -} - -// CompErrorln prints the specified completion message to stderr with a newline at the end. -func CompErrorln(msg string) { - CompError(fmt.Sprintf("%s\n", msg)) -} diff --git a/completion/zsh.go b/completion/zsh.go index 0fb2a43..6e917d9 100644 --- a/completion/zsh.go +++ b/completion/zsh.go @@ -1,250 +1,147 @@ package completion import ( - "bytes" "fmt" "io" + "strings" "github.com/alecthomas/kong" ) type Zsh struct{} -func (b Zsh) BeforeApply(app *kong.Kong) error { - return GenZshCompletion(app.Model.Node, app.Stdout) +func (z Zsh) writeFlag(buf io.StringWriter, f *kong.Flag) { + var str strings.Builder + str.WriteString(" ") + if f.Short != 0 { + str.WriteString("'(") + str.WriteString(fmt.Sprintf("-%c --%s", f.Short, f.Name)) + if !f.IsBool() { + str.WriteString("=") + } + str.WriteString(")'") + str.WriteString("{") + str.WriteString(fmt.Sprintf("-%c,--%s", f.Short, f.Name)) + if !f.IsBool() { + str.WriteString("=") + } + str.WriteString("}") + str.WriteString("\"") + } else { + str.WriteString("\"") + str.WriteString(fmt.Sprintf("--%s", f.Name)) + if !f.IsBool() { + str.WriteString("=") + } + } + str.WriteString(fmt.Sprintf("[%s]", f.Help)) + if !f.IsBool() { + str.WriteString(":") + str.WriteString(strings.ToLower(f.Help)) + str.WriteString(":") + } + enumSlice := make([]string, 0) + for _, enum := range f.EnumSlice() { + if strings.TrimSpace(enum) != "" { + enumSlice = append(enumSlice, enum) + } + } + if len(enumSlice) > 0 { + str.WriteString("(") + for i, v := range f.EnumSlice() { + str.WriteString(v) + if i < len(enumSlice)-1 { + str.WriteString(" ") + } + } + str.WriteString(")") + } + str.WriteString("\"") + writeString(buf, str.String()) } -// GenZshCompletion generates zsh completion file including descriptions -// and writes it to the passed writer. -func GenZshCompletion(c *kong.Node, w io.Writer) error { - return genZshCompletion(c, w, true) +func (z Zsh) writeFlags(buf io.StringWriter, cmd *kong.Node) { + for i, f := range cmd.Flags { + if f.Hidden { + continue + } + z.writeFlag(buf, f) + if i < len(cmd.Flags)-1 { + writeString(buf, " \\\n") + } + } } -// // GenZshCompletionNoDesc generates zsh completion file without descriptions -// // and writes it to the passed writer. -// func (c *Command) GenZshCompletionNoDesc(w io.Writer) error { -// return c.genZshCompletion(w, false) -// } +func (z Zsh) writeCommand(buf io.StringWriter, c *kong.Node) { + writeString(buf, fmt.Sprintf(" \"%s[%s]\"", c.Name, c.Help)) +} -func genZshCompletion(c *kong.Node, w io.Writer, includeDesc bool) error { - buf := new(bytes.Buffer) - genZshComp(buf, c.Name, includeDesc) - _, err := buf.WriteTo(w) +func (z Zsh) writeCommands(buf io.StringWriter, cmd *kong.Node) { + for i, c := range cmd.Children { + if c == nil || c.Hidden { + continue + } + z.writeCommand(buf, c) + if i < len(cmd.Children)-1 { + buf.WriteString(" \\") + } + writeString(buf, "\n") + } +} + +func (z Zsh) gen(buf io.StringWriter, cmd *kong.Node) { + for _, c := range cmd.Children { + if c == nil || c.Hidden { + continue + } + z.gen(buf, c) + } + cmdName := commandName(cmd) + + writeString(buf, fmt.Sprintf("_%s() {\n", cmdName)) + if hasCommands(cmd) { + writeString(buf, " local line state\n") + } + writeString(buf, " _arguments -C \\\n") + z.writeFlags(buf, cmd) + if hasCommands(cmd) { + writeString(buf, " \\\n") + writeString(buf, " \"1: :->cmds\" \\\n") + writeString(buf, " \"*::arg:->args\"\n") + writeString(buf, " case \"$state\" in\n") + writeString(buf, " cmds)\n") + writeString(buf, fmt.Sprintf(" _values \"%s command\" \\\n", cmdName)) + z.writeCommands(buf, cmd) + writeString(buf, " ;;\n") + writeString(buf, " args)\n") + writeString(buf, " case \"$line[1]\" in\n") + for _, c := range cmd.Children { + if c == nil || c.Hidden { + continue + } + writeString(buf, fmt.Sprintf(" %s)\n", c.Name)) + writeString(buf, fmt.Sprintf(" _%s\n", commandName(c))) + writeString(buf, " ;;\n") + } + writeString(buf, " esac\n") + writeString(buf, " ;;\n") + writeString(buf, " esac\n") + } + // writeArgAliases(buf, cmd) + writeString(buf, "\n") + writeString(buf, "}\n\n") +} + +// Run generates zsh shell completion. +func (z Zsh) Run(ctx *kong.Context) error { + var out strings.Builder + format := `#compdef %[1]s +# zsh completion for %[1]s +# generated by gum completion + +` + fmt.Fprintf(&out, format, ctx.Model.Name) + z.gen(&out, ctx.Model.Node) + _, err := fmt.Fprint(ctx.Stdout, out.String()) return err } - -func genZshComp(buf io.StringWriter, name string, includeDesc bool) { - compCmd := ShellCompRequestCmd - if !includeDesc { - compCmd = ShellCompNoDescRequestCmd - } - WriteStringAndCheck(buf, fmt.Sprintf(`#compdef %[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 shellCompDirectiveError=%[3]d - local shellCompDirectiveNoSpace=%[4]d - local shellCompDirectiveNoFileComp=%[5]d - local shellCompDirectiveFilterFileExt=%[6]d - local shellCompDirectiveFilterDirs=%[7]d - - local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace - local -a completions - - __%[1]s_debug "\n========= starting completion logic ==========" - __%[1]s_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" - - # The user could have moved the cursor backwards on the command-line. - # We need to trigger completion from the $CURRENT location, so we need - # to truncate the command-line ($words) up to the $CURRENT location. - # (We cannot use $CURSOR as its value does not work when a command is an alias.) - words=("${=words[1,CURRENT]}") - __%[1]s_debug "Truncated 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="-P ${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 - local lastLine - while IFS='\n' read -r line; do - lastLine=${line} - done < <(printf "%%s\n" "${out[@]}") - __%[1]s_debug "last line: ${lastLine}" - - if [ "${lastLine[1]}" = : ]; then - directive=${lastLine[2,-1]} - # Remove the directive including the : and the newline - local suffix - (( suffix=${#lastLine}+2)) - out=${out[1,-$suffix]} - 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 & shellCompDirectiveError)) -ne 0 ]; then - __%[1]s_debug "Completion received error. Ignoring completions." - return - fi - - local activeHelpMarker="%[8]s" - local endIndex=${#activeHelpMarker} - local startIndex=$((${#activeHelpMarker}+1)) - local hasActiveHelp=0 - while IFS='\n' read -r comp; do - # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker) - if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then - __%[1]s_debug "ActiveHelp found: $comp" - comp="${comp[$startIndex,-1]}" - if [ -n "$comp" ]; then - compadd -x "${comp}" - __%[1]s_debug "ActiveHelp will need delimiter" - hasActiveHelp=1 - fi - - continue - fi - - 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/:} - - __%[1]s_debug "Adding completion: ${comp}" - completions+=${comp} - lastComp=$comp - fi - done < <(printf "%%s\n" "${out[@]}") - - # Add a delimiter after the activeHelp statements, but only if: - # - there are completions following the activeHelp statements, or - # - file completion will be performed (so there will be choices after the activeHelp) - if [ $hasActiveHelp -eq 1 ]; then - if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then - __%[1]s_debug "Adding activeHelp delimiter" - compadd -x "--" - hasActiveHelp=0 - fi - fi - - if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then - __%[1]s_debug "Activating nospace." - noSpace="-S ''" - fi - - if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then - # File extension filtering - local filteringCmd - filteringCmd='_files' - for filter in ${completions[@]}; do - if [ ${filter[1]} != '*' ]; then - # zsh requires a glob pattern to do file filtering - filter="\*.$filter" - fi - filteringCmd+=" -g $filter" - done - filteringCmd+=" ${flagPrefix}" - - __%[1]s_debug "File filtering command: $filteringCmd" - _arguments '*:filename:'"$filteringCmd" - elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then - # File completion for directories only - local subdir - subdir="${completions[1]}" - if [ -n "$subdir" ]; then - __%[1]s_debug "Listing directories in $subdir" - pushd "${subdir}" >/dev/null 2>&1 - else - __%[1]s_debug "Listing directories in ." - fi - - local result - _arguments '*:dirname:_files -/'" ${flagPrefix}" - result=$? - if [ -n "$subdir" ]; then - popd >/dev/null 2>&1 - fi - return $result - else - __%[1]s_debug "Calling _describe" - if eval _describe "completions" completions $flagPrefix $noSpace; then - __%[1]s_debug "_describe found some completions" - - # Return the success of having called _describe - return 0 - else - __%[1]s_debug "_describe did not find completions." - __%[1]s_debug "Checking if we should do file completion." - if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then - __%[1]s_debug "deactivating file completion" - - # We must return an error code here to let zsh know that there were no - # completions found by _describe; this is what will trigger other - # matching algorithms to attempt to find completions. - # For example zsh can match letters in the middle of words. - return 1 - else - # Perform file completion - __%[1]s_debug "Activating file completion" - - # We must return the result of this command, so it must be the - # last command, or else we must store its result to return it. - _arguments '*:filename:_files'" ${flagPrefix}" - fi - fi - fi -} - -# don't run the completion function when being source-ed or eval-ed -if [ "$funcstack[1]" = "_%[1]s" ]; then - _%[1]s -fi -`, name, compCmd, - ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, - ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, - activeHelpMarker)) -} diff --git a/gum.go b/gum.go index 95af1a8..93ec5b2 100644 --- a/gum.go +++ b/gum.go @@ -15,6 +15,7 @@ import ( // Gum is the command-line interface for Gum. type Gum struct { + // Completion generates Gum shell completion scripts. Completion completion.Completion `cmd:"" hidden:"" help:"Request shell completion"` // Man is a hidden command that generates Gum man pages.