From b11532df8610d72603186c5d7b907ce3a8f9b58c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20=C4=8Cekrli=C4=87?= Date: Fri, 1 Nov 2024 17:27:16 +0100 Subject: [PATCH 1/2] WIP: Allow setting and reseting bool values via standard arguments This commit enables the developer to allow setting boolean values to `false`. There already was a piece of code which indirectly allowed this, but default could never be true for bools due to #159. With this patch, a new setting `disable-type` is introduced on boolean options. If unset, the bool variables act as before. However, if set, the user can set (and reset) the bool varaible to either `true` or `false`. The setting has three possible values: `value`. `no` and `enable-disable`. When the setting is enabled, bool values will behave as follows: | *`disable-value`* | Syntax | | `value` | `--bool[=true|false]` | | `no` | `--[no-]bool` | | `enable-disable` | `--[enable|disable]-bool` | This allows for use cases such as: ```sh alias program='program --debug` program --no-debug ``` or ```sh ./program --enable-raytracing --disable-antialiasing ``` --- README.md | 12 ++++ error.go | 2 +- group.go | 28 ++++++++- help.go | 62 ++++++++++++++++--- help_test.go | 148 ++++++++++++++++++++++++++++---------------- ini_test.go | 48 ++++++++++++++ option.go | 21 +++++++ optstyle_other.go | 5 +- optstyle_windows.go | 2 +- parser.go | 71 ++++++++++++++++++--- parser_test.go | 120 +++++++++++++++++++++++++++++++++-- 11 files changed, 440 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 759eeb0..11bb4aa 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Supported features: * Options with short names (-v) * Options with long names (--verbose) * Options with and without arguments (bool v.s. other type) +* Long boolean options with --enable-* and --disable-* syntax +* Long boolean options with --no-* syntax * Options with optional arguments and default values * Multiple option groups each containing a set of options * Generate and print well-formatted help message @@ -58,6 +60,16 @@ var opts struct { // Example of a callback, called each time the option is found. Call func(string) `short:"c" description:"Call phone number"` + // Example of a boolean flag which can be disabled by using "--no-debug" + Debug bool `long:"debug" default:"true" disable-type:"no"` + + // Example of a boolean flag which can be disabled by using "--d=false" + Color bool `short:"c" disable-type:"value"` + + // Example of a boolean flag which can be enabled by using "--enable-fast-dns" + FastDns bool `long:"fast-dns" disable-type:"value"` + + // Example of a required flag Name string `short:"n" long:"name" description:"A name" required:"true"` diff --git a/error.go b/error.go index 73e07cf..073f6b8 100644 --- a/error.go +++ b/error.go @@ -28,7 +28,7 @@ const ( ErrHelp // ErrNoArgumentForBool indicates that an argument was given for a - // boolean flag (which don't not take any arguments). + // boolean flag (which do not take any arguments). ErrNoArgumentForBool // ErrRequired indicates that a required flag was not provided. diff --git a/group.go b/group.go index 181caab..9678aec 100644 --- a/group.go +++ b/group.go @@ -195,6 +195,26 @@ func (g *Group) eachGroup(f func(*Group)) { } } +func getDisableType(s string) (DisableBoolFlag, error) { + disableType := strings.ToLower(s) + switch disableType { + case string(DisableTypeNone): + return DisableTypeNone, nil + case string(DisableBoolNo): + return DisableBoolNo, nil + case string(DisableBoolEnabledDisabled): + return DisableBoolEnabledDisabled, nil + case string(DisableBoolValue): + return DisableBoolValue, nil + default: + return DisableTypeNone, newErrorf(ErrInvalidChoice, + "'disable-type' can only be '%v', '%v' or '%v', but got `%s'", + DisableBoolNo, DisableBoolEnabledDisabled, DisableBoolValue, + s) + } + +} + func isStringFalsy(s string) bool { return s == "" || s == "false" || s == "no" || s == "0" } @@ -280,6 +300,11 @@ func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, h valueName := mtag.Get("value-name") defaultMask := mtag.Get("default-mask") + disableBool, err := getDisableType(mtag.Get("disable-type")) + if err != nil { + return err + } + optional := !isStringFalsy(mtag.Get("optional")) required := !isStringFalsy(mtag.Get("required")) choices := mtag.GetMany("choice") @@ -292,6 +317,7 @@ func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, h Default: def, EnvDefaultKey: mtag.Get("env"), EnvDefaultDelim: mtag.Get("env-delim"), + DisableBool: disableBool, OptionalArgument: optional, OptionalValue: optionalValue, Required: required, @@ -307,7 +333,7 @@ func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, h tag: mtag, } - if option.isBool() && option.Default != nil { + if disableBool == DisableTypeNone && option.isBool() && option.Default != nil { return newErrorf(ErrInvalidTag, "boolean flag `%s' may not have default values, they always default to `false' and can only be turned on", option.shortAndLongName()) diff --git a/help.go b/help.go index 8fd3244..face180 100644 --- a/help.go +++ b/help.go @@ -27,6 +27,12 @@ const ( distanceBetweenOptionAndDescription = 2 ) +var testGOOS = "" + +func isWindows() bool { + return runtime.GOOS == "windows" || testGOOS == "windows" +} + func (a *alignmentInfo) descriptionStart() int { ret := a.maxLongLen + distanceBetweenOptionAndDescription @@ -94,10 +100,26 @@ func (p *Parser) getAlignmentInfo() alignmentInfo { ret.hasValueName = true } - l := info.LongNameWithNamespace() + info.ValueName + var l string + if info.isBool() { + switch info.DisableBool { + case DisableBoolValue: + l = info.LongNameWithNamespace() + info.ValueName + l += "[" + strings.Join([]string{"true", "false"}, "|") + "]" + case DisableBoolNo: + l = "[no-]" + info.LongNameWithNamespace() + info.ValueName + case DisableBoolEnabledDisabled: + l = "enable-" + info.LongNameWithNamespace() + info.ValueName + l += defaultLongOptDelimiter + "disable-" + info.LongNameWithNamespace() + info.ValueName + default: + l = info.LongNameWithNamespace() + info.ValueName + } + } else { + l = info.LongNameWithNamespace() + info.ValueName - if len(info.Choices) != 0 { - l += "[" + strings.Join(info.Choices, "|") + "]" + if len(info.Choices) != 0 { + l += "[" + strings.Join(info.Choices, "|") + "]" + } } ret.updateLen(l, c != p.Command) @@ -194,18 +216,36 @@ func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alig line.WriteString(" ") } - line.WriteString(defaultLongOptDelimiter) - line.WriteString(option.LongNameWithNamespace()) + if option.isBool() { + if option.DisableBool == DisableBoolNo { + line.WriteString(defaultLongOptDelimiter) + line.WriteString(fmt.Sprintf("[no-]%s", option.LongNameWithNamespace())) + } else if option.DisableBool == DisableBoolEnabledDisabled { + line.WriteString(defaultLongOptDelimiter) + line.WriteString(fmt.Sprintf("enable-%s", option.LongNameWithNamespace())) + line.WriteString(", ") + line.WriteString(defaultLongOptDelimiter) + line.WriteString(fmt.Sprintf("disable-%s", option.LongNameWithNamespace())) + } else { + line.WriteString(defaultLongOptDelimiter) + line.WriteString(option.LongNameWithNamespace()) + } + } else { + line.WriteString(defaultLongOptDelimiter) + line.WriteString(option.LongNameWithNamespace()) + } } - if option.canArgument() { + if option.canArgument() || (option.isBool() && option.DisableBool == DisableBoolValue) { line.WriteRune(defaultNameArgDelimiter) if len(option.ValueName) > 0 { line.WriteString(option.ValueName) } - if len(option.Choices) > 0 { + if option.isBool() && option.DisableBool == DisableBoolValue { + line.WriteString("[" + strings.Join([]string{"true", "false"}, "|") + "]") + } else if len(option.Choices) > 0 { line.WriteString("[" + strings.Join(option.Choices, "|") + "]") } } @@ -215,7 +255,11 @@ func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alig if option.Description != "" { dw := descstart - written - writer.WriteString(strings.Repeat(" ", dw)) + if dw > 0 { + writer.WriteString(strings.Repeat(" ", dw)) + } else { + writer.WriteString(" ") + } var def string @@ -230,7 +274,7 @@ func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alig var envDef string if option.EnvKeyWithNamespace() != "" { var envPrintable string - if runtime.GOOS == "windows" { + if isWindows() { envPrintable = "%" + option.EnvKeyWithNamespace() + "%" } else { envPrintable = "$" + option.EnvKeyWithNamespace() diff --git a/help_test.go b/help_test.go index 7f8b463..585c2f5 100644 --- a/help_test.go +++ b/help_test.go @@ -56,6 +56,14 @@ type helpOptions struct { } `group:"Subsubgroup" namespace:"sap"` } `group:"Subgroup" namespace:"sip"` + GroupBool struct { + SimpleBool bool `short:"b" description:"Bool which supports setting to true only"` + LongBool bool `long:"bool" description:"Bool which supports setting to true only"` + ValueBool bool `short:"V" long:"value" description:"Bool which supports setting value via optional argument" disable-type:"value"` + NoBool bool `short:"n" long:"no" description:"Bool which supports setting value via 'no-' prefix" disable-type:"no"` + EnDisBool bool `short:"e" long:"endis" description:"Bool which supports setting value via 'enable-' and 'disable-' prefix" disable-type:"enable-disable"` + } `group:"SimpleBool" namespace:"bool"` + Bommand struct { Hidden bool `long:"hidden" description:"A hidden option" hidden:"yes"` } `command:"bommand" description:"A command with only hidden options"` @@ -111,44 +119,61 @@ func TestHelp(t *testing.T) { TestHelp [OPTIONS] [filename] [num] hidden-in-help Application Options: - /v, /verbose Show verbose debug information - /c: Call phone number - /ptrslice: A slice of pointers to string + /v, /verbose Show verbose debug information + /c: Call phone number + /ptrslice: A slice of pointers to string /empty-description - /default: Test default value (default: - "Some\nvalue") - /default-array: Test default array value (default: - Some value, "Other\tvalue") - /default-map: Testdefault map value (default: - some:value, another:value) - /env-default1: Test env-default1 value (default: - Some value) [%ENV_DEFAULT%] - /env-default2: Test env-default2 value - [%ENV_DEFAULT%] - /opt-with-arg-name:something Option with named argument - /opt-with-choices:choice[dog|cat] Option with choices + /default: Test default value (default: + "Some\nvalue") + /default-array: Test default array value + (default: Some value, + "Other\tvalue") + /default-map: Testdefault map value + (default: some:value, + another:value) + /env-default1: Test env-default1 value + (default: Some value) + [%ENV_DEFAULT%] + /env-default2: Test env-default2 value + [%ENV_DEFAULT%] + /opt-with-arg-name:something Option with named argument + /opt-with-choices:choice[dog|cat] Option with choices Other Options: - /s: A slice of strings (default: some, - value) - /intmap: A map from string to int (default: - a:1) + /s: A slice of strings (default: some, + value) + /intmap: A map from string to int (default: + a:1) Subgroup: - /sip.opt: This is a subgroup option - /sip.not-hidden-inside-group: Not hidden inside group + /sip.opt: This is a subgroup option + /sip.not-hidden-inside-group: Not hidden inside group Subsubgroup: - /sip.sap.opt: This is a subsubgroup option + /sip.sap.opt: This is a subsubgroup option + +SimpleBool: + /b Bool which supports setting + to true only + /bool.bool Bool which supports setting + to true only + /V, /bool.value:[true|false] Bool which supports setting + value via optional argument + /n, /[no-]bool.no Bool which supports setting + value via 'no-' prefix + /e, /enable-bool.endis, /disable-bool.endis Bool which supports setting + value via 'enable-' and + 'disable-' prefix Help Options: - /? Show this help message - /h, /help Show this help message + /? Show this help message + /h, /help Show this help message Arguments: - filename: A filename with a long description - to trigger line wrapping - num: A number + filename: A filename with a long + description to trigger line + wrapping + num: A number Available commands: bommand A command with only hidden options @@ -160,43 +185,60 @@ Available commands: TestHelp [OPTIONS] [filename] [num] hidden-in-help Application Options: - -v, --verbose Show verbose debug information - -c= Call phone number - --ptrslice= A slice of pointers to string + -v, --verbose Show verbose debug information + -c= Call phone number + --ptrslice= A slice of pointers to string --empty-description - --default= Test default value (default: - "Some\nvalue") - --default-array= Test default array value (default: - Some value, "Other\tvalue") - --default-map= Testdefault map value (default: - some:value, another:value) - --env-default1= Test env-default1 value (default: - Some value) [$ENV_DEFAULT] - --env-default2= Test env-default2 value - [$ENV_DEFAULT] - --opt-with-arg-name=something Option with named argument - --opt-with-choices=choice[dog|cat] Option with choices + --default= Test default value (default: + "Some\nvalue") + --default-array= Test default array value + (default: Some value, + "Other\tvalue") + --default-map= Testdefault map value + (default: some:value, + another:value) + --env-default1= Test env-default1 value + (default: Some value) + [$ENV_DEFAULT] + --env-default2= Test env-default2 value + [$ENV_DEFAULT] + --opt-with-arg-name=something Option with named argument + --opt-with-choices=choice[dog|cat] Option with choices Other Options: - -s= A slice of strings (default: some, - value) - --intmap= A map from string to int (default: - a:1) + -s= A slice of strings (default: + some, value) + --intmap= A map from string to int + (default: a:1) Subgroup: - --sip.opt= This is a subgroup option - --sip.not-hidden-inside-group= Not hidden inside group + --sip.opt= This is a subgroup option + --sip.not-hidden-inside-group= Not hidden inside group Subsubgroup: - --sip.sap.opt= This is a subsubgroup option + --sip.sap.opt= This is a subsubgroup option + +SimpleBool: + -b Bool which supports setting + to true only + --bool.bool Bool which supports setting + to true only + -V, --bool.value=[true|false] Bool which supports setting + value via optional argument + -n, --[no-]bool.no Bool which supports setting + value via 'no-' prefix + -e, --enable-bool.endis, --disable-bool.endis Bool which supports setting + value via 'enable-' and + 'disable-' prefix Help Options: - -h, --help Show this help message + -h, --help Show this help message Arguments: - filename: A filename with a long description - to trigger line wrapping - num: A number + filename: A filename with a long + description to trigger line + wrapping + num: A number Available commands: bommand A command with only hidden options diff --git a/ini_test.go b/ini_test.go index 72c890c..0205a73 100644 --- a/ini_test.go +++ b/ini_test.go @@ -100,6 +100,22 @@ NotHiddenInsideGroup = ; This is a subsubgroup option Opt = +[SimpleBool] +; Bool which supports setting to true only +; SimpleBool = false + +; Bool which supports setting to true only +; LongBool = false + +; Bool which supports setting value via optional argument +; ValueBool = false + +; Bool which supports setting value via 'no-' prefix +; NoBool = false + +; Bool which supports setting value via 'enable-' and 'disable-' prefix +; EnDisBool = false + [command] ; Use for extra verbosity ; ExtraVerbose = @@ -171,6 +187,22 @@ EnvDefault2 = env-def ; This is a subsubgroup option ; Opt = +[SimpleBool] +; Bool which supports setting to true only +; SimpleBool = false + +; Bool which supports setting to true only +; LongBool = false + +; Bool which supports setting value via optional argument +; ValueBool = false + +; Bool which supports setting value via 'no-' prefix +; NoBool = false + +; Bool which supports setting value via 'enable-' and 'disable-' prefix +; EnDisBool = false + [command] ; Use for extra verbosity ; ExtraVerbose = @@ -240,6 +272,22 @@ EnvDefault2 = env-def ; This is a subsubgroup option ; Opt = +[SimpleBool] +; Bool which supports setting to true only +; SimpleBool = false + +; Bool which supports setting to true only +; LongBool = false + +; Bool which supports setting value via optional argument +; ValueBool = false + +; Bool which supports setting value via 'no-' prefix +; NoBool = false + +; Bool which supports setting value via 'enable-' and 'disable-' prefix +; EnDisBool = false + [command] ; Use for extra verbosity ; ExtraVerbose = diff --git a/option.go b/option.go index 257996a..1a178a3 100644 --- a/option.go +++ b/option.go @@ -9,6 +9,13 @@ import ( "unicode/utf8" ) +type DisableBoolFlag string + +const DisableTypeNone DisableBoolFlag = "" +const DisableBoolNo DisableBoolFlag = "no" +const DisableBoolEnabledDisabled DisableBoolFlag = "enable-disable" +const DisableBoolValue DisableBoolFlag = "value" + // Option flag information. Contains a description of the option, short and // long name as well as a default value and whether an argument for this // flag is optional. @@ -36,6 +43,20 @@ type Option struct { // The optional delimiter string for EnvDefaultKey values. EnvDefaultDelim string + // Available for boolean values only and for long versions only. It specifies + // how boolean flag can be disabled. Two common approaches exist and this option + // supports both: + // - with a "--no-" prefix. E.g. to disable `--debug`, use `--no-debug` + // - with either "enable" or "disable", mimicking how a lot of Unix tools work. + // E.g. to enable "foo", long option would be `--enable-foo` and to disable it, + // "--disable-foo". + // + // To turn on the first option, specify "no". + // To turn on the second, specify "enable-disable". + // + // Empty value assumes no option is specified and this feature is disabled. + DisableBool DisableBoolFlag + // If true, specifies that the argument to an option flag is optional. // When no argument to the flag is specified on the command line, the // value of OptionalValue will be set in the field this option represents. diff --git a/optstyle_other.go b/optstyle_other.go index f84b697..5ac9ac0 100644 --- a/optstyle_other.go +++ b/optstyle_other.go @@ -61,7 +61,10 @@ func (c *Command) addHelpGroup(showHelp func() error) *Group { } help.ShowHelp = showHelp - ret, _ := c.AddGroup("Help Options", "", &help) + ret, err := c.AddGroup("Help Options", "", &help) + if err != nil { + panic(err) + } ret.isBuiltinHelp = true return ret diff --git a/optstyle_windows.go b/optstyle_windows.go index e802904..c554442 100644 --- a/optstyle_windows.go +++ b/optstyle_windows.go @@ -8,7 +8,7 @@ import ( ) // Windows uses a front slash for both short and long options. Also it uses -// a colon for name/argument delimter. +// a colon for name/argument delimiter. const ( defaultShortOptDelimiter = '/' defaultLongOptDelimiter = "/" diff --git a/parser.go b/parser.go index 939dd7b..b17b422 100644 --- a/parser.go +++ b/parser.go @@ -115,6 +115,7 @@ const ( // AllowBoolValues allows a user to assign true/false to a boolean value // rather than raising an error stating it cannot have an argument. + // Deprecated: Set "disable-type" on specific option. AllowBoolValues // Default is a convenient default set of options which should cover @@ -523,10 +524,13 @@ func (p *parseState) estimateCommand() error { return newError(errtype, msg) } -func (p *Parser) parseOption(s *parseState, name string, option *Option, canarg bool, argument *string) (err error) { +func (p *Parser) parseOption(s *parseState, name string, option *Option, canarg bool, argument *string, forcedArgument bool) (err error) { if !option.canArgument() { - if argument != nil && (p.Options&AllowBoolValues) == None { - return newErrorf(ErrNoArgumentForBool, "bool flag `%s' cannot have an argument", option) + if argument != nil { + accepted := forcedArgument || ((p.Options & AllowBoolValues) != None) || option.DisableBool == DisableBoolValue + if !accepted { + return newErrorf(ErrNoArgumentForBool, "bool flag `%s' cannot have an argument", option) + } } err = option.Set(argument) } else if argument != nil || (canarg && !s.eof()) { @@ -599,12 +603,63 @@ func (p *Parser) expectedType(option *Option) string { } func (p *Parser) parseLong(s *parseState, name string, argument *string) error { - if option := s.lookup.longNames[name]; option != nil { + var option *Option + var forcedArgument bool + truth := "true" + lie := "false" + + if opt, ok := s.lookup.longNames[name]; ok { + option = opt + if opt.DisableBool == DisableBoolEnabledDisabled { + option = nil + } + } else if strings.HasPrefix(name, "no-") { + n := name[3:] + if opt, ok := s.lookup.longNames[n]; ok { + if opt.DisableBool == DisableBoolNo { + option = opt + } + } + if argument != nil { + return newErrorf(ErrNoArgumentForBool, "bool flag `--%s' cannot have an argument", name) + } else { + argument = &lie + forcedArgument = true + } + } else if strings.HasPrefix(name, "enable-") { + n := name[7:] + if opt, ok := s.lookup.longNames[n]; ok { + if opt.DisableBool == DisableBoolEnabledDisabled { + option = opt + } + } + if argument != nil { + return newErrorf(ErrNoArgumentForBool, "bool flag `--%s' cannot have an argument", name) + } else { + argument = &truth + forcedArgument = true + } + } else if strings.HasPrefix(name, "disable-") { + n := name[8:] + if opt, ok := s.lookup.longNames[n]; ok { + if opt.DisableBool == DisableBoolEnabledDisabled { + option = opt + } + } + if argument != nil { + return newErrorf(ErrNoArgumentForBool, "bool flag `--%s' cannot have an argument", name) + } else { + argument = &lie + forcedArgument = true + } + } + + if option != nil { // Only long options that are required can consume an argument // from the argument list canarg := !option.OptionalArgument - return p.parseOption(s, name, option, canarg, argument) + return p.parseOption(s, name, option, canarg, argument, forcedArgument) } return newErrorf(ErrUnknownFlag, "unknown flag `%s'", name) @@ -637,17 +692,17 @@ func (p *Parser) parseShort(s *parseState, optname string, argument *string) err if option := s.lookup.shortNames[shortname]; option != nil { // Only the last short argument can consume an argument from - // the arguments list, and only if it's non optional + // the arguments list, and only if it's non-optional canarg := (i+utf8.RuneLen(c) == len(optname)) && !option.OptionalArgument - if err := p.parseOption(s, shortname, option, canarg, argument); err != nil { + if err := p.parseOption(s, shortname, option, canarg, argument, false); err != nil { return err } } else { return newErrorf(ErrUnknownFlag, "unknown flag `%s'", shortname) } - // Only the first option can have a concatted argument, so just + // Only the first option can have a concat-ed argument, so just // clear argument here argument = nil } diff --git a/parser_test.go b/parser_test.go index e675cfd..5fb19e1 100644 --- a/parser_test.go +++ b/parser_test.go @@ -168,6 +168,89 @@ func TestNoDefaultsForBools(t *testing.T) { } } +func TestBoolNoArguments(t *testing.T) { + var opts struct { + Bool bool `long:"bool"` + } + _, err := ParseArgs(&opts, []string{"--bool=true"}) + assertError(t, err, ErrNoArgumentForBool, "bool flag `--bool' cannot have an argument") +} + +func TestBoolArguments1(t *testing.T) { + var opts struct { + Bool bool `long:"bool" disable-type:"value"` + } + + _, err := ParseArgs(&opts, []string{"--bool=true", "--bool=false"}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if opts.Bool { + t.Fatalf("Bool flag should have not been set") + } + + // Reset + opts.Bool = true + + _, err = ParseArgs(&opts, []string{"--bool=false", "--bool=true"}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if !opts.Bool { + t.Fatalf("Bool flag should have been set") + } +} + +func TestBoolLongNoPrefix(t *testing.T) { + var opts struct { + Bool bool `short:"b" long:"bool" default:"true" disable-type:"no"` + } + + _, err := ParseArgs(&opts, []string{"--bool", "--no-bool"}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if opts.Bool { + t.Fatalf("Bool flag should have not been set") + } + + // Reset + opts.Bool = true + + _, err = ParseArgs(&opts, []string{"--no-bool", "--bool"}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if !opts.Bool { + t.Fatalf("Bool flag should have been set") + } +} + +func TestBoolEnableDisablePrefix(t *testing.T) { + var opts struct { + Bool bool `long:"bool" disable-type:"enable-disable"` + } + + _, err := ParseArgs(&opts, []string{"--enable-bool", "--disable-bool"}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if opts.Bool { + t.Fatalf("Bool flag should have not been set") + } + + // Reset + opts.Bool = true + + _, err = ParseArgs(&opts, []string{"--disable-bool", "--disable-bool", "--enable-bool"}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if !opts.Bool { + t.Fatalf("Bool flag should have been set") + } +} + func TestUnquoting(t *testing.T) { var tests = []struct { arg string @@ -711,6 +794,11 @@ func TestAllowBoolValues(t *testing.T) { expected bool expectedNonOptArgs []string }{ + { + msg: "bad value", + args: []string{"-v=badvalue"}, + expectedErr: `parsing "badvalue": invalid syntax`, + }, { msg: "no value", args: []string{"-v"}, @@ -726,11 +814,6 @@ func TestAllowBoolValues(t *testing.T) { args: []string{"-v=false"}, expected: false, }, - { - msg: "bad value", - args: []string{"-v=badvalue"}, - expectedErr: `parsing "badvalue": invalid syntax`, - }, { // this test is to ensure flag values can only be specified as --flag=value and not "--flag value". // if "--flag value" was supported it's not clear if value should be a non-optional argument @@ -742,6 +825,32 @@ func TestAllowBoolValues(t *testing.T) { }, } + for _, test := range tests { + var opts = struct { + Value bool `short:"v" disable-type:"value"` + }{} + parser := NewParser(&opts, Default) + nonOptArgs, err := parser.ParseArgs(test.args) + + if test.expectedErr == "" { + if err != nil { + t.Fatalf("%s:\nUnexpected parse error: %s", test.msg, err) + } + if opts.Value != test.expected { + t.Errorf("%s:\nExpected %v; got %v", test.msg, test.expected, opts.Value) + } + if len(test.expectedNonOptArgs) != len(nonOptArgs) && !reflect.DeepEqual(test.expectedNonOptArgs, nonOptArgs) { + t.Errorf("%s:\nUnexpected non-argument options\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.expectedNonOptArgs, nonOptArgs) + } + } else { + if err == nil { + t.Errorf("%s:\nExpected error containing substring %q", test.msg, test.expectedErr) + } else if !strings.Contains(err.Error(), test.expectedErr) { + t.Errorf("%s:\nExpected error %q to contain substring %q", test.msg, err, test.expectedErr) + } + } + } + for _, test := range tests { var opts = struct { Value bool `short:"v"` @@ -767,4 +876,5 @@ func TestAllowBoolValues(t *testing.T) { } } } + } From ffd29bff3036b15c34cdbf1c1f4c205da468f6e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20=C4=8Cekrli=C4=87?= Date: Fri, 1 Nov 2024 17:40:21 +0100 Subject: [PATCH 2/2] Replace spaces with tabs in `README.md` to align with other fields. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 11bb4aa..6557579 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,8 @@ var opts struct { // Example of a boolean flag which can be disabled by using "--no-debug" Debug bool `long:"debug" default:"true" disable-type:"no"` - // Example of a boolean flag which can be disabled by using "--d=false" - Color bool `short:"c" disable-type:"value"` + // Example of a boolean flag which can be disabled by using "--d=false" + Color bool `short:"c" disable-type:"value"` // Example of a boolean flag which can be enabled by using "--enable-fast-dns" FastDns bool `long:"fast-dns" disable-type:"value"`