Repository: alecthomas/kong
Pull request #171 (2021-05-21 14:44:51)

Fix issue with negatable flag being negated twice
Fixes https://github.com/alecthomas/kong/issues/170

The logic of negating boolean flag value was added inside [Context.Apply()](https://github.com/alecthomas/kong/blob/master/context.go#L640) method, which is executed both for `Parse` and `Run` calls. This led to the negated flag value being parsed properly, but being negated again during the call to `Run`.

alecthomas: Very nice, thank you.

Patch:
diff --git a/context.go b/context.go
index d01abfd..d132b7d 100644
--- a/context.go
+++ b/context.go
@@ -635,12 +635,7 @@ func (c *Context) Apply() (string, error) {
 			panic("unsupported path ?!")
 		}
 		if value != nil {
-			v := c.getValue(value)
-			if value.Flag != nil && value.Flag.Negated {
-				v.SetBool(!v.Bool())
-			}
-
-			value.Apply(v)
+			value.Apply(c.getValue(value))
 		}
 	}
 
@@ -673,6 +668,11 @@ func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
 			}
 			return err
 		}
+		if flag.Negated {
+			value := c.getValue(flag.Value)
+			value.SetBool(!value.Bool())
+			flag.Value.Apply(value)
+		}
 		c.Path = append(c.Path, &Path{Flag: flag})
 		return nil
 	}


Test patch:
diff --git a/kong_test.go b/kong_test.go
index 2f333b7..6dec560 100644
--- a/kong_test.go
+++ b/kong_test.go
@@ -356,30 +356,66 @@ func TestTraceErrorPartiallySucceeds(t *testing.T) {
 	require.Equal(t, "one", ctx.Command())
 }
 
-func TestNegatedBooleanFlag(t *testing.T) {
-	var cli struct {
-		Cmd struct {
-			Flag bool `kong:"default='true',negatable"`
-		} `kong:"cmd"`
-	}
+type commandWithNegatableFlag struct {
+	Flag bool `kong:"default='true',negatable"`
+	ran  bool
+}
 
-	p := mustNew(t, &cli)
-	_, err := p.Parse([]string{"cmd", "--no-flag"})
-	require.NoError(t, err)
-	require.Equal(t, false, cli.Cmd.Flag)
+func (c *commandWithNegatableFlag) Run() error {
+	c.ran = true
+	return nil
 }
 
-func TestInvertedNegatedBooleanFlag(t *testing.T) {
-	var cli struct {
-		Cmd struct {
-			Flag bool `kong:"default='true',negatable"`
-		} `kong:"cmd"`
-	}
+func TestNegatableFlag(t *testing.T) {
+	tests := []struct {
+		name     string
+		args     []string
+		expected bool
+	}{
+		{
+			name:     "no flag",
+			args:     []string{"cmd"},
+			expected: true,
+		},
+		{
+			name:     "boolean flag",
+			args:     []string{"cmd", "--flag"},
+			expected: true,
+		},
+		{
+			name:     "inverted boolean flag",
+			args:     []string{"cmd", "--flag=false"},
+			expected: false,
+		},
+		{
+			name:     "negated boolean flag",
+			args:     []string{"cmd", "--no-flag"},
+			expected: false,
+		},
+		{
+			name:     "inverted negated boolean flag",
+			args:     []string{"cmd", "--no-flag=false"},
+			expected: true,
+		},
+	}
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			var cli struct {
+				Cmd commandWithNegatableFlag `kong:"cmd"`
+			}
+
+			p := mustNew(t, &cli)
+			kctx, err := p.Parse(tt.args)
+			require.NoError(t, err)
+			require.Equal(t, tt.expected, cli.Cmd.Flag)
 
-	p := mustNew(t, &cli)
-	_, err := p.Parse([]string{"cmd", "--no-flag=false"})
-	require.NoError(t, err)
-	require.Equal(t, true, cli.Cmd.Flag)
+			err = kctx.Run()
+			require.NoError(t, err)
+			require.Equal(t, tt.expected, cli.Cmd.Flag)
+			require.True(t, cli.Cmd.ran)
+		})
+	}
 }
 
 func TestExistingNoFlag(t *testing.T) {