From 2fdddc4f51b84086f40a70db3756571d10739de0 Mon Sep 17 00:00:00 2001 From: Joe Schmitt Date: Thu, 18 Feb 2021 10:55:55 -0500 Subject: [PATCH] Add `negatable` tag to set a bool to be negatable. If negatable, add `--[no-]` prefix to help. --- README.md | 1 + context.go | 2 +- help.go | 12 ++++++++++-- help_test.go | 3 +++ kong_test.go | 13 ++++++++++++- model.go | 2 +- tag.go | 6 ++++++ 7 files changed, 34 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f0b7a30..ba040c0 100644 --- a/README.md +++ b/README.md @@ -440,6 +440,7 @@ Tag | Description `required` | If present, flag/arg is required. `optional` | If present, flag/arg is optional. `hidden` | If present, command or flag is hidden. +`negatable` | If present on a `bool` field, supports prefixing a flag with `--no-` to set flag to `false` `format:"X"` | Format for parsing input, if supported. `sep:"X"` | Separator for sequences (defaults to ","). May be `none` to disable splitting. `mapsep:"X"` | Separator for maps (defaults to ";"). May be `none` to disable splitting. diff --git a/context.go b/context.go index 9ca3f35..2c9b791 100644 --- a/context.go +++ b/context.go @@ -652,7 +652,7 @@ func (c *Context) parseFlag(flags []*Flag, match string) (err error) { } // Found a matching flag. c.scan.Pop() - if flag.Value.IsBool() && match == neg { + if match == neg && flag.Tag.Negatable { flag.Negated = true } err := flag.Parse(c.scan, c.getValue(flag.Value)) diff --git a/help.go b/help.go index ab66ab0..fd2f758 100644 --- a/help.go +++ b/help.go @@ -472,9 +472,17 @@ func formatFlag(haveShort bool, flag *Flag) string { flagString += fmt.Sprintf("-%c, --%s", flag.Short, name) } else { if haveShort { - flagString += fmt.Sprintf(" --%s", name) + if isBool && flag.Tag.Negatable { + flagString = fmt.Sprintf(" --[no-]%s", name) + } else { + flagString += fmt.Sprintf(" --%s", name) + } } else { - flagString += fmt.Sprintf("--%s", name) + if isBool && flag.Tag.Negatable { + flagString = fmt.Sprintf("--[no-]%s", name) + } else { + flagString += fmt.Sprintf("--%s", name) + } } } if !isBool { diff --git a/help_test.go b/help_test.go index 6bf00b3..71266b4 100644 --- a/help_test.go +++ b/help_test.go @@ -29,6 +29,7 @@ func TestHelp(t *testing.T) { Slice []string `help:"A slice of strings." placeholder:"STR"` Map map[string]int `help:"A map of strings to ints."` Required bool `required help:"A required flag."` + Sort bool `negatable help:"Is sortable or not."` One struct { Flag string `help:"Nested flag."` @@ -75,6 +76,7 @@ Flags: --slice=STR,... A slice of strings. --map=KEY=VALUE;... A map of strings to ints. --required A required flag. + --[no-]sort Is sortable or not. Commands: one --required @@ -115,6 +117,7 @@ Flags: --slice=STR,... A slice of strings. --map=KEY=VALUE;... A map of strings to ints. --required A required flag. + --[no-]sort Is sortable or not. --flag=STRING Nested flag under two. --required-two diff --git a/kong_test.go b/kong_test.go index e9316fe..b7c598f 100644 --- a/kong_test.go +++ b/kong_test.go @@ -359,7 +359,7 @@ func TestTraceErrorPartiallySucceeds(t *testing.T) { func TestNegatedBooleanFlag(t *testing.T) { var cli struct { Cmd struct { - Flag bool `kong:"default='true'"` + Flag bool `kong:"default='true',negatable"` } `kong:"cmd"` } @@ -369,6 +369,17 @@ func TestNegatedBooleanFlag(t *testing.T) { require.Equal(t, false, cli.Cmd.Flag) } +func TestInvalidNegatedNonBool(t *testing.T) { + var cli struct { + Cmd struct { + Flag string `kong:"negatable"` + } `kong:"cmd"` + } + + _, err := kong.New(&cli) + require.Error(t, err) +} + type hookContext struct { cmd bool values []string diff --git a/model.go b/model.go index 8b90acc..4ce2456 100644 --- a/model.go +++ b/model.go @@ -227,7 +227,6 @@ type Value struct { Tag *Tag Target reflect.Value Required bool - Negated bool Set bool // Set to true when this value is set through some mechanism. Format string // Formatting directive, if applicable. Position int // Position (for positional arguments). @@ -370,6 +369,7 @@ type Flag struct { Env string Short rune Hidden bool + Negated bool } func (f *Flag) String() string { diff --git a/tag.go b/tag.go index b780000..e4d9af4 100644 --- a/tag.go +++ b/tag.go @@ -33,6 +33,7 @@ type Tag struct { Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix. Embed bool Aliases []string + Negatable bool // Storage for all tag keys for arbitrary lookups. items map[string][]string @@ -158,6 +159,11 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag { t.Xor = t.Get("xor") t.Prefix = t.Get("prefix") t.Embed = t.Has("embed") + negatable := t.Has("negatable") + if negatable && ft.Type.Name() != "bool" { + fail("negatable can only be set on booleans") + } + t.Negatable = negatable splitFn := func(r rune) bool { return r == ',' || r == ' ' }