Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add fish completion support #754

Closed
167 changes: 167 additions & 0 deletions fish_completions.go
@@ -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
}
141 changes: 141 additions & 0 deletions fish_completions_test.go
@@ -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`)
}