diff --git a/app.go b/app.go index 333bd57b05..7e64c2d9f8 100644 --- a/app.go +++ b/app.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "reflect" "sort" "time" ) @@ -43,6 +44,9 @@ type App struct { Version string // Description of the program Description string + // DefaultCommand is the (optional) name of a command + // to run if no command names are passed as CLI arguments. + DefaultCommand string // List of commands to execute Commands []*Command // List of flags to parse @@ -333,13 +337,45 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) { } } + var c *Command args := cCtx.Args() if args.Present() { name := args.First() - c := a.Command(name) - if c != nil { - return c.Run(cCtx) + if a.validCommandName(name) { + c = a.Command(name) + } else { + hasDefault := a.DefaultCommand != "" + isFlagName := checkStringSliceIncludes(name, cCtx.FlagNames()) + + var ( + isDefaultSubcommand = false + defaultHasSubcommands = false + ) + + if hasDefault { + dc := a.Command(a.DefaultCommand) + defaultHasSubcommands = len(dc.Subcommands) > 0 + for _, dcSub := range dc.Subcommands { + if checkStringSliceIncludes(name, dcSub.Names()) { + isDefaultSubcommand = true + break + } + } + } + + if isFlagName || (hasDefault && (defaultHasSubcommands && isDefaultSubcommand)) { + argsWithDefault := a.argsWithDefaultCommand(args) + if !reflect.DeepEqual(args, argsWithDefault) { + c = a.Command(argsWithDefault.First()) + } + } } + } else if a.DefaultCommand != "" { + c = a.Command(a.DefaultCommand) + } + + if c != nil { + return c.Run(cCtx) } if a.Action == nil { @@ -570,6 +606,41 @@ func (a *App) handleExitCoder(cCtx *Context, err error) { } } +func (a *App) commandNames() []string { + var cmdNames []string + + for _, cmd := range a.Commands { + cmdNames = append(cmdNames, cmd.Names()...) + } + + return cmdNames +} + +func (a *App) validCommandName(checkCmdName string) bool { + valid := false + allCommandNames := a.commandNames() + + for _, cmdName := range allCommandNames { + if checkCmdName == cmdName { + valid = true + break + } + } + + return valid +} + +func (a *App) argsWithDefaultCommand(oldArgs Args) Args { + if a.DefaultCommand != "" { + rawArgs := append([]string{a.DefaultCommand}, oldArgs.Slice()...) + newArgs := args(rawArgs) + + return &newArgs + } + + return oldArgs +} + // Author represents someone who has contributed to a cli project. type Author struct { Name string // The Authors name @@ -602,3 +673,15 @@ func HandleAction(action interface{}, cCtx *Context) (err error) { return errInvalidActionType } + +func checkStringSliceIncludes(want string, sSlice []string) bool { + found := false + for _, s := range sSlice { + if want == s { + found = true + break + } + } + + return found +} diff --git a/app_test.go b/app_test.go index 3dd73abc16..437af25911 100644 --- a/app_test.go +++ b/app_test.go @@ -469,6 +469,175 @@ func TestApp_Command(t *testing.T) { } } +var defaultCommandAppTests = []struct { + cmdName string + defaultCmd string + expected bool +}{ + {"foobar", "foobar", true}, + {"batbaz", "foobar", true}, + {"b", "", true}, + {"f", "", true}, + {"", "foobar", true}, + {"", "", true}, + {" ", "", false}, + {"bat", "batbaz", false}, + {"nothing", "batbaz", false}, + {"nothing", "", false}, +} + +func TestApp_RunDefaultCommand(t *testing.T) { + for _, test := range defaultCommandAppTests { + testTitle := fmt.Sprintf("command=%[1]s-default=%[2]s", test.cmdName, test.defaultCmd) + t.Run(testTitle, func(t *testing.T) { + app := &App{ + DefaultCommand: test.defaultCmd, + Commands: []*Command{ + {Name: "foobar", Aliases: []string{"f"}}, + {Name: "batbaz", Aliases: []string{"b"}}, + }, + } + + err := app.Run([]string{"c", test.cmdName}) + expect(t, err == nil, test.expected) + }) + } +} + +var defaultCommandSubCmdAppTests = []struct { + cmdName string + subCmd string + defaultCmd string + expected bool +}{ + {"foobar", "", "foobar", true}, + {"foobar", "carly", "foobar", true}, + {"batbaz", "", "foobar", true}, + {"b", "", "", true}, + {"f", "", "", true}, + {"", "", "foobar", true}, + {"", "", "", true}, + {"", "jimbob", "foobar", true}, + {"", "j", "foobar", true}, + {"", "carly", "foobar", true}, + {"", "jimmers", "foobar", true}, + {"", "jimmers", "", true}, + {" ", "jimmers", "foobar", false}, + {"", "", "", true}, + {" ", "", "", false}, + {" ", "j", "", false}, + {"bat", "", "batbaz", false}, + {"nothing", "", "batbaz", false}, + {"nothing", "", "", false}, + {"nothing", "j", "batbaz", false}, + {"nothing", "carly", "", false}, +} + +func TestApp_RunDefaultCommandWithSubCommand(t *testing.T) { + for _, test := range defaultCommandSubCmdAppTests { + testTitle := fmt.Sprintf("command=%[1]s-subcmd=%[2]s-default=%[3]s", test.cmdName, test.subCmd, test.defaultCmd) + t.Run(testTitle, func(t *testing.T) { + app := &App{ + DefaultCommand: test.defaultCmd, + Commands: []*Command{ + { + Name: "foobar", + Aliases: []string{"f"}, + Subcommands: []*Command{ + {Name: "jimbob", Aliases: []string{"j"}}, + {Name: "carly"}, + }, + }, + {Name: "batbaz", Aliases: []string{"b"}}, + }, + } + + err := app.Run([]string{"c", test.cmdName, test.subCmd}) + expect(t, err == nil, test.expected) + }) + } +} + +var defaultCommandFlagAppTests = []struct { + cmdName string + flag string + defaultCmd string + expected bool +}{ + {"foobar", "", "foobar", true}, + {"foobar", "-c derp", "foobar", true}, + {"batbaz", "", "foobar", true}, + {"b", "", "", true}, + {"f", "", "", true}, + {"", "", "foobar", true}, + {"", "", "", true}, + {"", "-j", "foobar", true}, + {"", "-j", "foobar", true}, + {"", "-c derp", "foobar", true}, + {"", "--carly=derp", "foobar", true}, + {"", "-j", "foobar", true}, + {"", "-j", "", true}, + {" ", "-j", "foobar", false}, + {"", "", "", true}, + {" ", "", "", false}, + {" ", "-j", "", false}, + {"bat", "", "batbaz", false}, + {"nothing", "", "batbaz", false}, + {"nothing", "", "", false}, + {"nothing", "--jimbob", "batbaz", false}, + {"nothing", "--carly", "", false}, +} + +func TestApp_RunDefaultCommandWithFlags(t *testing.T) { + for _, test := range defaultCommandFlagAppTests { + testTitle := fmt.Sprintf("command=%[1]s-flag=%[2]s-default=%[3]s", test.cmdName, test.flag, test.defaultCmd) + t.Run(testTitle, func(t *testing.T) { + app := &App{ + DefaultCommand: test.defaultCmd, + Flags: []Flag{ + &StringFlag{ + Name: "carly", + Aliases: []string{"c"}, + Required: false, + }, + &BoolFlag{ + Name: "jimbob", + Aliases: []string{"j"}, + Required: false, + Value: true, + }, + }, + Commands: []*Command{ + { + Name: "foobar", + Aliases: []string{"f"}, + }, + {Name: "batbaz", Aliases: []string{"b"}}, + }, + } + + appArgs := []string{"c"} + + if test.flag != "" { + flags := strings.Split(test.flag, " ") + if len(flags) > 1 { + appArgs = append(appArgs, flags...) + } + + flags = strings.Split(test.flag, "=") + if len(flags) > 1 { + appArgs = append(appArgs, flags...) + } + } + + appArgs = append(appArgs, test.cmdName) + + err := app.Run(appArgs) + expect(t, err == nil, test.expected) + }) + } +} + func TestApp_Setup_defaultsReader(t *testing.T) { app := &App{} app.Setup()