Traverse commands for args on parents.

Signed-off-by: Daniel Nephin <dnephin@gmail.com>
This commit is contained in:
Daniel Nephin 2016-06-27 16:51:07 -04:00
parent bc69223348
commit 6259b8dea9
2 changed files with 111 additions and 26 deletions

View file

@ -125,6 +125,9 @@ type Command struct {
// Must be > 0. // Must be > 0.
SuggestionsMinimumDistance int SuggestionsMinimumDistance int
// TraverseChildCommands parses flags on all parents before the child command
TraverseChildCommands bool
// name is the command name, usually the executable's name. // name is the command name, usually the executable's name.
name string name string
// commands is the list of commands supported by this program. // commands is the list of commands supported by this program.
@ -475,13 +478,14 @@ func argsMinusFirstX(args []string, x string) []string {
return args return args
} }
func isFlagArg(arg string) bool {
return ((len(arg) >= 3 && arg[0] == '-') ||
(len(arg) >= 2 && arg[1] == '-' && arg[1] != '-'))
}
// Find the target command given the args and command tree // Find the target command given the args and command tree
// Meant to be run on the highest node. Only searches down. // Meant to be run on the highest node. Only searches down.
func (c *Command) Find(args []string) (*Command, []string, error) { func (c *Command) Find(args []string) (*Command, []string, error) {
if c == nil {
return nil, nil, fmt.Errorf("Called find() on a nil Command")
}
var innerfind func(*Command, []string) (*Command, []string) var innerfind func(*Command, []string) (*Command, []string)
innerfind = func(c *Command, innerArgs []string) (*Command, []string) { innerfind = func(c *Command, innerArgs []string) (*Command, []string) {
@ -490,28 +494,11 @@ func (c *Command) Find(args []string) (*Command, []string, error) {
return c, innerArgs return c, innerArgs
} }
nextSubCmd := argsWOflags[0] nextSubCmd := argsWOflags[0]
matches := make([]*Command, 0)
for _, cmd := range c.commands { cmd := c.findNext(nextSubCmd)
if cmd.Name() == nextSubCmd || cmd.HasAlias(nextSubCmd) { // exact name or alias match if cmd != nil {
return innerfind(cmd, argsMinusFirstX(innerArgs, nextSubCmd)) return innerfind(cmd, argsMinusFirstX(innerArgs, nextSubCmd))
} }
if EnablePrefixMatching {
if strings.HasPrefix(cmd.Name(), nextSubCmd) { // prefix match
matches = append(matches, cmd)
}
for _, x := range cmd.Aliases {
if strings.HasPrefix(x, nextSubCmd) {
matches = append(matches, cmd)
}
}
}
}
// only accept a single prefix match - multiple matches would be ambiguous
if len(matches) == 1 {
return innerfind(matches[0], argsMinusFirstX(innerArgs, argsWOflags[0]))
}
return c, innerArgs return c, innerArgs
} }
@ -539,6 +526,62 @@ func (c *Command) findSuggestions(arg string) string {
return suggestionsString return suggestionsString
} }
func (c *Command) findNext(next string) *Command {
matches := make([]*Command, 0)
for _, cmd := range c.commands {
if cmd.Name() == next || cmd.HasAlias(next) {
return cmd
}
if EnablePrefixMatching && cmd.HasNameOrAliasPrefix(next) {
matches = append(matches, cmd)
}
}
if len(matches) == 1 {
return matches[0]
}
return nil
}
// Traverse the command tree to find the command, and parse args for
// each parent.
func (c *Command) Traverse(args []string) (*Command, []string, error) {
flags := []string{}
inFlag := false
for i, arg := range args {
switch {
case strings.HasPrefix(arg, "--") && !strings.Contains(arg, "="):
// TODO: this isn't quite right, we should really check ahead for 'true' or 'false'
inFlag = !hasNoOptDefVal(arg[2:], c.Flags())
flags = append(flags, arg)
continue
case strings.HasPrefix(arg, "-") && !strings.Contains(arg, "=") && len(arg) == 2 && !shortHasNoOptDefVal(arg[1:], c.Flags()):
inFlag = true
flags = append(flags, arg)
continue
case inFlag:
inFlag = false
flags = append(flags, arg)
continue
case isFlagArg(arg):
flags = append(flags, arg)
continue
}
cmd := c.findNext(arg)
if cmd == nil {
return c, args, nil
}
if err := c.ParseFlags(flags); err != nil {
return nil, args, err
}
return cmd.Traverse(args[i+1:])
}
return c, args, nil
}
// SuggestionsFor provides suggestions for the typedName. // SuggestionsFor provides suggestions for the typedName.
func (c *Command) SuggestionsFor(typedName string) []string { func (c *Command) SuggestionsFor(typedName string) []string {
suggestions := []string{} suggestions := []string{}
@ -714,7 +757,12 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
args = c.args args = c.args
} }
cmd, flags, err := c.Find(args) var flags []string
if c.TraverseChildCommands {
cmd, flags, err = c.Traverse(args)
} else {
cmd, flags, err = c.Find(args)
}
if err != nil { if err != nil {
// If found parse to a subcommand and then failed, talk about the subcommand // If found parse to a subcommand and then failed, talk about the subcommand
if cmd != nil { if cmd != nil {
@ -726,6 +774,7 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
} }
return c, err return c, err
} }
err = cmd.execute(flags) err = cmd.execute(flags)
if err != nil { if err != nil {
// Always show help if requested, even if SilenceErrors is in // Always show help if requested, even if SilenceErrors is in
@ -993,7 +1042,20 @@ func (c *Command) HasAlias(s string) bool {
return false return false
} }
// NameAndAliases returns string containing name and all aliases // HasNameOrAliasPrefix returns true if the Name or any of aliases start
// with prefix
func (c *Command) HasNameOrAliasPrefix(prefix string) bool {
if strings.HasPrefix(c.Name(), prefix) {
return true
}
for _, alias := range c.Aliases {
if strings.HasPrefix(alias, prefix) {
return true
}
}
return false
}
func (c *Command) NameAndAliases() string { func (c *Command) NameAndAliases() string {
return strings.Join(append([]string{c.Name()}, c.Aliases...), ", ") return strings.Join(append([]string{c.Name()}, c.Aliases...), ", ")
} }

View file

@ -347,3 +347,26 @@ func TestSetHelpCommand(t *testing.T) {
t.Errorf("Expected to contain %q message, but got %q", correctMessage, output.String()) t.Errorf("Expected to contain %q message, but got %q", correctMessage, output.String())
} }
} }
func TestRunWithTraverse(t *testing.T) {
cmd := &Command{
Use: "do",
TraverseChildCommands: true,
}
cmd.Flags().String("foo", "", "foo things")
sub := &Command{Use: "next"}
sub.Flags().String("add", "", "add things")
cmd.AddCommand(sub)
c, args, err := cmd.Traverse([]string{"--foo", "ok", "next"})
if err != nil {
t.Fatalf("Expected no error: %s", err)
}
if len(args) != 0 {
t.Fatalf("wrong args %s", args)
}
if c.Name() != sub.Name() {
t.Fatalf("wrong command %q expected %q", c.Name(), sub.Name())
}
}