Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Allow setting and reseting bool values via standard arguments #418

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"`

Expand Down
2 changes: 1 addition & 1 deletion error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
28 changes: 27 additions & 1 deletion group.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down Expand Up @@ -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")
Expand All @@ -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,
Expand All @@ -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())
Expand Down
62 changes: 53 additions & 9 deletions help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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, "|") + "]")
}
}
Expand All @@ -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

Expand All @@ -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()
Expand Down
148 changes: 95 additions & 53 deletions help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -111,44 +119,61 @@ func TestHelp(t *testing.T) {
TestHelp [OPTIONS] [filename] [num] hidden-in-help <bommand | command | parent>

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
Expand All @@ -160,43 +185,60 @@ Available commands:
TestHelp [OPTIONS] [filename] [num] hidden-in-help <bommand | command | parent>

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
Expand Down
Loading