From cd0f08182cdf57044ec64ce3c38f68ec07f67242 Mon Sep 17 00:00:00 2001 From: Sri Charan Date: Mon, 12 Oct 2026 13:11:19 +0530 Subject: [PATCH 1/2] fix: reject explicit empty boolean flag values --- command_parse.go | 4 ++-- command_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/command_parse.go b/command_parse.go index 8d4807a0b5..27362866ff 100644 --- a/command_parse.go +++ b/command_parse.go @@ -160,7 +160,7 @@ func (cmd *Command) parseFlags(args Args) (Args, error) { if f != nil { tracef("Trying flag type (fName=%[1]q) (type=%[2]T)", flagName, f) if fb, ok := f.(boolFlag); ok && fb.IsBoolFlag() { - if flagVal == "" { + if flagVal == "" && !valFromEqual { flagVal = "true" } tracef("parse Apply bool flag (fName=%[1]q) (fVal=%[2]q)", flagName, flagVal) @@ -238,7 +238,7 @@ func (cmd *Command) parseFlags(args Args) (Args, error) { // 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 != "" { + if index == lastIndex && valFromEqual { fv = flagVal } if err := cmd.set(string(c), sf, fv); err != nil { diff --git a/command_test.go b/command_test.go index a207233013..51cad76775 100644 --- a/command_test.go +++ b/command_test.go @@ -7346,6 +7346,56 @@ func TestEmptyPositionalArgsAfterFlag(t *testing.T) { } } +func TestCommand_BooleanEqualsValue(t *testing.T) { + tests := []struct { + name string + args []string + inverse bool + wantErr bool + want bool + }{ + {name: "empty long value", args: []string{"--enabled="}, wantErr: true}, + {name: "empty short value", args: []string{"-e="}, wantErr: true}, + {name: "empty grouped value", args: []string{"-ve="}, wantErr: true}, + {name: "empty inverse-capable positive value", args: []string{"--enabled="}, inverse: true, wantErr: true}, + {name: "bare long flag", args: []string{"--enabled"}, want: true}, + {name: "explicit false", args: []string{"--enabled=false"}}, + {name: "grouped false", args: []string{"-ve=false"}}, + {name: "bare grouped flags", args: []string{"-ve"}, want: true}, + {name: "bare inverse flag", args: []string{"--no-enabled"}, inverse: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var called bool + var enabled bool + var flag Flag = &BoolFlag{Name: "enabled", Aliases: []string{"e"}} + if tt.inverse { + flag = &BoolWithInverseFlag{Name: "enabled", Aliases: []string{"e"}, Value: true} + } + cmd := &Command{ + UseShortOptionHandling: true, + Writer: io.Discard, + ErrWriter: io.Discard, + Flags: []Flag{&BoolFlag{Name: "v"}, flag}, + Action: func(_ context.Context, cmd *Command) error { + called = true + enabled = cmd.Bool("enabled") + return nil + }, + } + err := cmd.Run(buildTestContext(t), append([]string{"app"}, tt.args...)) + if tt.wantErr { + require.ErrorContains(t, err, "parse error") + assert.False(t, called) + return + } + require.NoError(t, err) + assert.True(t, called) + assert.Equal(t, tt.want, enabled) + }) + } +} + func TestFlagEqualsEmptyValue(t *testing.T) { t.Run("--flag= sets empty string", func(t *testing.T) { var val string From e8f173b1433401ca6ca39bd57e2d965f59d7dd59 Mon Sep 17 00:00:00 2001 From: Eng Zer Jun Date: Fri, 16 Oct 2026 23:13:38 +0800 Subject: [PATCH 2/2] fix: set the opposite of the value given to an inverse bool flag Since 061f95c6 (#2074, v3.1.0), BoolWithInverseFlag ignores the value given to its inverse name, so "--no-env=false", "--no-env=" and "--no-env=foo" all set env to false. Before #2074, the inverse was a separate bool flag parsed by the flag package, which rejected an empty or invalid value. Parse the value and set its opposite, so "--no-env=V" is the same as "--env=!V", as with negatable flags in kong. "--no-env=false" sets env to true, and an empty or invalid value fails with a parse error, as "--env=" does since the previous commit. Fixes: 061f95c6 ("Fix tests") Assisted-by: claude:claude-opus-5-5 Signed-off-by: Eng Zer Jun --- flag_bool_with_inverse.go | 10 +++++++++- flag_bool_with_inverse_test.go | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/flag_bool_with_inverse.go b/flag_bool_with_inverse.go index e17b0b41f1..1daead3d65 100644 --- a/flag_bool_with_inverse.go +++ b/flag_bool_with_inverse.go @@ -2,8 +2,10 @@ package cli import ( "context" + "errors" "fmt" "slices" + "strconv" "strings" ) @@ -156,7 +158,13 @@ func (bif *BoolWithInverseFlag) Set(name, val string) error { if bif.pset { return bif.errBothSet() } - if err := bif.value.Set("false"); err != nil { + // the inverse name sets the opposite of its value, so + // --no-env=false is the same as --env=true + v, err := strconv.ParseBool(val) + if err != nil { + return errors.New("parse error") + } + if err := bif.value.Set(strconv.FormatBool(!v)); err != nil { return err } bif.nset = true diff --git a/flag_bool_with_inverse_test.go b/flag_bool_with_inverse_test.go index 8d262ca40e..e9f8fa1539 100644 --- a/flag_bool_with_inverse_test.go +++ b/flag_bool_with_inverse_test.go @@ -97,6 +97,24 @@ func TestBoolWithInverseBasic(t *testing.T) { args: []string{"--env", "--no-env"}, err: errBothEnvFlagsAreSet, }, + { + args: []string{"--no-env=true"}, + toBeSet: true, + value: false, + }, + { + args: []string{"--no-env=false"}, + toBeSet: true, + value: true, + }, + { + args: []string{"--no-env="}, + err: fmt.Errorf(`invalid value "" for flag -no-env: parse error`), + }, + { + args: []string{"--no-env=foo"}, + err: fmt.Errorf(`invalid value "foo" for flag -no-env: parse error`), + }, } err := runBoolWithInverseFlagTests(t, flagMethod, testCases)