Skip to content

Commit 4ecb535

Browse files
authored
Make negatable flag name customisable (#439)
* fix: Check if negatable duplicates another flag Add a check for flags with the `negatable` option if the negative flag conflicts with another tag, such as: Flag bool `negatable:""` NoFlag bool The flag `--no-flag` is ambiguous in this scenario. * feat: Make negatable flag name customisable Allow a value on the `negatable` tag to specify a flag name to use for negation instead of using `--no-<flag-name>` as the flag. e.g. Approve bool `default:"true",negatable:"deny"` This example will allow `--deny` to set the `Approve` field to false.
1 parent 7d84b95 commit 4ecb535

File tree

8 files changed

+128
-49
lines changed

8 files changed

+128
-49
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,7 @@ Both can coexist with standard Tag parsing.
571571
| `optional:""` | If present, flag/arg is optional. |
572572
| `hidden:""` | If present, command or flag is hidden. |
573573
| `negatable:""` | If present on a `bool` field, supports prefixing a flag with `--no-` to invert the default value |
574+
| `negatable:"X"` | If present on a `bool` field, supports `--X` to invert the default value |
574575
| `format:"X"` | Format for parsing input, if supported. |
575576
| `sep:"X"` | Separator for sequences (defaults to ","). May be `none` to disable splitting. |
576577
| `mapsep:"X"` | Separator for maps (defaults to ";"). May be `none` to disable splitting. |

build.go

+7
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,13 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
315315
}
316316
seenFlags["-"+string(tag.Short)] = true
317317
}
318+
if tag.Negatable != "" {
319+
negFlag := negatableFlagName(value.Name, tag.Negatable)
320+
if seenFlags[negFlag] {
321+
return failField(v, ft, "duplicate negation flag %s", negFlag)
322+
}
323+
seenFlags[negFlag] = true
324+
}
318325
flag := &Flag{
319326
Value: value,
320327
Aliases: tag.Aliases,

context.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -710,13 +710,13 @@ func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
710710
candidates = append(candidates, alias)
711711
}
712712

713-
neg := "--no-" + flag.Name
714-
if !matched && !(match == neg && flag.Tag.Negatable) {
713+
neg := negatableFlagName(flag.Name, flag.Tag.Negatable)
714+
if !matched && match != neg {
715715
continue
716716
}
717717
// Found a matching flag.
718718
c.scan.Pop()
719-
if match == neg && flag.Tag.Negatable {
719+
if match == neg && flag.Tag.Negatable != "" {
720720
flag.Negated = true
721721
}
722722
err := flag.Parse(c.scan, c.getValue(flag.Value))

help.go

+14-19
Original file line numberDiff line numberDiff line change
@@ -491,27 +491,22 @@ func formatFlag(haveShort bool, flag *Flag) string {
491491
name := flag.Name
492492
isBool := flag.IsBool()
493493
isCounter := flag.IsCounter()
494+
495+
short := ""
494496
if flag.Short != 0 {
495-
if isBool && flag.Tag.Negatable {
496-
flagString += fmt.Sprintf("-%c, --[no-]%s", flag.Short, name)
497-
} else {
498-
flagString += fmt.Sprintf("-%c, --%s", flag.Short, name)
499-
}
500-
} else {
501-
if isBool && flag.Tag.Negatable {
502-
if haveShort {
503-
flagString = fmt.Sprintf(" --[no-]%s", name)
504-
} else {
505-
flagString = fmt.Sprintf("--[no-]%s", name)
506-
}
507-
} else {
508-
if haveShort {
509-
flagString += fmt.Sprintf(" --%s", name)
510-
} else {
511-
flagString += fmt.Sprintf("--%s", name)
512-
}
513-
}
497+
short = "-" + string(flag.Short) + ", "
498+
} else if haveShort {
499+
short = " "
500+
}
501+
502+
if isBool && flag.Tag.Negatable == negatableDefault {
503+
name = "[no-]" + name
504+
} else if isBool && flag.Tag.Negatable != "" {
505+
name += "/" + flag.Tag.Negatable
514506
}
507+
508+
flagString += fmt.Sprintf("%s--%s", short, name)
509+
515510
if !isBool && !isCounter {
516511
flagString += fmt.Sprintf("=%s", flag.FormatPlaceHolder())
517512
}

help_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ func TestHelp(t *testing.T) {
7171
Map map[string]int `help:"A map of strings to ints."`
7272
Required bool `required help:"A required flag."`
7373
Sort bool `negatable short:"s" help:"Is sortable or not."`
74+
Approve bool `negatable:"deny" help:"Approve or deny message."`
7475

7576
One struct {
7677
Flag string `help:"Nested flag."`
@@ -118,6 +119,7 @@ Flags:
118119
--map=KEY=VALUE;... A map of strings to ints.
119120
--required A required flag.
120121
-s, --[no-]sort Is sortable or not.
122+
--approve/deny Approve or deny message.
121123
122124
Commands:
123125
one --required [flags]
@@ -159,6 +161,7 @@ Flags:
159161
--map=KEY=VALUE;... A map of strings to ints.
160162
--required A required flag.
161163
-s, --[no-]sort Is sortable or not.
164+
--approve/deny Approve or deny message.
162165
163166
--flag=STRING Nested flag under two.
164167
--required-two

kong_test.go

+71-22
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,9 @@ func TestTraceErrorPartiallySucceeds(t *testing.T) {
357357
}
358358

359359
type commandWithNegatableFlag struct {
360-
Flag bool `kong:"default='true',negatable"`
361-
ran bool
360+
Flag bool `kong:"default='true',negatable"`
361+
Custom bool `kong:"default='true',negatable='standard'"`
362+
ran bool
362363
}
363364

364365
func (c *commandWithNegatableFlag) Run() error {
@@ -368,34 +369,64 @@ func (c *commandWithNegatableFlag) Run() error {
368369

369370
func TestNegatableFlag(t *testing.T) {
370371
tests := []struct {
371-
name string
372-
args []string
373-
expected bool
372+
name string
373+
args []string
374+
expectedFlag bool
375+
expectedCustom bool
374376
}{
375377
{
376-
name: "no flag",
377-
args: []string{"cmd"},
378-
expected: true,
378+
name: "no flag",
379+
args: []string{"cmd"},
380+
expectedFlag: true,
381+
expectedCustom: true,
379382
},
380383
{
381-
name: "boolean flag",
382-
args: []string{"cmd", "--flag"},
383-
expected: true,
384+
name: "boolean flag",
385+
args: []string{"cmd", "--flag"},
386+
expectedFlag: true,
387+
expectedCustom: true,
384388
},
385389
{
386-
name: "inverted boolean flag",
387-
args: []string{"cmd", "--flag=false"},
388-
expected: false,
390+
name: "custom boolean flag",
391+
args: []string{"cmd", "--custom"},
392+
expectedFlag: true,
393+
expectedCustom: true,
389394
},
390395
{
391-
name: "negated boolean flag",
392-
args: []string{"cmd", "--no-flag"},
393-
expected: false,
396+
name: "inverted boolean flag",
397+
args: []string{"cmd", "--flag=false"},
398+
expectedFlag: false,
399+
expectedCustom: true,
394400
},
395401
{
396-
name: "inverted negated boolean flag",
397-
args: []string{"cmd", "--no-flag=false"},
398-
expected: true,
402+
name: "custom inverted boolean flag",
403+
args: []string{"cmd", "--custom=false"},
404+
expectedFlag: true,
405+
expectedCustom: false,
406+
},
407+
{
408+
name: "negated boolean flag",
409+
args: []string{"cmd", "--no-flag"},
410+
expectedFlag: false,
411+
expectedCustom: true,
412+
},
413+
{
414+
name: "custom negated boolean flag",
415+
args: []string{"cmd", "--standard"},
416+
expectedFlag: true,
417+
expectedCustom: false,
418+
},
419+
{
420+
name: "inverted negated boolean flag",
421+
args: []string{"cmd", "--no-flag=false"},
422+
expectedFlag: true,
423+
expectedCustom: true,
424+
},
425+
{
426+
name: "inverted custom negated boolean flag",
427+
args: []string{"cmd", "--standard=false"},
428+
expectedFlag: true,
429+
expectedCustom: true,
399430
},
400431
}
401432
for _, tt := range tests {
@@ -408,16 +439,34 @@ func TestNegatableFlag(t *testing.T) {
408439
p := mustNew(t, &cli)
409440
kctx, err := p.Parse(tt.args)
410441
assert.NoError(t, err)
411-
assert.Equal(t, tt.expected, cli.Cmd.Flag)
442+
assert.Equal(t, tt.expectedFlag, cli.Cmd.Flag)
443+
assert.Equal(t, tt.expectedCustom, cli.Cmd.Custom)
412444

413445
err = kctx.Run()
414446
assert.NoError(t, err)
415-
assert.Equal(t, tt.expected, cli.Cmd.Flag)
447+
assert.Equal(t, tt.expectedFlag, cli.Cmd.Flag)
448+
assert.Equal(t, tt.expectedCustom, cli.Cmd.Custom)
416449
assert.True(t, cli.Cmd.ran)
417450
})
418451
}
419452
}
420453

454+
func TestDuplicateNegatableLong(t *testing.T) {
455+
cli2 := struct {
456+
NoFlag bool
457+
Flag bool `negatable:""` // negation duplicates NoFlag
458+
}{}
459+
_, err := kong.New(&cli2)
460+
assert.EqualError(t, err, "<anonymous struct>.Flag: duplicate negation flag --no-flag")
461+
462+
cli3 := struct {
463+
One bool
464+
Two bool `negatable:"one"` // negation duplicates Flag2
465+
}{}
466+
_, err = kong.New(&cli3)
467+
assert.EqualError(t, err, "<anonymous struct>.Two: duplicate negation flag --one")
468+
}
469+
421470
func TestExistingNoFlag(t *testing.T) {
422471
var cli struct {
423472
Cmd struct {

negatable.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package kong
2+
3+
// negatableDefault is a placeholder value for the Negatable tag to indicate
4+
// the negated flag is --no-<flag-name>. This is needed as at the time of
5+
// parsing a tag, the field's flag name is not yet known.
6+
const negatableDefault = "_"
7+
8+
// negatableFlagName returns the name of the flag for a negatable field, or
9+
// an empty string if the field is not negatable.
10+
func negatableFlagName(name, negation string) string {
11+
switch negation {
12+
case "":
13+
return ""
14+
case negatableDefault:
15+
return "--no-" + name
16+
default:
17+
return "--" + negation
18+
}
19+
}

tag.go

+10-5
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type Tag struct {
3838
EnvPrefix string
3939
Embed bool
4040
Aliases []string
41-
Negatable bool
41+
Negatable string
4242
Passthrough bool
4343

4444
// Storage for all tag keys for arbitrary lookups.
@@ -256,11 +256,16 @@ func hydrateTag(t *Tag, typ reflect.Type) error { //nolint: gocyclo
256256
t.Prefix = t.Get("prefix")
257257
t.EnvPrefix = t.Get("envprefix")
258258
t.Embed = t.Has("embed")
259-
negatable := t.Has("negatable")
260-
if negatable && !isBool && !isBoolPtr {
261-
return fmt.Errorf("negatable can only be set on booleans")
259+
if t.Has("negatable") {
260+
if !isBool && !isBoolPtr {
261+
return fmt.Errorf("negatable can only be set on booleans")
262+
}
263+
negatable := t.Get("negatable")
264+
if negatable == "" {
265+
negatable = negatableDefault // placeholder for default negation of --no-<flag>
266+
}
267+
t.Negatable = negatable
262268
}
263-
t.Negatable = negatable
264269
aliases := t.Get("aliases")
265270
if len(aliases) > 0 {
266271
t.Aliases = append(t.Aliases, strings.FieldsFunc(aliases, tagSplitFn)...)

0 commit comments

Comments
 (0)