Skip to content

Commit

Permalink
Merge pull request #1388 from jalavosus/feature/default-command
Browse files Browse the repository at this point in the history
feature: add DefaultCommand field to App
  • Loading branch information
meatballhat committed Jun 24, 2022
2 parents e2a844f + d8c93f8 commit d29120f
Show file tree
Hide file tree
Showing 2 changed files with 255 additions and 3 deletions.
89 changes: 86 additions & 3 deletions app.go
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"os"
"path/filepath"
"reflect"
"sort"
"time"
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
169 changes: 169 additions & 0 deletions app_test.go
Expand Up @@ -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()
Expand Down

0 comments on commit d29120f

Please sign in to comment.