From 32dec1ddaac72dad1d412745dbe8df15570e0c6b Mon Sep 17 00:00:00 2001 From: James Alavosus Date: Thu, 5 May 2022 23:50:22 -0400 Subject: [PATCH 1/5] feature: add DefaultCommand field to App See issue #1307 for context. --- app.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++--- app_test.go | 36 +++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/app.go b/app.go index 333bd57b05..a9ad131550 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,33 @@ 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 { + isFlagName := false + for _, flagName := range cCtx.FlagNames() { + if name == flagName { + isFlagName = true + break + } + } + if isFlagName { + 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 +594,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 diff --git a/app_test.go b/app_test.go index 3dd73abc16..8c2d3a92ce 100644 --- a/app_test.go +++ b/app_test.go @@ -469,6 +469,42 @@ 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) + }) + } + +} + func TestApp_Setup_defaultsReader(t *testing.T) { app := &App{} app.Setup() From 77feee843d87e3a4839fee5644e13ec85a264a27 Mon Sep 17 00:00:00 2001 From: James Alavosus Date: Fri, 6 May 2022 00:06:05 -0400 Subject: [PATCH 2/5] Implement slightly wonky setup for checking against ... subcommand names of a default command (should it be set) --- app.go | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/app.go b/app.go index a9ad131550..7e64c2d9f8 100644 --- a/app.go +++ b/app.go @@ -344,14 +344,26 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) { if a.validCommandName(name) { c = a.Command(name) } else { - isFlagName := false - for _, flagName := range cCtx.FlagNames() { - if name == flagName { - isFlagName = true - break + 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 { + + if isFlagName || (hasDefault && (defaultHasSubcommands && isDefaultSubcommand)) { argsWithDefault := a.argsWithDefaultCommand(args) if !reflect.DeepEqual(args, argsWithDefault) { c = a.Command(argsWithDefault.First()) @@ -661,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 +} From 1b3da50f163ae34fb7b1c06085a025d13b921aba Mon Sep 17 00:00:00 2001 From: James Alavosus Date: Fri, 6 May 2022 00:19:36 -0400 Subject: [PATCH 3/5] Add test cases for subcommands of default command --- app_test.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/app_test.go b/app_test.go index 8c2d3a92ce..3dd837f687 100644 --- a/app_test.go +++ b/app_test.go @@ -502,7 +502,60 @@ func TestApp_RunDefaultCommand(t *testing.T) { 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) + }) + } } func TestApp_Setup_defaultsReader(t *testing.T) { @@ -2333,4 +2386,4 @@ func TestSetupInitializesOnlyNilWriters(t *testing.T) { if a.Writer != os.Stdout { t.Errorf("expected a.Writer to be os.Stdout") } -} +} \ No newline at end of file From 1dfa9827f6b7cc866087a97bee6f5ca3efc8d309 Mon Sep 17 00:00:00 2001 From: James Alavosus Date: Tue, 21 Jun 2022 19:24:59 -0400 Subject: [PATCH 4/5] gofmt --- app_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app_test.go b/app_test.go index 3dd837f687..e814cbbd77 100644 --- a/app_test.go +++ b/app_test.go @@ -551,7 +551,7 @@ func TestApp_RunDefaultCommandWithSubCommand(t *testing.T) { {Name: "batbaz", Aliases: []string{"b"}}, }, } - + err := app.Run([]string{"c", test.cmdName, test.subCmd}) expect(t, err == nil, test.expected) }) @@ -2386,4 +2386,4 @@ func TestSetupInitializesOnlyNilWriters(t *testing.T) { if a.Writer != os.Stdout { t.Errorf("expected a.Writer to be os.Stdout") } -} \ No newline at end of file +} From d8c93f867b42d7de5991bb707e9c884ca1e04f78 Mon Sep 17 00:00:00 2001 From: James Alavosus Date: Tue, 21 Jun 2022 19:50:27 -0400 Subject: [PATCH 5/5] app_test.go: add tests for default command + flag --- app_test.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/app_test.go b/app_test.go index e814cbbd77..437af25911 100644 --- a/app_test.go +++ b/app_test.go @@ -558,6 +558,86 @@ func TestApp_RunDefaultCommandWithSubCommand(t *testing.T) { } } +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()