feat: add '[args]' and 'Valid Args:' to --help

This commit is contained in:
umarcor 2019-03-19 19:41:09 +01:00
parent a9dbe74c87
commit 8825c7b3f9
4 changed files with 498 additions and 125 deletions

22
args.go
View file

@ -26,27 +26,13 @@ func validateArgs(cmd *Command, args []string) error {
return nil return nil
} }
// Legacy arg validation has the following behaviour:
// - root commands with no subcommands can take arbitrary arguments
// - root commands with subcommands will do subcommand validity checking
// - subcommands will always accept arbitrary arguments
func legacyArgs(cmd *Command, args []string) error {
// no subcommand, always take args
if !cmd.HasSubCommands() {
return nil
}
// root command with subcommands, do subcommand checking.
if !cmd.HasParent() && len(args) > 0 {
return fmt.Errorf("unknown command %q for %q%s", args[0], cmd.CommandPath(), cmd.findSuggestions(args[0]))
}
return nil
}
// NoArgs returns an error if any args are included. // NoArgs returns an error if any args are included.
func NoArgs(cmd *Command, args []string) error { func NoArgs(cmd *Command, args []string) error {
if len(args) > 0 { if len(args) > 0 {
return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath()) if cmd.HasAvailableSubCommands() {
return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath())
}
return fmt.Errorf("\"%s\" rejected; %q does not accept args", args[0], cmd.CommandPath())
} }
return nil return nil
} }

View file

@ -1,19 +1,60 @@
package cobra package cobra
import ( import (
"bytes"
"fmt" "fmt"
"strings" "strings"
"testing" "testing"
) )
func executeUsage(c *Command) (string, error) {
buf := new(bytes.Buffer)
c.SetOutput(buf)
err := c.Usage()
return buf.String(), err
}
func checkOutput(o string, t *testing.T, i string) {
str := map[rune]string{
'u': "Usage:",
'h': "Run 'c --help' for usage",
'c': "c [command]",
'v': "Valid Args:",
'a': "c [flags] [args]",
'f': "c [flags]",
}
for _, x := range "uhcva" {
b := strings.Contains(i, string(x))
if s := str[x]; b != strings.Contains(o, s) {
m := "Did not expect"
if b {
m = "Expected"
}
t.Errorf("%s to find '%s' in the output", m, s)
continue
}
if (x == 'a') && b {
return
}
}
}
func expectErrorAndCheckOutput(t *testing.T, err error, err_k, o, i string) {
// expectError(err, t, err_k)
// checkOutput(o, t, i)
}
type argsTestcase struct { type argsTestcase struct {
exerr string // Expected error key (see map[string][string]) exerr string // Expected error key (see map[string][string])
args PositionalArgs // Args validator args PositionalArgs // Args validator
wValid bool // Define `ValidArgs` in the command wValid, wRun bool // Define `ValidArgs` in the command
rargs []string // Runtime args rargs []string // Runtime args
} }
var errStrings = map[string]string{ var errStrings = map[string]string{
"run": `command "c" is not runnable`,
"runsub": `command "c" is not runnable; please provide a subcmd`,
"no": `"one" rejected; "c" does not accept args`,
"invalid": `invalid argument "a" for "c"`, "invalid": `invalid argument "a" for "c"`,
"unknown": `unknown command "one" for "c"`, "unknown": `unknown command "one" for "c"`,
"less": "requires at least 2 arg(s), only received 1", "less": "requires at least 2 arg(s), only received 1",
@ -22,17 +63,29 @@ var errStrings = map[string]string{
"notinrange": "accepts between 2 and 4 arg(s), received 1", "notinrange": "accepts between 2 and 4 arg(s), received 1",
} }
func (tc *argsTestcase) test(t *testing.T) { func newCmd(args PositionalArgs, wValid, wRun bool) *Command {
c := &Command{ c := &Command{
Use: "c", Use: "c",
Args: tc.args, Short: "A generator",
Run: emptyRun, Long: `Cobra is a CLI ...`,
//Run: emptyRun,
} }
if tc.wValid { if args != nil {
c.Args = args
}
if wValid {
c.ValidArgs = []string{"one", "two", "three"} c.ValidArgs = []string{"one", "two", "three"}
} }
if wRun {
c.Run = func(cmd *Command, args []string) {
//fmt.Println("RUN", args)
}
}
return c
}
o, e := executeCommand(c, tc.rargs...) func (tc *argsTestcase) test(t *testing.T) {
o, e := executeCommand(newCmd(tc.args, tc.wValid, tc.wRun), tc.rargs...)
if len(tc.exerr) > 0 { if len(tc.exerr) > 0 {
// Expect error // Expect error
@ -66,72 +119,72 @@ func testArgs(t *testing.T, tests map[string]argsTestcase) {
func TestArgs_No(t *testing.T) { func TestArgs_No(t *testing.T) {
testArgs(t, map[string]argsTestcase{ testArgs(t, map[string]argsTestcase{
" | ": {"", NoArgs, false, []string{}}, " | ": {"", NoArgs, false, true, []string{}},
" | Arb": {"unknown", NoArgs, false, []string{"one"}}, " | Arb": {"no", NoArgs, false, true, []string{"one"}},
"Valid | Valid": {"unknown", NoArgs, true, []string{"one"}}, "Valid | Valid": {"no", NoArgs, true, true, []string{"one"}},
}) })
} }
func TestArgs_Nil(t *testing.T) { func TestArgs_Nil(t *testing.T) {
testArgs(t, map[string]argsTestcase{ testArgs(t, map[string]argsTestcase{
" | Arb": {"", nil, false, []string{"a", "b"}}, " | Arb": {"", nil, false, true, []string{"a", "b"}},
"Valid | Valid": {"", nil, true, []string{"one", "two"}}, "Valid | Valid": {"", nil, true, true, []string{"one", "two"}},
"Valid | Invalid": {"invalid", nil, true, []string{"a"}}, "Valid | Invalid": {"invalid", nil, true, true, []string{"a"}},
}) })
} }
func TestArgs_Arbitrary(t *testing.T) { func TestArgs_Arbitrary(t *testing.T) {
testArgs(t, map[string]argsTestcase{ testArgs(t, map[string]argsTestcase{
" | Arb": {"", ArbitraryArgs, false, []string{"a", "b"}}, " | Arb": {"", ArbitraryArgs, false, true, []string{"a", "b"}},
"Valid | Valid": {"", ArbitraryArgs, true, []string{"one", "two"}}, "Valid | Valid": {"", ArbitraryArgs, true, true, []string{"one", "two"}},
"Valid | Invalid": {"invalid", ArbitraryArgs, true, []string{"a"}}, "Valid | Invalid": {"invalid", ArbitraryArgs, true, true, []string{"a"}},
}) })
} }
func TestArgs_MinimumN(t *testing.T) { func TestArgs_MinimumN(t *testing.T) {
testArgs(t, map[string]argsTestcase{ testArgs(t, map[string]argsTestcase{
" | Arb": {"", MinimumNArgs(2), false, []string{"a", "b", "c"}}, " | Arb": {"", MinimumNArgs(2), false, true, []string{"a", "b", "c"}},
"Valid | Valid": {"", MinimumNArgs(2), true, []string{"one", "three"}}, "Valid | Valid": {"", MinimumNArgs(2), true, true, []string{"one", "three"}},
"Valid | Invalid": {"invalid", MinimumNArgs(2), true, []string{"a", "b"}}, "Valid | Invalid": {"invalid", MinimumNArgs(2), true, true, []string{"a", "b"}},
" | Less": {"less", MinimumNArgs(2), false, []string{"a"}}, " | Less": {"less", MinimumNArgs(2), false, true, []string{"a"}},
"Valid | Less": {"less", MinimumNArgs(2), true, []string{"one"}}, "Valid | Less": {"less", MinimumNArgs(2), true, true, []string{"one"}},
"Valid | LessInvalid": {"invalid", MinimumNArgs(2), true, []string{"a"}}, "Valid | LessInvalid": {"invalid", MinimumNArgs(2), true, true, []string{"a"}},
}) })
} }
func TestArgs_MaximumN(t *testing.T) { func TestArgs_MaximumN(t *testing.T) {
testArgs(t, map[string]argsTestcase{ testArgs(t, map[string]argsTestcase{
" | Arb": {"", MaximumNArgs(3), false, []string{"a", "b"}}, " | Arb": {"", MaximumNArgs(3), false, true, []string{"a", "b"}},
"Valid | Valid": {"", MaximumNArgs(2), true, []string{"one", "three"}}, "Valid | Valid": {"", MaximumNArgs(2), true, true, []string{"one", "three"}},
"Valid | Invalid": {"invalid", MaximumNArgs(2), true, []string{"a", "b"}}, "Valid | Invalid": {"invalid", MaximumNArgs(2), true, true, []string{"a", "b"}},
" | More": {"more", MaximumNArgs(2), false, []string{"a", "b", "c"}}, " | More": {"more", MaximumNArgs(2), false, true, []string{"a", "b", "c"}},
"Valid | More": {"more", MaximumNArgs(2), true, []string{"one", "three", "two"}}, "Valid | More": {"more", MaximumNArgs(2), true, true, []string{"one", "three", "two"}},
"Valid | MoreInvalid": {"invalid", MaximumNArgs(2), true, []string{"a", "b", "c"}}, "Valid | MoreInvalid": {"invalid", MaximumNArgs(2), true, true, []string{"a", "b", "c"}},
}) })
} }
func TestArgs_Exact(t *testing.T) { func TestArgs_Exact(t *testing.T) {
testArgs(t, map[string]argsTestcase{ testArgs(t, map[string]argsTestcase{
" | Arb": {"", ExactArgs(3), false, []string{"a", "b", "c"}}, " | Arb": {"", ExactArgs(3), false, true, []string{"a", "b", "c"}},
"Valid | Valid": {"", ExactArgs(3), true, []string{"three", "one", "two"}}, "Valid | Valid": {"", ExactArgs(3), true, true, []string{"three", "one", "two"}},
"Valid | Invalid": {"invalid", ExactArgs(3), true, []string{"three", "a", "two"}}, "Valid | Invalid": {"invalid", ExactArgs(3), true, true, []string{"three", "a", "two"}},
" | InvalidCount": {"notexact", ExactArgs(2), false, []string{"a", "b", "c"}}, " | InvalidCount": {"notexact", ExactArgs(2), false, true, []string{"a", "b", "c"}},
"Valid | InvalidCount": {"notexact", ExactArgs(2), true, []string{"three", "one", "two"}}, "Valid | InvalidCount": {"notexact", ExactArgs(2), true, true, []string{"three", "one", "two"}},
"Valid | InvalidCountInvalid": {"invalid", ExactArgs(2), true, []string{"three", "a", "two"}}, "Valid | InvalidCountInvalid": {"invalid", ExactArgs(2), true, true, []string{"three", "a", "two"}},
}) })
} }
func TestArgs_Range(t *testing.T) { func TestArgs_Range(t *testing.T) {
testArgs(t, map[string]argsTestcase{ testArgs(t, map[string]argsTestcase{
" | Arb": {"", RangeArgs(2, 4), false, []string{"a", "b", "c"}}, " | Arb": {"", RangeArgs(2, 4), false, true, []string{"a", "b", "c"}},
"Valid | Valid": {"", RangeArgs(2, 4), true, []string{"three", "one", "two"}}, "Valid | Valid": {"", RangeArgs(2, 4), true, true, []string{"three", "one", "two"}},
"Valid | Invalid": {"invalid", RangeArgs(2, 4), true, []string{"three", "a", "two"}}, "Valid | Invalid": {"invalid", RangeArgs(2, 4), true, true, []string{"three", "a", "two"}},
" | InvalidCount": {"notinrange", RangeArgs(2, 4), false, []string{"a"}}, " | InvalidCount": {"notinrange", RangeArgs(2, 4), false, true, []string{"a"}},
"Valid | InvalidCount": {"notinrange", RangeArgs(2, 4), true, []string{"two"}}, "Valid | InvalidCount": {"notinrange", RangeArgs(2, 4), true, true, []string{"two"}},
"Valid | InvalidCountInvalid": {"invalid", RangeArgs(2, 4), true, []string{"a"}}, "Valid | InvalidCountInvalid": {"invalid", RangeArgs(2, 4), true, true, []string{"a"}},
}) })
} }
func TestArgs_DEPRECATED(t *testing.T) { func TestArgs_DEPRECATED(t *testing.T) {
testArgs(t, map[string]argsTestcase{ testArgs(t, map[string]argsTestcase{
"OnlyValid | Valid | Valid": {"", OnlyValidArgs, true, []string{"one", "two"}}, "OnlyValid | Valid | Valid": {"", OnlyValidArgs, true, true, []string{"one", "two"}},
"OnlyValid | Valid | Invalid": {"invalid", OnlyValidArgs, true, []string{"a"}}, "OnlyValid | Valid | Invalid": {"invalid", OnlyValidArgs, true, true, []string{"a"}},
"ExactValid | Valid | Valid": {"", ExactValidArgs(3), true, []string{"two", "three", "one"}}, "ExactValid | Valid | Valid": {"", ExactValidArgs(3), true, true, []string{"two", "three", "one"}},
"ExactValid | Valid | InvalidCount": {"notexact", ExactValidArgs(2), true, []string{"two", "three", "one"}}, "ExactValid | Valid | InvalidCount": {"notexact", ExactValidArgs(2), true, true, []string{"two", "three", "one"}},
"ExactValid | Valid | Invalid": {"invalid", ExactValidArgs(2), true, []string{"two", "a"}}, "ExactValid | Valid | Invalid": {"invalid", ExactValidArgs(2), true, true, []string{"two", "a"}},
}) })
} }
@ -176,7 +229,7 @@ func TestChildTakesNoArgs(t *testing.T) {
} }
got := err.Error() got := err.Error()
expected := `unknown command "illegal" for "root child"` expected := `"illegal" rejected; "root child" does not accept args`
if !strings.Contains(got, expected) { if !strings.Contains(got, expected) {
t.Errorf("expected %q, got %q", expected, got) t.Errorf("expected %q, got %q", expected, got)
} }
@ -193,6 +246,280 @@ func TestChildTakesArgs(t *testing.T) {
} }
} }
// NOTE 'c [command]' is not shown because this command does not have any subcommand
// NOTE 'Valid Args:' is not shown because this command is not runnable
// NOTE 'c [flags]' is not shown because this command is not runnable
func noRunChecks(t *testing.T, err error, err_k, o string) {
expectErrorAndCheckOutput(t, err, err_k, o, "u")
}
// NoRun (no children)
func TestArgs_NoRun(t *testing.T) {
tc := argsTestcase{"run", nil, false, false, []string{}}
t.Run("|", tc.test)
// noRunChecks(t, e, "run", o)
}
func TestArgs_NoRun_ArbValid(t *testing.T) {
tc := argsTestcase{"run", nil, false, false, []string{"one", "three"}}
t.Run("|", tc.test)
// noRunChecks(t, e, "run", o)
}
func TestArgs_NoRun_Invalid(t *testing.T) {
tc := argsTestcase{"run", nil, false, false, []string{"two", "a"}}
t.Run("|", tc.test)
//noRunChecks(t, e, "run", o)
}
// NoRun (with children)
// NOTE 'Valid Args:' is not shown because this command is not runnable
// NOTE 'c [flags]' is not shown because this command is not runnable
func TestArgs_NoRun_wChild(t *testing.T) {
c := newCmd(nil, false, false)
d := newCmd(nil, false, true)
c.AddCommand(d)
o, e := executeCommand(c)
expectErrorAndCheckOutput(t, e, "runsub", o, "uc")
}
func TestArgs_NoRun_wChild_ArbValid(t *testing.T) {
c := newCmd(nil, false, false)
d := newCmd(nil, false, true)
c.AddCommand(d)
o, e := executeCommand(c, "one", "three")
expectErrorAndCheckOutput(t, e, "runsub", o, "h")
}
func TestArgs_NoRun_wChild_Invalid(t *testing.T) {
c := newCmd(nil, false, false)
d := newCmd(nil, false, true)
c.AddCommand(d)
o, e := executeCommand(c, "one", "a")
expectErrorAndCheckOutput(t, e, "runsub", o, "h")
}
// NoRun Args
func TestArgs_NoRun_wArgs(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"run", ArbitraryArgs, false, false, []string{}},
})
//noRunChecks(t, e, "run", o)
}
func TestArgs_NoRun_wArgs_ArbValid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"run", ArbitraryArgs, false, false, []string{"one", "three"}},
})
//noRunChecks(t, e, "run", o)
}
func TestArgs_NoRun_wArgs_Invalid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"run", ArbitraryArgs, false, false, []string{"two", "a"}},
})
//noRunChecks(t, e, "run", o)
}
// NoRun ValidArgs
func TestArgs_NoRun_wValid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"run", nil, true, false, []string{}},
})
//noRunChecks(t, e, "run", o)
}
func TestArgs_NoRun_wValid_ArbValid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"run", nil, true, false, []string{"one", "three"}},
})
//noRunChecks(t, e, "run", o)
}
func TestArgs_NoRun_wValid_Invalid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"run", nil, true, false, []string{"two", "a"}},
})
//noRunChecks(t, e, "run", o)
}
// NoRun Args ValidArgs
func TestArgs_NoRun_wArgswValid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"run", ArbitraryArgs, true, false, []string{}},
})
// noRunChecks(t, e, "run", o)
}
func TestArgs_NoRun_wArgswValid_ArbValid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"run", ArbitraryArgs, true, false, []string{"one", "three"}},
})
// noRunChecks(t, e, "run", o)
}
func TestArgs_NoRun_wArgswValid_Invalid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"run", ArbitraryArgs, true, false, []string{"two", "a"}},
})
// noRunChecks(t, e, "run", o)
}
// Run (no children)
// NOTE 'c [command]' is not shown because this command does not have any subcommand
// NOTE 'Valid Args:' is not shown because ValidArgs is not defined
func TestArgs_Run(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"", nil, false, true, []string{}},
})
//o, e = executeUsage(c)
//checkOutput(o, t, "ua")
}
func TestArgs_Run_ArbValid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"", nil, false, true, []string{"one", "three"}},
})
// o, e = executeUsage(c)
// checkOutput(o, t, "ua")
}
func TestArgs_Run_Invalid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"", nil, false, true, []string{"two", "a"}},
})
//o, e = executeUsage(c)
//checkOutput(o, t, "ua")
}
// Run (with children)
// NOTE 'Valid Args:' is not shown because ValidArgs is not defined
func TestArgs_Run_wChild(t *testing.T) {
c := newCmd(nil, false, true)
d := newCmd(nil, false, true)
c.AddCommand(d)
// o, e := executeCommand(c)
// expectSuccess(o, e, t)
o, _ := executeUsage(c)
checkOutput(o, t, "ucf")
}
func TestArgs_Run_wChild_ArbValid(t *testing.T) {
c := newCmd(nil, false, true)
d := newCmd(nil, false, false)
c.AddCommand(d)
o, _ := executeCommand(c, "one", "three")
// expectError(e, t, "no")
// NOTE 'c [command]' is not shown because this command does not have any available subcommand
checkOutput(o, t, "uf")
}
func TestArgs_Run_wChild_Invalid(t *testing.T) {
c := newCmd(nil, false, true)
d := newCmd(nil, false, false)
c.AddCommand(d)
o, _ := executeCommand(c, "one", "a")
// expectError(e, t, "no")
// NOTE 'c [command]' is not shown because this command does not have any available subcommand
checkOutput(o, t, "uf")
}
// Run Args
// NOTE 'c [command]' is not shown because this command does not have any subcommand
func TestArgs_Run_wArgs(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"", ArbitraryArgs, false, true, []string{}},
})
// o, e = executeUsage(c)
// checkOutput(o, t, "ua")
}
func TestArgs_Run_wArgs_ArbValid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"", ArbitraryArgs, false, true, []string{"one", "three"}},
})
// o, e = executeUsage(c)
// checkOutput(o, t, "ua")
}
func TestArgs_Run_wArgs_Invalid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"", ArbitraryArgs, false, true, []string{"two", "a"}},
})
// o, e = executeUsage(c)
// checkOutput(o, t, "ua")
}
// Run ValidArgs
// NOTE 'c [command]' is not shown because this command does not have any subcommand
func TestArgs_Run_wValid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"", nil, true, true, []string{}},
})
// o, e = executeUsage(c)
// checkOutput(o, t, "uva")
}
func TestArgs_Run_wValid_ArbValid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"", nil, true, true, []string{"one", "three"}},
})
// o, e = executeUsage(c)
// checkOutput(o, t, "uva")
}
func TestArgs_Run_wValid_Invalid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"invalid", nil, true, true, []string{"two", "a"}},
})
// checkOutput(o, t, "uva")
}
// Run Args ValidArgs
// NOTE 'c [command]' is not shown because this command does not have any subcommand
func TestArgs_Run_wArgswValid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"", ArbitraryArgs, true, true, []string{}},
})
//o, e = executeUsage(c)
//checkOutput(o, t, "uva")
}
func TestArgs_Run_wArgswValid_ArbValid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"", ArbitraryArgs, true, true, []string{"one", "three"}},
})
//o, e = executeUsage(c)
//checkOutput(o, t, "uva")
}
func TestArgs_Run_wArgswValid_Invalid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"invalid", ArbitraryArgs, true, true, []string{"two", "a"}},
})
//checkOutput(o, t, "uva")
}
//
func TestArgs_Run_wMinimumNArgs_ArbValid(t *testing.T) {
testArgs(t, map[string]argsTestcase{
"|": {"", MinimumNArgs(2), false, true, []string{"one", "three"}},
})
//o, e = executeUsage(c)
//checkOutput(o, t, "ua")
}
func TestMatchAll(t *testing.T) { func TestMatchAll(t *testing.T) {
// Somewhat contrived example check that ensures there are exactly 3 // Somewhat contrived example check that ensures there are exactly 3
// arguments, and each argument is exactly 2 bytes long. // arguments, and each argument is exactly 2 bytes long.

View file

@ -29,6 +29,13 @@ import (
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
) )
func ErrSubCommandRequired(s string) error {
return fmt.Errorf("command %s is not runnable; please provide a subcmd", s)
}
func ErrCommandNotRunnable(s string) error {
return fmt.Errorf(`command "%s" is not runnable`, s)
}
// FParseErrWhitelist configures Flag parse errors to be ignored // FParseErrWhitelist configures Flag parse errors to be ignored
type FParseErrWhitelist flag.ParseErrorsWhitelist type FParseErrWhitelist flag.ParseErrorsWhitelist
@ -505,7 +512,10 @@ func (c *Command) UsageTemplate() string {
{{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}
Aliases: Aliases:
{{.NameAndAliases}}{{end}}{{if .HasExample}} {{.NameAndAliases}}{{end}}{{if (and .HasValidArgs .Runnable)}}
Valid Args:
{{range .ValidArgs}}{{.}} {{end}}{{end}}{{if .HasExample}}
Examples: Examples:
{{.Example}}{{end}}{{if .HasAvailableSubCommands}} {{.Example}}{{end}}{{if .HasAvailableSubCommands}}
@ -632,7 +642,7 @@ func isFlagArg(arg string) bool {
// 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) {
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) {
@ -649,11 +659,13 @@ func (c *Command) Find(args []string) (*Command, []string, error) {
return c, innerArgs return c, innerArgs
} }
commandFound, a := innerfind(c, args) cF, a := innerfind(c, args)
if commandFound.Args == nil { // if Args is undefined and this is a root command with subcommands,
return commandFound, a, legacyArgs(commandFound, stripFlags(a, commandFound)) // do not accept arguments, unless ValidArgs is set
if cF.Args == nil && cF.HasSubCommands() && !cF.HasParent() && (len(cF.ValidArgs) == 0) {
cF.Args = NoArgs
} }
return commandFound, a, nil return cF, a
} }
func (c *Command) findSuggestions(arg string) string { func (c *Command) findSuggestions(arg string) string {
@ -694,7 +706,7 @@ func (c *Command) findNext(next string) *Command {
// Traverse the command tree to find the command, and parse args for // Traverse the command tree to find the command, and parse args for
// each parent. // each parent.
func (c *Command) Traverse(args []string) (*Command, []string, error) { func (c *Command) Traverse(args []string) (*Command, []string) {
flags := []string{} flags := []string{}
inFlag := false inFlag := false
@ -724,15 +736,15 @@ func (c *Command) Traverse(args []string) (*Command, []string, error) {
cmd := c.findNext(arg) cmd := c.findNext(arg)
if cmd == nil { if cmd == nil {
return c, args, nil return c, args
} }
if err := c.ParseFlags(flags); err != nil { if err := c.ParseFlags(flags); err != nil {
return nil, args, err return nil, append([]string{err.Error()}, args...)
} }
return cmd.Traverse(args[i+1:]) return cmd.Traverse(args[i+1:])
} }
return c, args, nil return c, args
} }
// SuggestionsFor provides suggestions for the typedName. // SuggestionsFor provides suggestions for the typedName.
@ -828,7 +840,10 @@ func (c *Command) execute(a []string) (err error) {
} }
if !c.Runnable() { if !c.Runnable() {
return flag.ErrHelp if c.HasAvailableSubCommands() {
return ErrSubCommandRequired(c.Name())
}
return ErrCommandNotRunnable(c.Name())
} }
c.preRun() c.preRun()
@ -949,7 +964,6 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
c.initDefaultCompletionCmd() c.initDefaultCompletionCmd()
args := c.args args := c.args
// Workaround FAIL with "go test -v" or "cobra.test -test.v", see #155 // Workaround FAIL with "go test -v" or "cobra.test -test.v", see #155
if c.args == nil && filepath.Base(os.Args[0]) != "cobra.test" { if c.args == nil && filepath.Base(os.Args[0]) != "cobra.test" {
args = os.Args[1:] args = os.Args[1:]
@ -959,35 +973,29 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
c.initCompleteCmd(args) c.initCompleteCmd(args)
var flags []string var flags []string
f := c.Find
if c.TraverseChildren { if c.TraverseChildren {
cmd, flags, err = c.Traverse(args) f = c.Traverse
} else {
cmd, flags, err = c.Find(args)
} }
if err != nil { if cmd, flags = f(args); cmd != nil {
// If found parse to a subcommand and then failed, talk about the subcommand cmd.commandCalledAs.called = true
if cmd != nil { if cmd.commandCalledAs.name == "" {
c = cmd cmd.commandCalledAs.name = cmd.Name()
} }
if !c.SilenceErrors { if !c.SilenceErrors {
c.PrintErrln("Error:", err.Error()) c.PrintErrln("Error:", err.Error())
c.PrintErrf("Run '%v --help' for usage.\n", c.CommandPath()) c.PrintErrf("Run '%v --help' for usage.\n", c.CommandPath())
} }
return c, err // We have to pass global context to children command
// if context is present on the parent command.
if cmd.ctx == nil {
cmd.ctx = c.ctx
}
err = cmd.execute(flags)
} else {
err = fmt.Errorf(flags[0])
} }
cmd.commandCalledAs.called = true
if cmd.commandCalledAs.name == "" {
cmd.commandCalledAs.name = cmd.Name()
}
// We have to pass global context to children command
// if context is present on the parent command.
if cmd.ctx == nil {
cmd.ctx = c.ctx
}
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
// effect // effect
@ -996,6 +1004,11 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
return cmd, nil return cmd, nil
} }
// Check if a shorter help hint should be shown instead of the full Usage()
if err := helpHint(cmd, flags, err.Error()); err != nil {
return cmd, err
}
// If root command has SilenceErrors flagged, // If root command has SilenceErrors flagged,
// all subcommands should respect it // all subcommands should respect it
if !cmd.SilenceErrors && !c.SilenceErrors { if !cmd.SilenceErrors && !c.SilenceErrors {
@ -1011,6 +1024,25 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
return cmd, err return cmd, err
} }
func helpHint(c *Command, fs []string, e string) error {
if len(fs) > 0 {
f := fs[0]
for _, s := range []string{"please provide a subcmd", "unknown command"} {
if strings.Contains(e, s) {
if s := c.findSuggestions(f); len(s) != 0 {
e += s
}
if !c.SilenceErrors {
c.Printf("Error: %s\n", e)
c.Printf("Run '%v --help' for usage.\n", c.CommandPath())
}
return fmt.Errorf("%s", e)
}
}
}
return nil
}
// ValidateArgs returns an error if any positional args are not in the // ValidateArgs returns an error if any positional args are not in the
// `ValidArgs` field of `Command`. Then, run the `Args` validator, if // `ValidArgs` field of `Command`. Then, run the `Args` validator, if
// specified. // specified.
@ -1277,9 +1309,35 @@ func (c *Command) UseLine() string {
if c.HasAvailableFlags() && !strings.Contains(useline, "[flags]") { if c.HasAvailableFlags() && !strings.Contains(useline, "[flags]") {
useline += " [flags]" useline += " [flags]"
} }
useline += useLineArgs(c)
return useline return useline
} }
// useLineArgs puts out '[args]' if a given command accepts positional args
func useLineArgs(c *Command) (s string) {
s = " [args]"
if c.Args == nil {
if !c.HasAvailableSubCommands() || c.HasParent() {
return
}
// if Args is undefined and this is a root command with subcommands,
// do not accept arguments, unless ValidArgs is set
if !c.HasParent() && c.HasAvailableSubCommands() && (len(c.ValidArgs) > 0) {
return
}
return ""
}
// Check if the Args validator is other than 'NoArgs'
err := c.Args(c, []string{"someUnexpectedIllegalArg"})
nerr := NoArgs(c, []string{"someUnexpectedIllegalArg"})
if err == nil || ((nerr != nil) && (err.Error() != nerr.Error())) {
return
}
return ""
}
// DebugFlags used to determine which flags have been assigned to which commands // DebugFlags used to determine which flags have been assigned to which commands
// and which persist. // and which persist.
func (c *Command) DebugFlags() { func (c *Command) DebugFlags() {
@ -1371,6 +1429,10 @@ func (c *Command) NameAndAliases() string {
return strings.Join(append([]string{c.Name()}, c.Aliases...), ", ") return strings.Join(append([]string{c.Name()}, c.Aliases...), ", ")
} }
func (c *Command) HasValidArgs() bool {
return len(c.ValidArgs) > 0
}
// HasExample determines if the command has example. // HasExample determines if the command has example.
func (c *Command) HasExample() bool { func (c *Command) HasExample() bool {
return len(c.Example) > 0 return len(c.Example) > 0
@ -1444,16 +1506,14 @@ func (c *Command) HasHelpSubCommands() bool {
// HasAvailableSubCommands determines if a command has available sub commands that // HasAvailableSubCommands determines if a command has available sub commands that
// need to be shown in the usage/help default template under 'available commands'. // need to be shown in the usage/help default template under 'available commands'.
func (c *Command) HasAvailableSubCommands() bool { func (c *Command) HasAvailableSubCommands() bool {
// return true on the first found available (non deprecated/help/hidden) // return true on the first found available (non deprecated/help/hidden) subcmd
// sub command
for _, sub := range c.commands { for _, sub := range c.commands {
if sub.IsAvailableCommand() { if sub.IsAvailableCommand() {
return true return true
} }
} }
// the command either has no sub commands,
// the command either has no sub commands, or no available (non deprecated/help/hidden) // or no available (non deprecated/help/hidden) subcmds
// sub commands
return false return false
} }

View file

@ -134,9 +134,7 @@ func TestRootExecuteUnknownCommand(t *testing.T) {
rootCmd.AddCommand(&Command{Use: "child", Run: emptyRun}) rootCmd.AddCommand(&Command{Use: "child", Run: emptyRun})
output, _ := executeCommand(rootCmd, "unknown") output, _ := executeCommand(rootCmd, "unknown")
expected := "Error: unknown command \"unknown\" for \"root\"\nRun 'root --help' for usage.\n" expected := "Error: unknown command \"unknown\" for \"root\"\nRun 'root --help' for usage.\n"
if output != expected { if output != expected {
t.Errorf("Expected:\n %q\nGot:\n %q\n", expected, output) t.Errorf("Expected:\n %q\nGot:\n %q\n", expected, output)
} }
@ -968,11 +966,13 @@ func TestHelpExecutedOnNonRunnableChild(t *testing.T) {
rootCmd.AddCommand(childCmd) rootCmd.AddCommand(childCmd)
output, err := executeCommand(rootCmd, "child") output, err := executeCommand(rootCmd, "child")
if err != nil {
t.Errorf("Unexpected error: %v", err) expected := `command "child" is not runnable`
if err.Error() != expected {
t.Errorf("Expected %q, got %q", expected, err.Error())
} }
checkStringContains(t, output, childCmd.Long) checkStringContains(t, output, "Usage:")
} }
func TestVersionFlagExecuted(t *testing.T) { func TestVersionFlagExecuted(t *testing.T) {
@ -1820,9 +1820,9 @@ func TestTraverseWithParentFlags(t *testing.T) {
rootCmd.AddCommand(childCmd) rootCmd.AddCommand(childCmd)
c, args, err := rootCmd.Traverse([]string{"-b", "--str", "ok", "child", "--int"}) c, args := rootCmd.Traverse([]string{"-b", "--str", "ok", "child", "--int"})
if err != nil { if c == nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %s", args[0])
} }
if len(args) != 1 && args[0] != "--add" { if len(args) != 1 && args[0] != "--add" {
t.Errorf("Wrong args: %v", args) t.Errorf("Wrong args: %v", args)
@ -1840,9 +1840,9 @@ func TestTraverseNoParentFlags(t *testing.T) {
childCmd.Flags().String("str", "", "") childCmd.Flags().String("str", "", "")
rootCmd.AddCommand(childCmd) rootCmd.AddCommand(childCmd)
c, args, err := rootCmd.Traverse([]string{"child"}) c, args := rootCmd.Traverse([]string{"child"})
if err != nil { if c == nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", args[0])
} }
if len(args) != 0 { if len(args) != 0 {
t.Errorf("Wrong args %v", args) t.Errorf("Wrong args %v", args)
@ -1861,13 +1861,13 @@ func TestTraverseWithBadParentFlags(t *testing.T) {
expected := "unknown flag: --str" expected := "unknown flag: --str"
c, _, err := rootCmd.Traverse([]string{"--str", "ok", "child"}) c, args := rootCmd.Traverse([]string{"--str", "ok", "child"})
if err == nil || !strings.Contains(err.Error(), expected) {
t.Errorf("Expected error, %q, got %q", expected, err)
}
if c != nil { if c != nil {
t.Errorf("Expected nil command") t.Errorf("Expected nil command")
} }
if !strings.Contains(args[0], expected) {
t.Errorf("Expected error, %q, got %q", expected, args[0])
}
} }
func TestTraverseWithBadChildFlag(t *testing.T) { func TestTraverseWithBadChildFlag(t *testing.T) {
@ -1879,9 +1879,9 @@ func TestTraverseWithBadChildFlag(t *testing.T) {
// Expect no error because the last commands args shouldn't be parsed in // Expect no error because the last commands args shouldn't be parsed in
// Traverse. // Traverse.
c, args, err := rootCmd.Traverse([]string{"child", "--str"}) c, args := rootCmd.Traverse([]string{"child", "--str"})
if err != nil { if c == nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %s", args[0])
} }
if len(args) != 1 && args[0] != "--str" { if len(args) != 1 && args[0] != "--str" {
t.Errorf("Wrong args: %v", args) t.Errorf("Wrong args: %v", args)
@ -1902,9 +1902,9 @@ func TestTraverseWithTwoSubcommands(t *testing.T) {
} }
subCmd.AddCommand(subsubCmd) subCmd.AddCommand(subsubCmd)
c, _, err := rootCmd.Traverse([]string{"sub", "subsub"}) c, args := rootCmd.Traverse([]string{"sub", "subsub"})
if err != nil { if c == nil {
t.Fatalf("Unexpected error: %v", err) t.Fatalf("Unexpected error: %v", args[0])
} }
if c.Name() != subsubCmd.Name() { if c.Name() != subsubCmd.Name() {
t.Fatalf("Expected command: %q, got %q", subsubCmd.Name(), c.Name()) t.Fatalf("Expected command: %q, got %q", subsubCmd.Name(), c.Name())