mirror of
https://github.com/charmbracelet/gum
synced 2024-06-02 13:52:16 +02:00
feat: implement zsh & bash completion scripts
This commit is contained in:
parent
eaef49cdbe
commit
dbbc48a1c3
|
@ -1,3 +1,4 @@
|
||||||
|
// TODO implement our own gum bash completion
|
||||||
package completion
|
package completion
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -12,13 +13,84 @@ import (
|
||||||
|
|
||||||
type Bash struct{}
|
type Bash struct{}
|
||||||
|
|
||||||
func (b Bash) BeforeApply(app *kong.Kong) error {
|
// Run generates bash shell completion.
|
||||||
return GenBashCompletion(app.Model.Node, app.Stdout)
|
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 <PROGRAM>_ACTIVE_HELP where <PROGRAM> 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) {
|
func writePreamble(buf io.StringWriter, name string) {
|
||||||
WriteStringAndCheck(buf, fmt.Sprintf("# bash completion for %-36s -*- shell-script -*-\n", name))
|
writeString(buf, fmt.Sprintf("# bash completion for %-36s -*- shell-script -*-\n", name))
|
||||||
WriteStringAndCheck(buf, fmt.Sprintf(`
|
writeString(buf, fmt.Sprintf(`
|
||||||
__%[1]s_debug()
|
__%[1]s_debug()
|
||||||
{
|
{
|
||||||
if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then
|
if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then
|
||||||
|
@ -386,8 +458,8 @@ __%[1]s_handle_word()
|
||||||
|
|
||||||
func writePostscript(buf io.StringWriter, name string) {
|
func writePostscript(buf io.StringWriter, name string) {
|
||||||
name = strings.ReplaceAll(name, ":", "__")
|
name = strings.ReplaceAll(name, ":", "__")
|
||||||
WriteStringAndCheck(buf, fmt.Sprintf("__start_%s()\n", name))
|
writeString(buf, fmt.Sprintf("__start_%s()\n", name))
|
||||||
WriteStringAndCheck(buf, fmt.Sprintf(`{
|
writeString(buf, fmt.Sprintf(`{
|
||||||
local cur prev words cword split
|
local cur prev words cword split
|
||||||
declare -A flaghash 2>/dev/null || :
|
declare -A flaghash 2>/dev/null || :
|
||||||
declare -A aliashash 2>/dev/null || :
|
declare -A aliashash 2>/dev/null || :
|
||||||
|
@ -417,33 +489,33 @@ func writePostscript(buf io.StringWriter, name string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
`, name))
|
`, 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
|
complete -o default -F __start_%s %s
|
||||||
else
|
else
|
||||||
complete -o default -o nospace -F __start_%s %s
|
complete -o default -o nospace -F __start_%s %s
|
||||||
fi
|
fi
|
||||||
|
|
||||||
`, name, name, name, name))
|
`, 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) {
|
func writeCommands(buf io.StringWriter, cmd *kong.Node) {
|
||||||
WriteStringAndCheck(buf, " commands=()\n")
|
writeString(buf, " commands=()\n")
|
||||||
for _, c := range cmd.Children {
|
for _, c := range cmd.Children {
|
||||||
if c == nil || c.Hidden {
|
if c == nil || c.Hidden {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
WriteStringAndCheck(buf, fmt.Sprintf(" commands+=(%q)\n", c.Name))
|
writeString(buf, fmt.Sprintf(" commands+=(%q)\n", c.Name))
|
||||||
writeCmdAliases(buf, c)
|
writeCmdAliases(buf, c)
|
||||||
}
|
}
|
||||||
WriteStringAndCheck(buf, "\n")
|
writeString(buf, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeFlagHandler(buf io.StringWriter, name string, annotations map[string][]string, cmd *kong.Node) {
|
func writeFlagHandler(buf io.StringWriter, name string, annotations map[string][]string, cmd *kong.Node) {
|
||||||
for key, value := range annotations {
|
for key, value := range annotations {
|
||||||
switch key {
|
switch key {
|
||||||
case BashCompFilenameExt:
|
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
|
var ext string
|
||||||
if len(value) > 0 {
|
if len(value) > 0 {
|
||||||
|
@ -451,18 +523,18 @@ func writeFlagHandler(buf io.StringWriter, name string, annotations map[string][
|
||||||
} else {
|
} else {
|
||||||
ext = "_filedir"
|
ext = "_filedir"
|
||||||
}
|
}
|
||||||
WriteStringAndCheck(buf, fmt.Sprintf(" flags_completion+=(%q)\n", ext))
|
writeString(buf, fmt.Sprintf(" flags_completion+=(%q)\n", ext))
|
||||||
case BashCompCustom:
|
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 {
|
if len(value) > 0 {
|
||||||
handlers := strings.Join(value, "; ")
|
handlers := strings.Join(value, "; ")
|
||||||
WriteStringAndCheck(buf, fmt.Sprintf(" flags_completion+=(%q)\n", handlers))
|
writeString(buf, fmt.Sprintf(" flags_completion+=(%q)\n", handlers))
|
||||||
} else {
|
} else {
|
||||||
WriteStringAndCheck(buf, " flags_completion+=(:)\n")
|
writeString(buf, " flags_completion+=(:)\n")
|
||||||
}
|
}
|
||||||
case BashCompSubdirsInDir:
|
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
|
var ext string
|
||||||
if len(value) == 1 {
|
if len(value) == 1 {
|
||||||
|
@ -470,7 +542,7 @@ func writeFlagHandler(buf io.StringWriter, name string, annotations map[string][
|
||||||
} else {
|
} else {
|
||||||
ext = "_filedir -d"
|
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 += "two_word_"
|
||||||
}
|
}
|
||||||
format += "flags+=(\"-%s" + cbn
|
format += "flags+=(\"-%s" + cbn
|
||||||
WriteStringAndCheck(buf, fmt.Sprintf(format, name))
|
writeString(buf, fmt.Sprintf(format, name))
|
||||||
writeFlagHandler(buf, "-"+name, map[string][]string{}, cmd)
|
writeFlagHandler(buf, "-"+name, map[string][]string{}, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -495,10 +567,10 @@ func writeFlag(buf io.StringWriter, flag *kong.Flag, cmd *kong.Node) {
|
||||||
format += "="
|
format += "="
|
||||||
}
|
}
|
||||||
format += cbn
|
format += cbn
|
||||||
WriteStringAndCheck(buf, fmt.Sprintf(format, name))
|
writeString(buf, fmt.Sprintf(format, name))
|
||||||
if len(flag.DefaultValue.String()) == 0 {
|
if len(flag.DefaultValue.String()) == 0 {
|
||||||
format = " two_word_flags+=(\"--%s" + cbn
|
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)
|
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 {
|
if len(flag.DefaultValue.String()) == 0 {
|
||||||
format += " local_nonpersistent_flags+=(\"--%[1]s=" + cbn
|
format += " local_nonpersistent_flags+=(\"--%[1]s=" + cbn
|
||||||
}
|
}
|
||||||
WriteStringAndCheck(buf, fmt.Sprintf(format, name))
|
writeString(buf, fmt.Sprintf(format, name))
|
||||||
if flag.Short > 0 {
|
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) {
|
func writeFlags(buf io.StringWriter, cmd *kong.Node) {
|
||||||
WriteStringAndCheck(buf, ` flags=()
|
writeString(buf, ` flags=()
|
||||||
two_word_flags=()
|
two_word_flags=()
|
||||||
local_nonpersistent_flags=()
|
local_nonpersistent_flags=()
|
||||||
flags_with_completion=()
|
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) {
|
func writeCmdAliases(buf io.StringWriter, cmd *kong.Node) {
|
||||||
|
@ -544,57 +616,46 @@ func writeCmdAliases(buf io.StringWriter, cmd *kong.Node) {
|
||||||
|
|
||||||
sort.Strings(cmd.Aliases)
|
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 {
|
for _, value := range cmd.Aliases {
|
||||||
WriteStringAndCheck(buf, fmt.Sprintf(" command_aliases+=(%q)\n", value))
|
writeString(buf, fmt.Sprintf(" command_aliases+=(%q)\n", value))
|
||||||
WriteStringAndCheck(buf, fmt.Sprintf(" aliashash[%q]=%q\n", value, cmd.Name))
|
writeString(buf, fmt.Sprintf(" aliashash[%q]=%q\n", value, cmd.Name))
|
||||||
}
|
}
|
||||||
WriteStringAndCheck(buf, ` fi`)
|
writeString(buf, ` fi`)
|
||||||
WriteStringAndCheck(buf, "\n")
|
writeString(buf, "\n")
|
||||||
}
|
}
|
||||||
func writeArgAliases(buf io.StringWriter, cmd *kong.Node) {
|
func writeArgAliases(buf io.StringWriter, cmd *kong.Node) {
|
||||||
WriteStringAndCheck(buf, " noun_aliases=()\n")
|
writeString(buf, " noun_aliases=()\n")
|
||||||
sort.Strings(cmd.Aliases)
|
sort.Strings(cmd.Aliases)
|
||||||
for _, value := range 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 {
|
for _, c := range cmd.Children {
|
||||||
if c == nil || c.Hidden {
|
if c == nil || c.Hidden {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
gen(buf, c)
|
b.gen(buf, c)
|
||||||
}
|
}
|
||||||
commandName := cmd.FullPath()
|
commandName := cmd.FullPath()
|
||||||
commandName = strings.ReplaceAll(commandName, " ", "_")
|
commandName = strings.ReplaceAll(commandName, " ", "_")
|
||||||
commandName = strings.ReplaceAll(commandName, ":", "__")
|
commandName = strings.ReplaceAll(commandName, ":", "__")
|
||||||
|
|
||||||
if cmd.Parent == nil {
|
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 {
|
} 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))
|
writeString(buf, fmt.Sprintf(" last_command=%q\n", commandName))
|
||||||
WriteStringAndCheck(buf, "\n")
|
writeString(buf, "\n")
|
||||||
WriteStringAndCheck(buf, " command_aliases=()\n")
|
writeString(buf, " command_aliases=()\n")
|
||||||
WriteStringAndCheck(buf, "\n")
|
writeString(buf, "\n")
|
||||||
|
|
||||||
writeCommands(buf, cmd)
|
writeCommands(buf, cmd)
|
||||||
writeFlags(buf, cmd)
|
writeFlags(buf, cmd)
|
||||||
writeArgAliases(buf, cmd)
|
writeArgAliases(buf, cmd)
|
||||||
WriteStringAndCheck(buf, "}\n\n")
|
writeString(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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,49 @@
|
||||||
package completion
|
package completion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
|
)
|
||||||
|
|
||||||
type Completion struct {
|
type Completion struct {
|
||||||
Complete Complete `cmd:"" hidden:"" help:"Request shell completion"`
|
Bash Bash `cmd:"" help:"Generate the autocompletion script for bash"`
|
||||||
Bash Bash `cmd:"" help:"Generate the autocompletion script for bash"`
|
Zsh Zsh `cmd:"" help:"Generate the autocompletion script for zsh"`
|
||||||
Zsh Zsh `cmd:"" help:"Generate the autocompletion script for zsh"`
|
|
||||||
// Fish Fish `cmd:"" help:"Generate the autocompletion script for fish"`
|
// 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
|
||||||
|
}
|
||||||
|
|
|
@ -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 <PROGRAM>_ACTIVE_HELP where <PROGRAM> 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 :<directive>
|
|
||||||
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., <program> someInvalidCmd <TAB>
|
|
||||||
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))
|
|
||||||
}
|
|
|
@ -1,250 +1,147 @@
|
||||||
package completion
|
package completion
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
"github.com/alecthomas/kong"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Zsh struct{}
|
type Zsh struct{}
|
||||||
|
|
||||||
func (b Zsh) BeforeApply(app *kong.Kong) error {
|
func (z Zsh) writeFlag(buf io.StringWriter, f *kong.Flag) {
|
||||||
return GenZshCompletion(app.Model.Node, app.Stdout)
|
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
|
func (z Zsh) writeFlags(buf io.StringWriter, cmd *kong.Node) {
|
||||||
// and writes it to the passed writer.
|
for i, f := range cmd.Flags {
|
||||||
func GenZshCompletion(c *kong.Node, w io.Writer) error {
|
if f.Hidden {
|
||||||
return genZshCompletion(c, w, true)
|
continue
|
||||||
|
}
|
||||||
|
z.writeFlag(buf, f)
|
||||||
|
if i < len(cmd.Flags)-1 {
|
||||||
|
writeString(buf, " \\\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// // GenZshCompletionNoDesc generates zsh completion file without descriptions
|
func (z Zsh) writeCommand(buf io.StringWriter, c *kong.Node) {
|
||||||
// // and writes it to the passed writer.
|
writeString(buf, fmt.Sprintf(" \"%s[%s]\"", c.Name, c.Help))
|
||||||
// func (c *Command) GenZshCompletionNoDesc(w io.Writer) error {
|
}
|
||||||
// return c.genZshCompletion(w, false)
|
|
||||||
// }
|
|
||||||
|
|
||||||
func genZshCompletion(c *kong.Node, w io.Writer, includeDesc bool) error {
|
func (z Zsh) writeCommands(buf io.StringWriter, cmd *kong.Node) {
|
||||||
buf := new(bytes.Buffer)
|
for i, c := range cmd.Children {
|
||||||
genZshComp(buf, c.Name, includeDesc)
|
if c == nil || c.Hidden {
|
||||||
_, err := buf.WriteTo(w)
|
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
|
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=<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="-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))
|
|
||||||
}
|
|
||||||
|
|
1
gum.go
1
gum.go
|
@ -15,6 +15,7 @@ import (
|
||||||
|
|
||||||
// Gum is the command-line interface for Gum.
|
// Gum is the command-line interface for Gum.
|
||||||
type Gum struct {
|
type Gum struct {
|
||||||
|
// Completion generates Gum shell completion scripts.
|
||||||
Completion completion.Completion `cmd:"" hidden:"" help:"Request shell completion"`
|
Completion completion.Completion `cmd:"" hidden:"" help:"Request shell completion"`
|
||||||
|
|
||||||
// Man is a hidden command that generates Gum man pages.
|
// Man is a hidden command that generates Gum man pages.
|
||||||
|
|
Loading…
Reference in a new issue