forked from spf13/cobra
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add fish completion support (spf13#754)
* add fish completion basics to cobra. * add helpCommand check to fish completions. * add basic tests for fish completion. * support ValidArgs completion of commands in fish. * add additional support for argument aliases. * remove oly mention. * use custom function for checking subcommand path. * use fish builtin for flag/argument checking. * backport __fish_seen_argument. * escape description of commands & flags. * allow subcommand_path to match even with flags * deal with unreachable code warning.
- Loading branch information
1 parent
aa95966
commit 17685dd
Showing
2 changed files
with
308 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
package cobra | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"io" | ||
"strings" | ||
|
||
"github.com/spf13/pflag" | ||
) | ||
|
||
// GenFishCompletion generates fish completion and writes to the passed writer. | ||
func (c *Command) GenFishCompletion(w io.Writer) error { | ||
buf := new(bytes.Buffer) | ||
|
||
writeFishPreamble(c, buf) | ||
writeFishCommandCompletion(c, c, buf) | ||
|
||
_, err := buf.WriteTo(w) | ||
return err | ||
} | ||
|
||
func writeFishPreamble(cmd *Command, buf *bytes.Buffer) { | ||
subCommandNames := []string{} | ||
rangeCommands(cmd, func(subCmd *Command) { | ||
subCommandNames = append(subCommandNames, subCmd.Name()) | ||
}) | ||
buf.WriteString(fmt.Sprintf(` | ||
function __fish_%s_no_subcommand --description 'Test if %s has yet to be given the subcommand' | ||
for i in (commandline -opc) | ||
if contains -- $i %s | ||
return 1 | ||
end | ||
end | ||
return 0 | ||
end | ||
function __fish_%s_seen_subcommand_path --description 'Test whether the full path of subcommands is the current path' | ||
set -l cmd (commandline -opc) | ||
set -e cmd[1] | ||
set -l pattern (string replace -a " " ".+" "$argv") | ||
string match -r "$pattern" (string trim -- "$cmd") | ||
end | ||
# borrowed from current fish-shell master, since it is not in current 2.7.1 release | ||
function __fish_seen_argument | ||
argparse 's/short=+' 'l/long=+' -- $argv | ||
set cmd (commandline -co) | ||
set -e cmd[1] | ||
for t in $cmd | ||
for s in $_flag_s | ||
if string match -qr "^-[A-z0-9]*"$s"[A-z0-9]*\$" -- $t | ||
return 0 | ||
end | ||
end | ||
for l in $_flag_l | ||
if string match -q -- "--$l" $t | ||
return 0 | ||
end | ||
end | ||
end | ||
return 1 | ||
end | ||
`, cmd.Name(), cmd.Name(), strings.Join(subCommandNames, " "), cmd.Name())) | ||
} | ||
|
||
func writeFishCommandCompletion(rootCmd, cmd *Command, buf *bytes.Buffer) { | ||
rangeCommands(cmd, func(subCmd *Command) { | ||
condition := commandCompletionCondition(rootCmd, cmd) | ||
escapedDescription := strings.Replace(subCmd.Short, "'", "\\'", -1) | ||
buf.WriteString(fmt.Sprintf("complete -c %s -f %s -a %s -d '%s'\n", rootCmd.Name(), condition, subCmd.Name(), escapedDescription)) | ||
}) | ||
for _, validArg := range append(cmd.ValidArgs, cmd.ArgAliases...) { | ||
condition := commandCompletionCondition(rootCmd, cmd) | ||
buf.WriteString( | ||
fmt.Sprintf("complete -c %s -f %s -a %s -d '%s'\n", | ||
rootCmd.Name(), condition, validArg, fmt.Sprintf("Positional Argument to %s", cmd.Name()))) | ||
} | ||
writeCommandFlagsCompletion(rootCmd, cmd, buf) | ||
rangeCommands(cmd, func(subCmd *Command) { | ||
writeFishCommandCompletion(rootCmd, subCmd, buf) | ||
}) | ||
} | ||
|
||
func writeCommandFlagsCompletion(rootCmd, cmd *Command, buf *bytes.Buffer) { | ||
cmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) { | ||
if nonCompletableFlag(flag) { | ||
return | ||
} | ||
writeCommandFlagCompletion(rootCmd, cmd, buf, flag) | ||
}) | ||
cmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) { | ||
if nonCompletableFlag(flag) { | ||
return | ||
} | ||
writeCommandFlagCompletion(rootCmd, cmd, buf, flag) | ||
}) | ||
} | ||
|
||
func writeCommandFlagCompletion(rootCmd, cmd *Command, buf *bytes.Buffer, flag *pflag.Flag) { | ||
shortHandPortion := "" | ||
if len(flag.Shorthand) > 0 { | ||
shortHandPortion = fmt.Sprintf("-s %s", flag.Shorthand) | ||
} | ||
condition := completionCondition(rootCmd, cmd) | ||
escapedUsage := strings.Replace(flag.Usage, "'", "\\'", -1) | ||
buf.WriteString(fmt.Sprintf("complete -c %s -f %s %s %s -l %s -d '%s'\n", | ||
rootCmd.Name(), condition, flagRequiresArgumentCompletion(flag), shortHandPortion, flag.Name, escapedUsage)) | ||
} | ||
|
||
func flagRequiresArgumentCompletion(flag *pflag.Flag) string { | ||
if flag.Value.Type() != "bool" { | ||
return "-r" | ||
} | ||
return "" | ||
} | ||
|
||
func subCommandPath(rootCmd *Command, cmd *Command) string { | ||
path := []string{} | ||
currentCmd := cmd | ||
if rootCmd == cmd { | ||
return "" | ||
} | ||
for { | ||
path = append([]string{currentCmd.Name()}, path...) | ||
if currentCmd.Parent() == rootCmd { | ||
return strings.Join(path, " ") | ||
} | ||
currentCmd = currentCmd.Parent() | ||
} | ||
} | ||
|
||
func rangeCommands(cmd *Command, callback func(subCmd *Command)) { | ||
for _, subCmd := range cmd.Commands() { | ||
if !subCmd.IsAvailableCommand() || subCmd == cmd.helpCommand { | ||
continue | ||
} | ||
callback(subCmd) | ||
} | ||
} | ||
|
||
func commandCompletionCondition(rootCmd, cmd *Command) string { | ||
localNonPersistentFlags := cmd.LocalNonPersistentFlags() | ||
bareConditions := []string{} | ||
if rootCmd != cmd { | ||
bareConditions = append(bareConditions, fmt.Sprintf("__fish_%s_seen_subcommand_path %s", rootCmd.Name(), subCommandPath(rootCmd, cmd))) | ||
} else { | ||
bareConditions = append(bareConditions, fmt.Sprintf("__fish_%s_no_subcommand", rootCmd.Name())) | ||
} | ||
localNonPersistentFlags.VisitAll(func(flag *pflag.Flag) { | ||
flagSelector := fmt.Sprintf("-l %s", flag.Name) | ||
if len(flag.Shorthand) > 0 { | ||
flagSelector = fmt.Sprintf("-s %s %s", flag.Shorthand, flagSelector) | ||
} | ||
bareConditions = append(bareConditions, fmt.Sprintf("not __fish_seen_argument %s", flagSelector)) | ||
}) | ||
return fmt.Sprintf("-n '%s'", strings.Join(bareConditions, "; and ")) | ||
} | ||
|
||
func completionCondition(rootCmd, cmd *Command) string { | ||
condition := fmt.Sprintf("-n '__fish_%s_no_subcommand'", rootCmd.Name()) | ||
if rootCmd != cmd { | ||
condition = fmt.Sprintf("-n '__fish_%s_seen_subcommand_path %s'", rootCmd.Name(), subCommandPath(rootCmd, cmd)) | ||
} | ||
return condition | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
package cobra | ||
|
||
import ( | ||
"bytes" | ||
"testing" | ||
) | ||
|
||
func TestFishCompletions(t *testing.T) { | ||
rootCmd := &Command{ | ||
Use: "root", | ||
ArgAliases: []string{"pods", "nodes", "services", "replicationcontrollers", "po", "no", "svc", "rc"}, | ||
ValidArgs: []string{"pod", "node", "service", "replicationcontroller"}, | ||
Run: emptyRun, | ||
} | ||
rootCmd.Flags().IntP("introot", "i", -1, "help's message for flag introot") | ||
rootCmd.MarkFlagRequired("introot") | ||
|
||
// Filename. | ||
rootCmd.Flags().String("filename", "", "Enter a filename") | ||
rootCmd.MarkFlagFilename("filename", "json", "yaml", "yml") | ||
|
||
// Persistent filename. | ||
rootCmd.PersistentFlags().String("persistent-filename", "", "Enter a filename") | ||
rootCmd.MarkPersistentFlagFilename("persistent-filename") | ||
rootCmd.MarkPersistentFlagRequired("persistent-filename") | ||
|
||
// Filename extensions. | ||
rootCmd.Flags().String("filename-ext", "", "Enter a filename (extension limited)") | ||
rootCmd.MarkFlagFilename("filename-ext") | ||
rootCmd.Flags().String("custom", "", "Enter a filename (extension limited)") | ||
rootCmd.MarkFlagCustom("custom", "__complete_custom") | ||
|
||
// Subdirectories in a given directory. | ||
rootCmd.Flags().String("theme", "", "theme to use (located in /themes/THEMENAME/)") | ||
|
||
echoCmd := &Command{ | ||
Use: "echo [string to echo]", | ||
Aliases: []string{"say"}, | ||
Short: "Echo anything's to the screen", | ||
Long: "an utterly useless command for testing.", | ||
Example: "Just run cobra-test echo", | ||
Run: emptyRun, | ||
} | ||
|
||
echoCmd.Flags().String("filename", "", "Enter a filename") | ||
echoCmd.MarkFlagFilename("filename", "json", "yaml", "yml") | ||
echoCmd.Flags().String("config", "", "config to use (located in /config/PROFILE/)") | ||
|
||
printCmd := &Command{ | ||
Use: "print [string to print]", | ||
Args: MinimumNArgs(1), | ||
Short: "Print anything to the screen", | ||
Long: "an absolutely utterly useless command for testing.", | ||
Run: emptyRun, | ||
} | ||
|
||
deprecatedCmd := &Command{ | ||
Use: "deprecated [can't do anything here]", | ||
Args: NoArgs, | ||
Short: "A command which is deprecated", | ||
Long: "an absolutely utterly useless command for testing deprecation!.", | ||
Deprecated: "Please use echo instead", | ||
Run: emptyRun, | ||
} | ||
|
||
colonCmd := &Command{ | ||
Use: "cmd:colon", | ||
Run: emptyRun, | ||
} | ||
|
||
timesCmd := &Command{ | ||
Use: "times [# times] [string to echo]", | ||
SuggestFor: []string{"counts"}, | ||
Args: OnlyValidArgs, | ||
ValidArgs: []string{"one", "two", "three", "four"}, | ||
Short: "Echo anything to the screen more times", | ||
Long: "a slightly useless command for testing.", | ||
Run: emptyRun, | ||
} | ||
|
||
echoCmd.AddCommand(timesCmd) | ||
rootCmd.AddCommand(echoCmd, printCmd, deprecatedCmd, colonCmd) | ||
|
||
buf := new(bytes.Buffer) | ||
rootCmd.GenFishCompletion(buf) | ||
output := buf.String() | ||
|
||
// check for preamble helper functions | ||
check(t, output, "__fish_root_no_subcommand") | ||
check(t, output, "__fish_root_seen_subcommand_path") | ||
check(t, output, "__fish_seen_argument") | ||
|
||
// check for subcommands | ||
check(t, output, "-a echo") | ||
check(t, output, "-a print") | ||
checkOmit(t, output, "-a deprecated") | ||
check(t, output, "-a cmd:colon") | ||
|
||
// check for nested subcommands | ||
checkRegex(t, output, `-n '__fish_root_seen_subcommand_path echo(; and[^']*)?' -a times`) | ||
|
||
// check for flags that will take arguments | ||
check(t, output, "-n '__fish_root_no_subcommand' -r -s i -l introot") | ||
check(t, output, "-n '__fish_root_no_subcommand' -r -l filename") | ||
check(t, output, "-n '__fish_root_no_subcommand' -r -l persistent-filename") | ||
check(t, output, "-n '__fish_root_no_subcommand' -r -l theme") | ||
check(t, output, "-n '__fish_root_seen_subcommand_path echo' -r -l config") | ||
check(t, output, "-n '__fish_root_seen_subcommand_path echo' -r -l filename") | ||
|
||
// checks escape of description in flags | ||
check(t, output, "-n '__fish_root_no_subcommand' -r -s i -l introot -d 'help\\'s message for flag introot'") | ||
|
||
// check for persistent flags that will take arguments | ||
check(t, output, "-n '__fish_root_seen_subcommand_path cmd:colon' -r -l persistent-filename") | ||
check(t, output, "-n '__fish_root_seen_subcommand_path echo' -r -l persistent-filename") | ||
check(t, output, "-n '__fish_root_seen_subcommand_path echo times' -r -l persistent-filename") | ||
check(t, output, "-n '__fish_root_seen_subcommand_path print' -r -l persistent-filename") | ||
|
||
// check for local non-persistent flags | ||
checkRegex(t, output, `; and not __fish_seen_argument -l custom[^']*' -a echo`) | ||
checkRegex(t, output, `; and not __fish_seen_argument -l filename[^']*' -a echo`) | ||
checkRegex(t, output, `; and not __fish_seen_argument -l filename-ext[^']*' -a echo`) | ||
checkRegex(t, output, `; and not __fish_seen_argument -s i -l introot[^']*' -a echo`) | ||
checkRegex(t, output, `; and not __fish_seen_argument -l theme[^']*' -a echo`) | ||
|
||
// check for positional arguments to a command | ||
checkRegex(t, output, `-n '__fish_root_no_subcommand(; and[^']*)?' -a pod`) | ||
checkRegex(t, output, `-n '__fish_root_no_subcommand(; and[^']*)?' -a node`) | ||
checkRegex(t, output, `-n '__fish_root_no_subcommand(; and[^']*)?' -a service`) | ||
checkRegex(t, output, `-n '__fish_root_no_subcommand(; and[^']*)?' -a replicationcontroller`) | ||
|
||
// check for aliases to positional arguments for a command | ||
checkRegex(t, output, `-n '__fish_root_no_subcommand(; and[^']*)?' -a pods`) | ||
checkRegex(t, output, `-n '__fish_root_no_subcommand(; and[^']*)?' -a nodes`) | ||
checkRegex(t, output, `-n '__fish_root_no_subcommand(; and[^']*)?' -a services`) | ||
checkRegex(t, output, `-n '__fish_root_no_subcommand(; and[^']*)?' -a replicationcontrollers`) | ||
checkRegex(t, output, `-n '__fish_root_no_subcommand(; and[^']*)?' -a po`) | ||
checkRegex(t, output, `-n '__fish_root_no_subcommand(; and[^']*)?' -a no`) | ||
checkRegex(t, output, `-n '__fish_root_no_subcommand(; and[^']*)?' -a svc`) | ||
checkRegex(t, output, `-n '__fish_root_no_subcommand(; and[^']*)?' -a rc`) | ||
} |