diff --git a/command_parse.go b/command_parse.go index 9265d8279d..2d516f7ca2 100644 --- a/command_parse.go +++ b/command_parse.go @@ -236,19 +236,20 @@ func (cmd *Command) parseFlags(args Args) (Args, error) { // the check above has already found every flag of the group sf := cmd.lookupAppliedFlag(string(c)) if fb, ok := sf.(boolFlag); ok && fb.IsBoolFlag() { - fv := flagVal - if index == lastIndex && flagVal == "" { - fv = "true" - } - if fv == "" { - fv = "true" + // the value after "=" belongs to the last flag of the group, + // so a bool flag before it is just set + fv := "true" + if index == lastIndex && flagVal != "" { + fv = flagVal } if err := cmd.set(string(c), sf, fv); err != nil { tracef("processing flag.2 (fName=%[1]q)", string(c)) return &stringSliceArgs{posArgs}, err } } else if index == lastIndex { // last flag can take an arg - if flagVal == "" { + // as with a single flag, an explicit "=" with nothing after + // it is an empty value, not a request to read the next arg + if flagVal == "" && !valFromEqual { if len(rargs) == 1 { return &stringSliceArgs{posArgs}, fmt.Errorf("%s%s", argumentNotProvidedErrMsg, string(c)) } diff --git a/command_test.go b/command_test.go index 73599dc643..18cf901d0a 100644 --- a/command_test.go +++ b/command_test.go @@ -1534,6 +1534,41 @@ func TestCommand_UseShortOptionHandlingMultiByteLastFlag(t *testing.T) { assert.EqualError(t, err, "flag needs an argument: "+name) } +func TestCommand_UseShortOptionHandlingValueForLastFlag(t *testing.T) { + var verbose, extra bool + var value string + var args []string + newCmd := func() *Command { + return &Command{ + Name: "app", + UseShortOptionHandling: true, + Flags: []Flag{ + &BoolFlag{Name: "v"}, + &BoolFlag{Name: "x"}, + &StringFlag{Name: "n"}, + }, + Action: func(_ context.Context, cmd *Command) error { + verbose = cmd.Bool("v") + extra = cmd.Bool("x") + value = cmd.String("n") + args = cmd.Args().Slice() + return nil + }, + Writer: io.Discard, + ErrWriter: io.Discard, + } + } + + require.NoError(t, newCmd().Run(buildTestContext(t), []string{"app", "-vn=value", "operand"})) + assert.True(t, verbose) + assert.Equal(t, "value", value) + assert.Equal(t, []string{"operand"}, args) + + require.NoError(t, newCmd().Run(buildTestContext(t), []string{"app", "-vx=false"})) + assert.True(t, verbose) + assert.False(t, extra) +} + func TestCommand_UseShortOptionHandlingCommand(t *testing.T) { var ( one, two bool @@ -6948,6 +6983,48 @@ func TestFlagEqualsEmptyValue(t *testing.T) { assert.Equal(t, "", val) assert.Equal(t, []string{"positional"}, args) }) + + t.Run("-vn= in a short option group sets empty string", func(t *testing.T) { + var val string + var verbose bool + var args []string + + cmd := &Command{ + UseShortOptionHandling: true, + Flags: []Flag{ + &BoolFlag{Name: "v", Destination: &verbose}, + &StringFlag{Name: "n", Destination: &val, Value: "default"}, + }, + Action: func(_ context.Context, cmd *Command) error { + args = cmd.Args().Slice() + return nil + }, + } + + err := cmd.Run(buildTestContext(t), []string{"app", "-vn=", "positional"}) + assert.NoError(t, err) + assert.True(t, verbose) + assert.Equal(t, "", val) + assert.Equal(t, []string{"positional"}, args) + }) + + t.Run("-vn= at the end of a short option group sets empty string", func(t *testing.T) { + var val string + var verbose bool + + cmd := &Command{ + UseShortOptionHandling: true, + Flags: []Flag{ + &BoolFlag{Name: "v", Destination: &verbose}, + &StringFlag{Name: "n", Destination: &val, Value: "default"}, + }, + } + + err := cmd.Run(buildTestContext(t), []string{"app", "-vn="}) + assert.NoError(t, err) + assert.True(t, verbose) + assert.Equal(t, "", val) + }) } // TestCommand_NoDefaultCmdArgMatchingFlag tests the argument set