diff --git a/command_parse.go b/command_parse.go index 2b9a481df2..939ad90b85 100644 --- a/command_parse.go +++ b/command_parse.go @@ -199,6 +199,12 @@ func (cmd *Command) parseFlags(args Args) (Args, error) { posArgs = append(posArgs, rargs...) return &stringSliceArgs{posArgs}, nil } + // When DefaultCommand is set, pass unknown flags through as positional args + // so the default command can handle them (fixes #2249) + if cmd.DefaultCommand != "" { + posArgs = append(posArgs, rargs...) + return &stringSliceArgs{posArgs}, nil + } return &stringSliceArgs{posArgs}, fmt.Errorf("%s%s", providedButNotDefinedErrMsg, flagName) } @@ -206,6 +212,10 @@ func (cmd *Command) parseFlags(args Args) (Args, error) { for index, c := range flagName { tracef("processing flag (fName=%[1]q)", string(c)) if sf := cmd.lookupFlag(string(c)); sf == nil { + if index == 0 && cmd.DefaultCommand != "" { + posArgs = append(posArgs, rargs...) + return &stringSliceArgs{posArgs}, nil + } return &stringSliceArgs{posArgs}, fmt.Errorf("%s%s", providedButNotDefinedErrMsg, flagName) } else if fb, ok := sf.(boolFlag); ok && fb.IsBoolFlag() { fv := flagVal diff --git a/command_test.go b/command_test.go index d3933c96a5..b018bd8801 100644 --- a/command_test.go +++ b/command_test.go @@ -5771,3 +5771,108 @@ func TestFlagEqualsEmptyValue(t *testing.T) { assert.Equal(t, []string{"positional"}, args) }) } + +func TestDefaultCommandWithSubcommandFlags(t *testing.T) { + // Regression test for https://github.com/urfave/cli/issues/2249 + // When DefaultCommand is set, flags defined on the default subcommand + // should work even when the subcommand name is omitted. + actionExecuted := false + cmd := &Command{ + DefaultCommand: "run1", + Commands: []*Command{ + { + Name: "run1", + Usage: "run the main application", + Action: func(ctx context.Context, cmd *Command) error { + actionExecuted = true + return nil + }, + Flags: []Flag{ + &StringFlag{ + Name: "foo", + Required: true, + }, + }, + }, + { + Name: "run2", + Action: func(ctx context.Context, cmd *Command) error { + return nil + }, + Flags: []Flag{ + &StringFlag{ + Name: "value", + Required: true, + }, + }, + }, + }, + } + + // Using flag without subcommand name should route to default command + err := cmd.Run(buildTestContext(t), []string{"c", "--foo", "bar"}) + assert.NoError(t, err) + assert.True(t, actionExecuted, "expected run1 action to be executed") +} + +func TestDefaultCommandWithShortFlag(t *testing.T) { + // Covers the short-flag splitting path in parseFlags when DefaultCommand is set + actionRun := false + cmd := &Command{ + DefaultCommand: "run", + Commands: []*Command{ + { + Name: "run", + Usage: "run the app", + Action: func(ctx context.Context, cmd *Command) error { + actionRun = true + if cmd.String("foo") != "baz" { + return fmt.Errorf("expected foo=baz, got %s", cmd.String("foo")) + } + return nil + }, + Flags: []Flag{ + &StringFlag{ + Name: "foo", + Aliases: []string{"f"}, + }, + }, + }, + }, + } + + err := cmd.Run(buildTestContext(t), []string{"c", "-f", "baz"}) + assert.NoError(t, err) + assert.True(t, actionRun, "expected run action to be executed") +} + +func TestDefaultCommandWithShortFlagHandling(t *testing.T) { + // Covers the shortOptionHandling for-loop path when DefaultCommand is set + actionRun := false + cmd := &Command{ + UseShortOptionHandling: true, + DefaultCommand: "run", + Commands: []*Command{ + { + Name: "run", + Usage: "run the app", + Action: func(ctx context.Context, cmd *Command) error { + actionRun = true + if cmd.String("foo") != "baz" { + return fmt.Errorf("expected foo=baz, got %s", cmd.String("foo")) + } + return nil + }, + Flags: []Flag{ + &StringFlag{ + Name: "foo", + Aliases: []string{"f"}, + }, + }, + }, + }, + } + err := cmd.Run(buildTestContext(t), []string{"c", "-f", "baz"}) + assert.NoError(t, err) + assert.True(t, actionRun, "expected run action to be executed") +}