From 5f175cd80c505392146d158173f12ecbcde8af6f Mon Sep 17 00:00:00 2001 From: Markus Zimmermann Date: Sun, 28 Dec 2014 16:32:29 +0100 Subject: [PATCH 01/38] test IniParser.WriteFile with no-ini option --- ini_test.go | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/ini_test.go b/ini_test.go index 8c3176c..5d19247 100644 --- a/ini_test.go +++ b/ini_test.go @@ -569,7 +569,8 @@ other = subgroup func TestIniNoIni(t *testing.T) { var opts struct { - Value string `short:"v" long:"value" no-ini:"yes"` + NoValue string `short:"n" long:"novalue" no-ini:"yes"` + Value string `short:"v" long:"value"` } p := NewNamedParser("TestIni", Default) @@ -577,8 +578,10 @@ func TestIniNoIni(t *testing.T) { inip := NewIniParser(p) + // read INI inic := `[Application Options] -value = some value +novalue = some value +value = some other value ` b := strings.NewReader(inic) @@ -594,9 +597,33 @@ value = some value t.Errorf("Expected opts.Add.Name to be %d, but got %d", v, iniError.LineNumber) } - if v := "unknown option: value"; iniError.Message != v { + if v := "unknown option: novalue"; iniError.Message != v { t.Errorf("Expected opts.Add.Name to be %s, but got %s", v, iniError.Message) } + + // write INI + opts.NoValue = "some value" + opts.Value = "some other value" + + file, err := ioutil.TempFile("", "") + if err != nil { + t.Fatalf("Cannot create temporary file: %s", err) + } + defer os.Remove(file.Name()) + + err = inip.WriteFile(file.Name(), IniIncludeDefaults) + if err != nil { + t.Fatalf("Could not write ini file: %s", err) + } + + found, err := ioutil.ReadFile(file.Name()) + if err != nil { + t.Fatalf("Could not read written ini file: %s", err) + } + + expected := "[Application Options]\nValue = some other value\n\n" + + assertDiff(t, string(found), expected, "ini content") } func TestIniParse(t *testing.T) { From eee67de014e87a62b189618220f818fc5fe296a9 Mon Sep 17 00:00:00 2001 From: Gabriel Gilder Date: Sat, 24 Jan 2015 23:38:05 -0800 Subject: [PATCH 02/38] Fix docs typo --- flags.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flags.go b/flags.go index e3e72a3..9d7d571 100644 --- a/flags.go +++ b/flags.go @@ -102,7 +102,7 @@ The following is a list of tags for struct fields supported by go-flags: env-delim: the 'env' default value from environment is split into multiple values with the given delimiter string, use with slices and maps (optional) - value-name: the name of the argument value (to be shown in the help, + value-name: the name of the argument value (to be shown in the help) (optional) base: a base (radix) used to convert strings to integer values, the From 1c0255aa6bfdae2aad6596fc418a4688d08a08ed Mon Sep 17 00:00:00 2001 From: Gabriel Gilder Date: Sun, 25 Jan 2015 00:13:37 -0800 Subject: [PATCH 03/38] Use positional-arg-name tag --- command_private.go | 2 +- flags.go | 4 ++++ help_test.go | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/command_private.go b/command_private.go index 1727a30..5d30a8a 100644 --- a/command_private.go +++ b/command_private.go @@ -43,7 +43,7 @@ func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler { return true, err } - name := m.Get("name") + name := m.Get("positional-arg-name") if len(name) == 0 { name = field.Name diff --git a/flags.go b/flags.go index 9d7d571..37d331d 100644 --- a/flags.go +++ b/flags.go @@ -134,6 +134,10 @@ The following is a list of tags for struct fields supported by go-flags: Positional arguments are optional by default, unless the "required" tag is specified together with the "positional-args" tag (optional) + positional-arg-name: used on a field in a positional argument struct; name + of the positional argument placeholder to be shown in + the help (optional) + Either the `short:` tag or the `long:` must be specified to make the field eligible as an option. diff --git a/help_test.go b/help_test.go index 32220fb..c7ef1ee 100644 --- a/help_test.go +++ b/help_test.go @@ -41,8 +41,8 @@ type helpOptions struct { } `command:"command" alias:"cm" alias:"cmd" description:"A command"` Args struct { - Filename string `name:"filename" description:"A filename"` - Num int `name:"num" description:"A number"` + Filename string `positional-arg-name:"filename" description:"A filename"` + Number int `positional-arg-name:"num" description:"A number"` } `positional-args:"yes"` } From e27a44aaa2b4d6657d6ee24eab42fd6ed3961e3a Mon Sep 17 00:00:00 2001 From: Gabriel Gilder Date: Sun, 25 Jan 2015 00:14:20 -0800 Subject: [PATCH 04/38] Add test for value-name tag --- help_test.go | 81 +++++++++++++++++++++++++++++----------------------- ini_test.go | 9 ++++++ 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/help_test.go b/help_test.go index c7ef1ee..c802883 100644 --- a/help_test.go +++ b/help_test.go @@ -15,11 +15,12 @@ type helpOptions struct { PtrSlice []*string `long:"ptrslice" description:"A slice of pointers to string"` EmptyDescription bool `long:"empty-description"` - Default string `long:"default" default:"Some\nvalue" description:"Test default value"` - DefaultArray []string `long:"default-array" default:"Some value" default:"Other\tvalue" description:"Test default array value"` - DefaultMap map[string]string `long:"default-map" default:"some:value" default:"another:value" description:"Testdefault map value"` - EnvDefault1 string `long:"env-default1" default:"Some value" env:"ENV_DEFAULT" description:"Test env-default1 value"` - EnvDefault2 string `long:"env-default2" env:"ENV_DEFAULT" description:"Test env-default2 value"` + Default string `long:"default" default:"Some\nvalue" description:"Test default value"` + DefaultArray []string `long:"default-array" default:"Some value" default:"Other\tvalue" description:"Test default array value"` + DefaultMap map[string]string `long:"default-map" default:"some:value" default:"another:value" description:"Testdefault map value"` + EnvDefault1 string `long:"env-default1" default:"Some value" env:"ENV_DEFAULT" description:"Test env-default1 value"` + EnvDefault2 string `long:"env-default2" env:"ENV_DEFAULT" description:"Test env-default2 value"` + OptionWithArgName string `long:"opt-with-arg-name" value-name:"something" description:"Option with named argument"` OnlyIni string `ini-name:"only-ini" description:"Option only available in ini"` @@ -75,33 +76,34 @@ func TestHelp(t *testing.T) { TestHelp [OPTIONS] [filename] [num] 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 ("Some\nvalue") - /default-array: Test default array value (Some value, "Other\tvalue") - /default-map: Testdefault map value (some:value, another:value) - /env-default1: Test env-default1 value (Some value) [%ENV_DEFAULT%] - /env-default2: Test env-default2 value [%ENV_DEFAULT%] + /default: Test default value ("Some\nvalue") + /default-array: Test default array value (Some value, "Other\tvalue") + /default-map: Testdefault map value (some:value, another:value) + /env-default1: Test env-default1 value (Some value) [%ENV_DEFAULT%] + /env-default2: Test env-default2 value [%ENV_DEFAULT%] + /opt-with-arg-name:something Option with named argument Other Options: - /s: A slice of strings (some, value) - /intmap: A map from string to int (a:1) + /s: A slice of strings (some, value) + /intmap: A map from string to int (a:1) Subgroup: - /sip.opt: This is a subgroup option + /sip.opt: This is a subgroup option Subsubgroup: - /sip.sap.opt: This is a subsubgroup option + /sip.sap.opt: This is a subsubgroup option 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 - num: A number + filename: A filename + num: A number Available commands: command A command (aliases: cm, cmd) @@ -111,32 +113,36 @@ Available commands: TestHelp [OPTIONS] [filename] [num] 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 ("Some\nvalue") - --default-array= Test default array value (Some value, "Other\tvalue") - --default-map= Testdefault map value (some:value, another:value) - --env-default1= Test env-default1 value (Some value) [$ENV_DEFAULT] - --env-default2= Test env-default2 value [$ENV_DEFAULT] + --default= Test default value ("Some\nvalue") + --default-array= Test default array value (Some value, + "Other\tvalue") + --default-map= Testdefault map value (some:value, + another:value) + --env-default1= Test env-default1 value (Some value) + [$ENV_DEFAULT] + --env-default2= Test env-default2 value [$ENV_DEFAULT] + --opt-with-arg-name=something Option with named argument Other Options: - -s= A slice of strings (some, value) - --intmap= A map from string to int (a:1) + -s= A slice of strings (some, value) + --intmap= A map from string to int (a:1) Subgroup: - --sip.opt= This is a subgroup option + --sip.opt= This is a subgroup option Subsubgroup: - --sip.sap.opt= This is a subsubgroup option + --sip.sap.opt= This is a subsubgroup option Help Options: - -h, --help Show this help message + -h, --help Show this help message Arguments: - filename: A filename - num: A number + filename: A filename + num: A number Available commands: command A command (aliases: cm, cmd) @@ -202,6 +208,9 @@ Test env-default1 value \fB--env-default2\fP Test env-default2 value .TP +\fB--opt-with-arg-name\fP +Option with named argument +.TP \fB-s\fP A slice of strings .TP diff --git a/ini_test.go b/ini_test.go index 8c3176c..c3f3e91 100644 --- a/ini_test.go +++ b/ini_test.go @@ -71,6 +71,9 @@ EnvDefault1 = env-def ; Test env-default2 value EnvDefault2 = env-def +; Option with named argument +OptionWithArgName = + ; Option only available in ini only-ini = @@ -126,6 +129,9 @@ EnvDefault1 = env-def ; Test env-default2 value EnvDefault2 = env-def +; Option with named argument +; OptionWithArgName = + ; Option only available in ini ; only-ini = @@ -178,6 +184,9 @@ EnvDefault1 = env-def ; Test env-default2 value EnvDefault2 = env-def +; Option with named argument +; OptionWithArgName = + ; Option only available in ini ; only-ini = From 3f36317820d47d6e08d0d8bdee51ade32b5e8bd3 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Sat, 14 Feb 2015 12:53:51 -0200 Subject: [PATCH 05/38] Updating the go get path for vet and cover Signed-off-by: Sergio Schvezov --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 485d318..3165f00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,12 +6,12 @@ install: - go build -v ./... # linting - - go get code.google.com/p/go.tools/cmd/vet + - go get golang.org/x/tools/cmd/vet - go get github.com/golang/lint - go install github.com/golang/lint/golint # code coverage - - go get code.google.com/p/go.tools/cmd/cover + - go get golang.org/x/tools/cmd/cover - go get github.com/onsi/ginkgo/ginkgo - go get github.com/modocache/gover - if [ "$TRAVIS_SECURE_ENV_VARS" = "true" ]; then go get github.com/mattn/goveralls; fi From 3c2a20ff44662f7a703701df397255ecc63fae0b Mon Sep 17 00:00:00 2001 From: mattes Date: Fri, 27 Mar 2015 00:41:38 +0100 Subject: [PATCH 06/38] expose isSet --- option.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/option.go b/option.go index 29e702c..d337a62 100644 --- a/option.go +++ b/option.go @@ -155,3 +155,8 @@ func (option *Option) String() string { func (option *Option) Value() interface{} { return option.value.Interface() } + +// IsSet returns true if option has been set +func (option *Option) IsSet() bool { + return option.isSet +} From f59c328da6215a0b6acf0441ef19e361660ff405 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Tue, 31 Mar 2015 12:06:11 +0200 Subject: [PATCH 07/38] Add "Hidden" option to comamnds and options --- command.go | 3 +++ command_private.go | 10 ++++++++-- flags.go | 1 + group_private.go | 2 ++ help.go | 4 ++++ option.go | 3 +++ 6 files changed, 21 insertions(+), 2 deletions(-) diff --git a/command.go b/command.go index 13332ae..f8579d2 100644 --- a/command.go +++ b/command.go @@ -23,6 +23,9 @@ type Command struct { // Whether positional arguments are required ArgsRequired bool + // If true, the option is not displayed in the help output + Hidden bool + commands []*Command hasBuiltinHelpGroup bool args []*Arg diff --git a/command_private.go b/command_private.go index 5d30a8a..2107ee0 100644 --- a/command_private.go +++ b/command_private.go @@ -201,9 +201,15 @@ func (c commandList) Swap(i, j int) { c[i], c[j] = c[j], c[i] } +// FIXME: maybe call this sortedVisibleCommands ? func (c *Command) sortedCommands() []*Command { - ret := make(commandList, len(c.commands)) - copy(ret, c.commands) + ret := make(commandList, 0, len(c.commands)) + + for _, e := range c.commands { + if !e.Hidden { + ret = append(ret, e) + } + } sort.Sort(ret) return []*Command(ret) diff --git a/flags.go b/flags.go index 37d331d..7bdcdb4 100644 --- a/flags.go +++ b/flags.go @@ -104,6 +104,7 @@ The following is a list of tags for struct fields supported by go-flags: slices and maps (optional) value-name: the name of the argument value (to be shown in the help) (optional) + hidden: the option is not visible in the help output base: a base (radix) used to convert strings to integer values, the default base is 10 (i.e. decimal) (optional) diff --git a/group_private.go b/group_private.go index 15251ce..569c81b 100644 --- a/group_private.go +++ b/group_private.go @@ -131,6 +131,7 @@ func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, h optional := (mtag.Get("optional") != "") required := (mtag.Get("required") != "") + hidden := (mtag.Get("hidden") != "") option := &Option{ Description: description, @@ -144,6 +145,7 @@ func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, h Required: required, ValueName: valueName, DefaultMask: defaultMask, + Hidden: hidden, group: g, diff --git a/help.go b/help.go index e26fcd0..88c4b5f 100644 --- a/help.go +++ b/help.go @@ -108,6 +108,10 @@ func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alig prefix += 4 } + if option.Hidden { + return + } + line.WriteString(strings.Repeat(" ", prefix)) if option.ShortName != 0 { diff --git a/option.go b/option.go index 29e702c..00fb461 100644 --- a/option.go +++ b/option.go @@ -51,6 +51,9 @@ type Option struct { // error. Required bool + // If true, the option is not displayed in the help output + Hidden bool + // A name for the value of an option shown in the Help as --flag [ValueName] ValueName string From b77ba87d34aa2b26f97093a7e47d18385b47dd74 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sat, 27 Jun 2015 18:14:31 +0200 Subject: [PATCH 08/38] Add default, optional values in man page output Fixes #140. --- convert.go | 20 +++++++++++++++++++ help_test.go | 40 ++++++++++++++++++++++---------------- man.go | 54 +++++++++++++++++++++++++++++++++++++++------------- option.go | 2 +- 4 files changed, 86 insertions(+), 30 deletions(-) diff --git a/convert.go b/convert.go index 191b5f4..1185737 100644 --- a/convert.go +++ b/convert.go @@ -312,6 +312,26 @@ func quoteIfNeeded(s string) string { return s } +func quoteIfNeededV(s []string) []string { + ret := make([]string, len(s)) + + for i, v := range s { + ret[i] = quoteIfNeeded(v) + } + + return ret +} + +func quoteV(s []string) []string { + ret := make([]string, len(s)) + + for i, v := range s { + ret[i] = strconv.Quote(v) + } + + return ret +} + func unquoteIfPossible(s string) (string, error) { if len(s) == 0 || s[0] != '"' { return s, nil diff --git a/help_test.go b/help_test.go index c802883..e10f4b6 100644 --- a/help_test.go +++ b/help_test.go @@ -173,6 +173,14 @@ func TestMan(t *testing.T) { tt := time.Now() + var envDefaultName string + + if runtime.GOOS == "windows" { + envDefaultName = "%ENV_DEFAULT%" + } else { + envDefaultName = "$ENV_DEFAULT" + } + expected := fmt.Sprintf(`.TH TestMan 1 "%s" .SH NAME TestMan \- Test manpage generation @@ -182,45 +190,45 @@ TestMan \- Test manpage generation This is a somewhat \fBlonger\fP description of what this does .SH OPTIONS .TP -\fB-v, --verbose\fP +\fB\fB\-v\fR, \fB\-\-verbose\fR\fP Show verbose debug information .TP -\fB-c\fP +\fB\fB\-c\fR\fP Call phone number .TP -\fB--ptrslice\fP +\fB\fB\-\-ptrslice\fR\fP A slice of pointers to string .TP -\fB--empty-description\fP +\fB\fB\-\-empty-description\fR\fP .TP -\fB--default\fP +\fB\fB\-\-default\fR \fP Test default value .TP -\fB--default-array\fP +\fB\fB\-\-default-array\fR \fP Test default array value .TP -\fB--default-map\fP +\fB\fB\-\-default-map\fR \fP Testdefault map value .TP -\fB--env-default1\fP +\fB\fB\-\-env-default1\fR \fP Test env-default1 value .TP -\fB--env-default2\fP +\fB\fB\-\-env-default2\fR \fP Test env-default2 value .TP -\fB--opt-with-arg-name\fP +\fB\fB\-\-opt-with-arg-name\fR \fIsomething\fR\fP Option with named argument .TP -\fB-s\fP +\fB\fB\-s\fR \fP A slice of strings .TP -\fB--intmap\fP +\fB\fB\-\-intmap\fR \fP A map from string to int .TP -\fB--sip.opt\fP +\fB\fB\-\-sip.opt\fR\fP This is a subgroup option .TP -\fB--sip.sap.opt\fP +\fB\fB\-\-sip.sap.opt\fR\fP This is a subsubgroup option .SH COMMANDS .SS command @@ -234,9 +242,9 @@ Longer \fBcommand\fP description \fBAliases\fP: cm, cmd .TP -\fB--extra-verbose\fP +\fB\fB\-\-extra-verbose\fR\fP Use for extra verbosity -`, tt.Format("2 January 2006")) +`, tt.Format("2 January 2006"), envDefaultName) assertDiff(t, got, expected, "man page") } diff --git a/man.go b/man.go index e8e5916..95347d0 100644 --- a/man.go +++ b/man.go @@ -3,30 +3,35 @@ package flags import ( "fmt" "io" + "runtime" "strings" "time" ) +func manQuote(s string) string { + return strings.Replace(s, "\\", "\\\\", -1) +} + func formatForMan(wr io.Writer, s string) { for { idx := strings.IndexRune(s, '`') if idx < 0 { - fmt.Fprintf(wr, "%s", s) + fmt.Fprintf(wr, "%s", manQuote(s)) break } - fmt.Fprintf(wr, "%s", s[:idx]) + fmt.Fprintf(wr, "%s", manQuote(s[:idx])) s = s[idx+1:] idx = strings.IndexRune(s, '\'') if idx < 0 { - fmt.Fprintf(wr, "%s", s) + fmt.Fprintf(wr, "%s", manQuote(s)) break } - fmt.Fprintf(wr, "\\fB%s\\fP", s[:idx]) + fmt.Fprintf(wr, "\\fB%s\\fP", manQuote(s[:idx])) s = s[idx+1:] } } @@ -42,7 +47,7 @@ func writeManPageOptions(wr io.Writer, grp *Group) { fmt.Fprintf(wr, "\\fB") if opt.ShortName != 0 { - fmt.Fprintf(wr, "-%c", opt.ShortName) + fmt.Fprintf(wr, "\\fB\\-%c\\fR", opt.ShortName) } if len(opt.LongName) != 0 { @@ -50,10 +55,33 @@ func writeManPageOptions(wr io.Writer, grp *Group) { fmt.Fprintf(wr, ", ") } - fmt.Fprintf(wr, "--%s", opt.LongNameWithNamespace()) + fmt.Fprintf(wr, "\\fB\\-\\-%s\\fR", manQuote(opt.LongNameWithNamespace())) + } + + if len(opt.ValueName) != 0 || opt.OptionalArgument { + if opt.OptionalArgument { + fmt.Fprintf(wr, " [\\fI%s=%s\\fR]", manQuote(opt.ValueName), manQuote(strings.Join(quoteV(opt.OptionalValue), ", "))) + } else { + fmt.Fprintf(wr, " \\fI%s\\fR", manQuote(opt.ValueName)) + } + } + + if len(opt.Default) != 0 { + fmt.Fprintf(wr, " ", manQuote(strings.Join(quoteV(opt.Default), ", "))) + } else if len(opt.EnvDefaultKey) != 0 { + if runtime.GOOS == "windows" { + fmt.Fprintf(wr, " ", manQuote(opt.EnvDefaultKey)) + } else { + fmt.Fprintf(wr, " ", manQuote(opt.EnvDefaultKey)) + } + } + + if opt.Required { + fmt.Fprintf(wr, " (\\fIrequired\\fR)") } fmt.Fprintln(wr, "\\fP") + if len(opt.Description) != 0 { formatForMan(wr, opt.Description) fmt.Fprintln(wr, "") @@ -85,10 +113,10 @@ func writeManPageCommand(wr io.Writer, name string, root *Command, command *Comm if len(command.LongDescription) > 0 { fmt.Fprintln(wr, "") - cmdstart := fmt.Sprintf("The %s command", command.Name) + cmdstart := fmt.Sprintf("The %s command", manQuote(command.Name)) if strings.HasPrefix(command.LongDescription, cmdstart) { - fmt.Fprintf(wr, "The \\fI%s\\fP command", command.Name) + fmt.Fprintf(wr, "The \\fI%s\\fP command", manQuote(command.Name)) formatForMan(wr, command.LongDescription[len(cmdstart):]) fmt.Fprintln(wr, "") @@ -113,11 +141,11 @@ func writeManPageCommand(wr io.Writer, name string, root *Command, command *Comm } if len(usage) > 0 { - fmt.Fprintf(wr, "\n\\fBUsage\\fP: %s %s\n\n", pre, usage) + fmt.Fprintf(wr, "\n\\fBUsage\\fP: %s %s\n\n", manQuote(pre), manQuote(usage)) } if len(command.Aliases) > 0 { - fmt.Fprintf(wr, "\n\\fBAliases\\fP: %s\n\n", strings.Join(command.Aliases, ", ")) + fmt.Fprintf(wr, "\n\\fBAliases\\fP: %s\n\n", manQuote(strings.Join(command.Aliases, ", "))) } writeManPageOptions(wr, command.Group) @@ -129,9 +157,9 @@ func writeManPageCommand(wr io.Writer, name string, root *Command, command *Comm func (p *Parser) WriteManPage(wr io.Writer) { t := time.Now() - fmt.Fprintf(wr, ".TH %s 1 \"%s\"\n", p.Name, t.Format("2 January 2006")) + fmt.Fprintf(wr, ".TH %s 1 \"%s\"\n", manQuote(p.Name), t.Format("2 January 2006")) fmt.Fprintln(wr, ".SH NAME") - fmt.Fprintf(wr, "%s \\- %s\n", p.Name, p.ShortDescription) + fmt.Fprintf(wr, "%s \\- %s\n", manQuote(p.Name), manQuote(p.ShortDescription)) fmt.Fprintln(wr, ".SH SYNOPSIS") usage := p.Usage @@ -140,7 +168,7 @@ func (p *Parser) WriteManPage(wr io.Writer) { usage = "[OPTIONS]" } - fmt.Fprintf(wr, "\\fB%s\\fP %s\n", p.Name, usage) + fmt.Fprintf(wr, "\\fB%s\\fP %s\n", manQuote(p.Name), manQuote(usage)) fmt.Fprintln(wr, ".SH DESCRIPTION") formatForMan(wr, p.LongDescription) diff --git a/option.go b/option.go index d337a62..42c1059 100644 --- a/option.go +++ b/option.go @@ -35,7 +35,7 @@ type Option struct { // 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 Default will be set in the field this option represents. + // value of OptionalValue will be set in the field this option represents. // This is only valid for non-boolean options. OptionalArgument bool From 1b89bf73cd2c3a911d7b2a279ab085c4a18cf539 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 28 Jun 2015 16:14:40 +0200 Subject: [PATCH 09/38] Allow specifying parent options after subcommands Issue #141. --- command_private.go | 25 +++++++++++++++++++++-- command_test.go | 50 +++++++++++++++++++++++++++++++++++++++++++++- flags.go | 14 ++++++++----- 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/command_private.go b/command_private.go index 5d30a8a..82ce793 100644 --- a/command_private.go +++ b/command_private.go @@ -144,6 +144,25 @@ func (c *Command) makeLookup() lookup { commands: make(map[string]*Command), } + parent := c.parent + + for parent != nil { + if cmd, ok := parent.(*Command); ok { + cmd.fillLookup(&ret, true) + } + + if grp, ok := parent.(*Group); ok { + parent = grp + } else { + parent = nil + } + } + + c.fillLookup(&ret, false) + return ret +} + +func (c *Command) fillLookup(ret *lookup, onlyOptions bool) { c.eachGroup(func(g *Group) { for _, option := range g.options { if option.ShortName != 0 { @@ -156,6 +175,10 @@ func (c *Command) makeLookup() lookup { } }) + if onlyOptions { + return + } + for _, subcommand := range c.commands { ret.commands[subcommand.Name] = subcommand @@ -163,8 +186,6 @@ func (c *Command) makeLookup() lookup { ret.commands[a] = subcommand } } - - return ret } func (c *Command) groupByName(name string) *Group { diff --git a/command_test.go b/command_test.go index a093e15..1d904ae 100644 --- a/command_test.go +++ b/command_test.go @@ -95,7 +95,55 @@ func TestCommandFlagOrder2(t *testing.T) { } `command:"cmd"` }{} - assertParseFail(t, ErrUnknownFlag, "unknown flag `v'", &opts, "cmd", "-v", "-g") + assertParseSuccess(t, &opts, "cmd", "-v", "-g") + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if !opts.Command.G { + t.Errorf("Expected Command.G to be true") + } +} + +func TestCommandFlagOverride1(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Command struct { + Value bool `short:"v"` + } `command:"cmd"` + }{} + + assertParseSuccess(t, &opts, "-v", "cmd") + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if opts.Command.Value { + t.Errorf("Expected Command.Value to be false") + } +} + +func TestCommandFlagOverride2(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Command struct { + Value bool `short:"v"` + } `command:"cmd"` + }{} + + assertParseSuccess(t, &opts, "cmd", "-v") + + if opts.Value { + t.Errorf("Expected Value to be false") + } + + if !opts.Command.Value { + t.Errorf("Expected Command.Value to be true") + } } func TestCommandEstimate(t *testing.T) { diff --git a/flags.go b/flags.go index 37d331d..4ac6793 100644 --- a/flags.go +++ b/flags.go @@ -183,12 +183,16 @@ the Commander interface, then its Execute method will be run with the remaining command line arguments. Command structs can have options which become valid to parse after the -command has been specified on the command line. It is currently not valid -to specify options from the parent level of the command after the command -name has occurred. Thus, given a top-level option "-v" and a command "add": +command has been specified on the command line, in addition to the options +of all the parent commands. I.e. considering a -v flag on the parser and an +add command, the following are equivalent: - Valid: ./app -v add - Invalid: ./app add -v + ./app -v add + ./app add -v + +However, if the -v flag is defined on the add command, then the first of +the two examples above would fail since the -v flag is not defined before +the add command. Completion From c104d9a3fc3ee3bc1450367ef02ed736da71e0ad Mon Sep 17 00:00:00 2001 From: Yuta Hayashibe Date: Tue, 14 Jul 2015 17:57:40 +0900 Subject: [PATCH 10/38] Accept arguments which start with '-' if the next character is a digit --- optstyle_other.go | 3 ++- parser_test.go | 27 +++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/optstyle_other.go b/optstyle_other.go index 29ca4b6..815db54 100644 --- a/optstyle_other.go +++ b/optstyle_other.go @@ -4,6 +4,7 @@ package flags import ( "strings" + "unicode" ) const ( @@ -17,7 +18,7 @@ func argumentStartsOption(arg string) bool { } func argumentIsOption(arg string) bool { - if len(arg) > 1 && arg[0] == '-' && arg[1] != '-' { + if len(arg) > 1 && arg[0] == '-' && arg[1] != '-' && !unicode.IsDigit(rune(arg[1])) { return true } diff --git a/parser_test.go b/parser_test.go index 5792872..dff979a 100644 --- a/parser_test.go +++ b/parser_test.go @@ -14,6 +14,9 @@ type defaultOptions struct { Int int `long:"i"` IntDefault int `long:"id" default:"1"` + Float64 float64 `long:"f"` + Float64Default float64 `long:"fd" default:"-3.14"` + String string `long:"str"` StringDefault string `long:"strd" default:"abc"` StringNotUnquoted string `long:"strnot" unquote:"false"` @@ -41,6 +44,9 @@ func TestDefaults(t *testing.T) { Int: 0, IntDefault: 1, + Float64: 0.0, + Float64Default: -3.14, + String: "", StringDefault: "abc", @@ -56,11 +62,14 @@ func TestDefaults(t *testing.T) { }, { msg: "non-zero value arguments, expecting overwritten arguments", - args: []string{"--i=3", "--id=3", "--str=def", "--strd=def", "--t=3ms", "--td=3ms", "--m=c:3", "--md=c:3", "--s=3", "--sd=3"}, + args: []string{"--i=3", "--id=3", "--f=-2.71", "--fd=2.71", "--str=def", "--strd=def", "--t=3ms", "--td=3ms", "--m=c:3", "--md=c:3", "--s=3", "--sd=3"}, expected: defaultOptions{ Int: 3, IntDefault: 3, + Float64: -2.71, + Float64Default: 2.71, + String: "def", StringDefault: "def", @@ -76,11 +85,14 @@ func TestDefaults(t *testing.T) { }, { msg: "zero value arguments, expecting overwritten arguments", - args: []string{"--i=0", "--id=0", "--str", "", "--strd=\"\"", "--t=0ms", "--td=0s", "--m=:0", "--md=:0", "--s=0", "--sd=0"}, + args: []string{"--i=0", "--id=0", "--f=0", "--fd=0", "--str", "", "--strd=\"\"", "--t=0ms", "--td=0s", "--m=:0", "--md=:0", "--s=0", "--sd=0"}, expected: defaultOptions{ Int: 0, IntDefault: 0, + Float64: 0, + Float64Default: 0, + String: "", StringDefault: "", @@ -344,6 +356,17 @@ func TestOptionAsArgument(t *testing.T) { // Accept any single character arguments including '-' args: []string{"--string-slice", "-"}, }, + { + // Accept argumtns which start with '-' if the next character is a digit + args: []string{"--string-slice", "-3.14"}, + }, + { + // Do not accept argumtns which start with '-' if the next character is not a digit + args: []string{"--string-slice", "-character"}, + expectError: true, + errType: ErrExpectedArgument, + errMsg: "expected argument for flag `--string-slice', but got option `-character'", + }, { args: []string{"-o", "-", "-"}, rest: []string{"-", "-"}, From a22c1141f37733bab4433d97fe31a62c95d74eb3 Mon Sep 17 00:00:00 2001 From: Yuta Hayashibe Date: Sun, 16 Aug 2015 17:56:23 +0900 Subject: [PATCH 11/38] Fix typos --- parser_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parser_test.go b/parser_test.go index dff979a..241aecf 100644 --- a/parser_test.go +++ b/parser_test.go @@ -357,11 +357,11 @@ func TestOptionAsArgument(t *testing.T) { args: []string{"--string-slice", "-"}, }, { - // Accept argumtns which start with '-' if the next character is a digit + // Accept arguments which start with '-' if the next character is a digit args: []string{"--string-slice", "-3.14"}, }, { - // Do not accept argumtns which start with '-' if the next character is not a digit + // Do not accept arguments which start with '-' if the next character is not a digit args: []string{"--string-slice", "-character"}, expectError: true, errType: ErrExpectedArgument, From 4a954c811a96898c02a04be92be949cf6a261d90 Mon Sep 17 00:00:00 2001 From: Yuta Hayashibe Date: Sun, 16 Aug 2015 18:07:55 +0900 Subject: [PATCH 12/38] Reverted the function argumentIsOption --- optstyle_other.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/optstyle_other.go b/optstyle_other.go index 815db54..29ca4b6 100644 --- a/optstyle_other.go +++ b/optstyle_other.go @@ -4,7 +4,6 @@ package flags import ( "strings" - "unicode" ) const ( @@ -18,7 +17,7 @@ func argumentStartsOption(arg string) bool { } func argumentIsOption(arg string) bool { - if len(arg) > 1 && arg[0] == '-' && arg[1] != '-' && !unicode.IsDigit(rune(arg[1])) { + if len(arg) > 1 && arg[0] == '-' && arg[1] != '-' { return true } From adb660f62867434be9bd53189dc6d967d2248623 Mon Sep 17 00:00:00 2001 From: Yuta Hayashibe Date: Sun, 16 Aug 2015 18:15:31 +0900 Subject: [PATCH 13/38] Added tests for a numeric flag like -3 --- parser_test.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/parser_test.go b/parser_test.go index 241aecf..51091f1 100644 --- a/parser_test.go +++ b/parser_test.go @@ -17,6 +17,8 @@ type defaultOptions struct { Float64 float64 `long:"f"` Float64Default float64 `long:"fd" default:"-3.14"` + NumericFlag bool `short:"3" default:"false"` + String string `long:"str"` StringDefault string `long:"strd" default:"abc"` StringNotUnquoted string `long:"strnot" unquote:"false"` @@ -47,6 +49,8 @@ func TestDefaults(t *testing.T) { Float64: 0.0, Float64Default: -3.14, + NumericFlag: false, + String: "", StringDefault: "abc", @@ -62,7 +66,7 @@ func TestDefaults(t *testing.T) { }, { msg: "non-zero value arguments, expecting overwritten arguments", - args: []string{"--i=3", "--id=3", "--f=-2.71", "--fd=2.71", "--str=def", "--strd=def", "--t=3ms", "--td=3ms", "--m=c:3", "--md=c:3", "--s=3", "--sd=3"}, + args: []string{"--i=3", "--id=3", "--f=-2.71", "--fd=2.71", "-3", "--str=def", "--strd=def", "--t=3ms", "--td=3ms", "--m=c:3", "--md=c:3", "--s=3", "--sd=3"}, expected: defaultOptions{ Int: 3, IntDefault: 3, @@ -70,6 +74,8 @@ func TestDefaults(t *testing.T) { Float64: -2.71, Float64Default: 2.71, + NumericFlag: true, + String: "def", StringDefault: "def", @@ -357,8 +363,11 @@ func TestOptionAsArgument(t *testing.T) { args: []string{"--string-slice", "-"}, }, { - // Accept arguments which start with '-' if the next character is a digit - args: []string{"--string-slice", "-3.14"}, + // Do not accept arguments which start with '-' even if the next character is a digit + args: []string{"--string-slice", "-3.14"}, + expectError: true, + errType: ErrExpectedArgument, + errMsg: "expected argument for flag `--string-slice', but got option `-3.14'", }, { // Do not accept arguments which start with '-' if the next character is not a digit From 1d02749df00a77d3d25417de3453ccc8b9b86911 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sat, 10 Oct 2015 18:40:01 +0200 Subject: [PATCH 14/38] Implement support for limiting argument values to a certain set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a choice:”value” tag that can be specified multiple times and limits the valid values of an option to a specific set. Fixes #147. --- error.go | 6 ++++ flags.go | 3 +- group_private.go | 2 ++ help.go | 12 ++++++- help_test.go | 83 +++++++++++++++++++++++++++-------------------- ini_test.go | 9 +++++ option.go | 3 ++ option_private.go | 23 +++++++++++++ parser_test.go | 20 +++++++++--- 9 files changed, 118 insertions(+), 43 deletions(-) diff --git a/error.go b/error.go index fce9d31..2f27aee 100644 --- a/error.go +++ b/error.go @@ -51,6 +51,10 @@ const ( // ErrUnknownCommand indicates that an unknown command was specified. ErrUnknownCommand + + // ErrInvalidChoice indicates an invalid option value which only allows + // a certain number of choices. + ErrInvalidChoice ) func (e ErrorType) String() string { @@ -81,6 +85,8 @@ func (e ErrorType) String() string { return "command required" case ErrUnknownCommand: return "unknown command" + case ErrInvalidChoice: + return "invalid choice" } return "unrecognized error type" diff --git a/flags.go b/flags.go index 4ac6793..6bf736b 100644 --- a/flags.go +++ b/flags.go @@ -104,6 +104,8 @@ The following is a list of tags for struct fields supported by go-flags: slices and maps (optional) value-name: the name of the argument value (to be shown in the help) (optional) + choice: limits the values for an option to a set of values. + This tag can be specified mltiple times (optional) base: a base (radix) used to convert strings to integer values, the default base is 10 (i.e. decimal) (optional) @@ -138,7 +140,6 @@ The following is a list of tags for struct fields supported by go-flags: of the positional argument placeholder to be shown in the help (optional) - Either the `short:` tag or the `long:` must be specified to make the field eligible as an option. diff --git a/group_private.go b/group_private.go index 15251ce..e2b1d9c 100644 --- a/group_private.go +++ b/group_private.go @@ -131,6 +131,7 @@ func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, h optional := (mtag.Get("optional") != "") required := (mtag.Get("required") != "") + choices := mtag.GetMany("choice") option := &Option{ Description: description, @@ -144,6 +145,7 @@ func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, h Required: required, ValueName: valueName, DefaultMask: defaultMask, + Choices: choices, group: g, diff --git a/help.go b/help.go index e26fcd0..1431b12 100644 --- a/help.go +++ b/help.go @@ -92,7 +92,13 @@ func (p *Parser) getAlignmentInfo() alignmentInfo { ret.hasValueName = true } - ret.updateLen(info.LongNameWithNamespace()+info.ValueName, c != p.Command) + l := info.LongNameWithNamespace() + info.ValueName + + if len(info.Choices) != 0 { + l += "[" + strings.Join(info.Choices, "|") + "]" + } + + ret.updateLen(l, c != p.Command) } }) @@ -136,6 +142,10 @@ func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alig if len(option.ValueName) > 0 { line.WriteString(option.ValueName) } + + if len(option.Choices) > 0 { + line.WriteString("[" + strings.Join(option.Choices, "|") + "]") + } } written := line.Len() diff --git a/help_test.go b/help_test.go index e10f4b6..cd7beb4 100644 --- a/help_test.go +++ b/help_test.go @@ -21,6 +21,7 @@ type helpOptions struct { EnvDefault1 string `long:"env-default1" default:"Some value" env:"ENV_DEFAULT" description:"Test env-default1 value"` EnvDefault2 string `long:"env-default2" env:"ENV_DEFAULT" description:"Test env-default2 value"` OptionWithArgName string `long:"opt-with-arg-name" value-name:"something" description:"Option with named argument"` + OptionWithChoices string `long:"opt-with-choices" value-name:"choice" choice:"dog" choice:"cat" description:"Option with choices"` OnlyIni string `ini-name:"only-ini" description:"Option only available in ini"` @@ -76,34 +77,39 @@ func TestHelp(t *testing.T) { TestHelp [OPTIONS] [filename] [num] 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 ("Some\nvalue") - /default-array: Test default array value (Some value, "Other\tvalue") - /default-map: Testdefault map value (some:value, another:value) - /env-default1: Test env-default1 value (Some value) [%ENV_DEFAULT%] - /env-default2: Test env-default2 value [%ENV_DEFAULT%] - /opt-with-arg-name:something Option with named argument + /default: Test default value ("Some\nvalue") + /default-array: Test default array value (Some + value, "Other\tvalue") + /default-map: Testdefault map value (some:value, + another:value) + /env-default1: Test env-default1 value (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 (some, value) - /intmap: A map from string to int (a:1) + /s: A slice of strings (some, value) + /intmap: A map from string to int (a:1) Subgroup: - /sip.opt: This is a subgroup option + /sip.opt: This is a subgroup option Subsubgroup: - /sip.sap.opt: This is a subsubgroup option + /sip.sap.opt: This is a subsubgroup option 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 - num: A number + filename: A filename + num: A number Available commands: command A command (aliases: cm, cmd) @@ -113,36 +119,38 @@ Available commands: TestHelp [OPTIONS] [filename] [num] 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 ("Some\nvalue") - --default-array= Test default array value (Some value, - "Other\tvalue") - --default-map= Testdefault map value (some:value, - another:value) - --env-default1= Test env-default1 value (Some value) - [$ENV_DEFAULT] - --env-default2= Test env-default2 value [$ENV_DEFAULT] - --opt-with-arg-name=something Option with named argument + --default= Test default value ("Some\nvalue") + --default-array= Test default array value (Some + value, "Other\tvalue") + --default-map= Testdefault map value (some:value, + another:value) + --env-default1= Test env-default1 value (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 (some, value) - --intmap= A map from string to int (a:1) + -s= A slice of strings (some, value) + --intmap= A map from string to int (a:1) Subgroup: - --sip.opt= This is a subgroup option + --sip.opt= This is a subgroup option Subsubgroup: - --sip.sap.opt= This is a subsubgroup option + --sip.sap.opt= This is a subsubgroup option Help Options: - -h, --help Show this help message + -h, --help Show this help message Arguments: - filename: A filename - num: A number + filename: A filename + num: A number Available commands: command A command (aliases: cm, cmd) @@ -219,6 +227,9 @@ Test env-default2 value \fB\fB\-\-opt-with-arg-name\fR \fIsomething\fR\fP Option with named argument .TP +\fB\fB\-\-opt-with-choices\fR \fIchoice\fR\fP +Option with choices +.TP \fB\fB\-s\fR \fP A slice of strings .TP diff --git a/ini_test.go b/ini_test.go index 215b757..aa3b556 100644 --- a/ini_test.go +++ b/ini_test.go @@ -74,6 +74,9 @@ EnvDefault2 = env-def ; Option with named argument OptionWithArgName = +; Option with choices +OptionWithChoices = + ; Option only available in ini only-ini = @@ -132,6 +135,9 @@ EnvDefault2 = env-def ; Option with named argument ; OptionWithArgName = +; Option with choices +; OptionWithChoices = + ; Option only available in ini ; only-ini = @@ -187,6 +193,9 @@ EnvDefault2 = env-def ; Option with named argument ; OptionWithArgName = +; Option with choices +; OptionWithChoices = + ; Option only available in ini ; only-ini = diff --git a/option.go b/option.go index 42c1059..73c24f5 100644 --- a/option.go +++ b/option.go @@ -59,6 +59,9 @@ type Option struct { // passwords. DefaultMask string + // If non empty, only a certain set of values is allowed for an option. + Choices []string + // The group which the option belongs to group *Group diff --git a/option_private.go b/option_private.go index d36c841..06fc206 100644 --- a/option_private.go +++ b/option_private.go @@ -12,6 +12,29 @@ import ( func (option *Option) set(value *string) error { option.isSet = true + if len(option.Choices) != 0 { + found := false + + for _, choice := range option.Choices { + if choice == *value { + found = true + break + } + } + + if !found { + allowed := strings.Join(option.Choices[0:len(option.Choices)-1], ", ") + + if len(option.Choices) > 1 { + allowed += " or " + option.Choices[len(option.Choices)-1] + } + + return newErrorf(ErrInvalidChoice, + "Invalid value `%s' for option `%s'. Allowed values are: %s", + *value, option, allowed) + } + } + if option.isFunc() { return option.call(value) } else if value != nil { diff --git a/parser_test.go b/parser_test.go index 51091f1..fe6865c 100644 --- a/parser_test.go +++ b/parser_test.go @@ -338,21 +338,21 @@ func TestOptionAsArgument(t *testing.T) { args: []string{"--string-slice", "foobar", "--string-slice", "-o"}, expectError: true, errType: ErrExpectedArgument, - errMsg: "expected argument for flag `--string-slice', but got option `-o'", + errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-o'", }, { // long option must not be accepted as argument args: []string{"--string-slice", "foobar", "--string-slice", "--other-option"}, expectError: true, errType: ErrExpectedArgument, - errMsg: "expected argument for flag `--string-slice', but got option `--other-option'", + errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `--other-option'", }, { // long option must not be accepted as argument args: []string{"--string-slice", "--"}, expectError: true, errType: ErrExpectedArgument, - errMsg: "expected argument for flag `--string-slice', but got double dash `--'", + errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got double dash `--'", }, { // quoted and appended option should be accepted as argument (even if it looks like an option) @@ -367,14 +367,14 @@ func TestOptionAsArgument(t *testing.T) { args: []string{"--string-slice", "-3.14"}, expectError: true, errType: ErrExpectedArgument, - errMsg: "expected argument for flag `--string-slice', but got option `-3.14'", + errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-3.14'", }, { // Do not accept arguments which start with '-' if the next character is not a digit args: []string{"--string-slice", "-character"}, expectError: true, errType: ErrExpectedArgument, - errMsg: "expected argument for flag `--string-slice', but got option `-character'", + errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-character'", }, { args: []string{"-o", "-", "-"}, @@ -461,3 +461,13 @@ func TestUnknownFlagHandler(t *testing.T) { assertErrorf(t, "Parser should have returned error, but returned nil") } } + +func TestChoices(t *testing.T) { + var opts struct { + Choice string `long:"choose" choice:"v1" choice:"v2"` + } + + assertParseFail(t, ErrInvalidChoice, "Invalid value `invalid' for option `"+defaultLongOptDelimiter+"choose'. Allowed values are: v1 or v2", &opts, "--choose", "invalid") + assertParseSuccess(t, &opts, "--choose", "v2") + assertString(t, opts.Choice, "v2") +} From f412642eaca3b4e6f6ee12aec0e44adebef5c4ea Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sat, 10 Oct 2015 18:56:03 +0200 Subject: [PATCH 15/38] Prefix default value in help with default: Fixes #145. --- help.go | 2 +- help_test.go | 42 ++++++++++++++++++++++++------------------ parser.go | 3 ++- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/help.go b/help.go index 1431b12..24a5778 100644 --- a/help.go +++ b/help.go @@ -204,7 +204,7 @@ func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alig var desc string if def != "" { - desc = fmt.Sprintf("%s (%v)%s", option.Description, def, envDef) + desc = fmt.Sprintf("%s (default: %v)%s", option.Description, def, envDef) } else { desc = option.Description + envDef } diff --git a/help_test.go b/help_test.go index cd7beb4..1843cba 100644 --- a/help_test.go +++ b/help_test.go @@ -81,21 +81,24 @@ Application Options: /c: Call phone number /ptrslice: A slice of pointers to string /empty-description - /default: Test default value ("Some\nvalue") - /default-array: Test default array value (Some - value, "Other\tvalue") - /default-map: Testdefault map value (some:value, - another:value) - /env-default1: Test env-default1 value (Some - value) [%ENV_DEFAULT%] + /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 (some, value) - /intmap: A map from string to int (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 @@ -123,21 +126,24 @@ Application Options: -c= Call phone number --ptrslice= A slice of pointers to string --empty-description - --default= Test default value ("Some\nvalue") - --default-array= Test default array value (Some - value, "Other\tvalue") - --default-map= Testdefault map value (some:value, - another:value) - --env-default1= Test env-default1 value (Some - value) [$ENV_DEFAULT] + --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 (some, value) - --intmap= A map from string to int (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 diff --git a/parser.go b/parser.go index 6f45a03..d403a40 100644 --- a/parser.go +++ b/parser.go @@ -162,7 +162,8 @@ func (p *Parser) Parse() ([]string, error) { // // When the common help group has been added (AddHelp) and either -h or --help // was specified in the command line arguments, a help message will be -// automatically printed. Furthermore, the special error type ErrHelp is returned. +// automatically printed if the PrintErrors option is enabled. +// Furthermore, the special error type ErrHelp is returned. // It is up to the caller to exit the program if so desired. func (p *Parser) ParseArgs(args []string) ([]string, error) { if p.internalError != nil { From 1c008c186a1945576a96a77f6d066014a1e9aa1e Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sat, 10 Oct 2015 20:19:53 +0200 Subject: [PATCH 16/38] Store default literal value before parsing Fixes #143. --- command_private.go | 10 ++++++ help.go | 38 +++-------------------- help_test.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++ option.go | 2 ++ option_private.go | 35 +++++++++++++++++++++ parser.go | 23 +++++++------- 6 files changed, 139 insertions(+), 45 deletions(-) diff --git a/command_private.go b/command_private.go index 82ce793..3ae7247 100644 --- a/command_private.go +++ b/command_private.go @@ -104,6 +104,16 @@ func (c *Command) scan() error { return c.scanType(c.scanSubcommandHandler(c.Group)) } +func (c *Command) eachOption(f func(*Command, *Group, *Option)) { + c.eachCommand(func(c *Command) { + c.eachGroup(func(g *Group) { + for _, option := range g.options { + f(c, g, option) + } + }) + }, true) +} + func (c *Command) eachCommand(f func(*Command), recurse bool) { f(c) diff --git a/help.go b/help.go index 24a5778..5abe257 100644 --- a/help.go +++ b/help.go @@ -9,7 +9,6 @@ import ( "bytes" "fmt" "io" - "reflect" "runtime" "strings" "unicode/utf8" @@ -155,39 +154,12 @@ func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alig dw := descstart - written writer.WriteString(strings.Repeat(" ", dw)) - def := "" - defs := option.Default + var def string - if len(option.DefaultMask) != 0 { - if option.DefaultMask != "-" { - def = option.DefaultMask - } - } else if len(defs) == 0 && option.canArgument() { - var showdef bool - - switch option.field.Type.Kind() { - case reflect.Func, reflect.Ptr: - showdef = !option.value.IsNil() - case reflect.Slice, reflect.String, reflect.Array: - showdef = option.value.Len() > 0 - case reflect.Map: - showdef = !option.value.IsNil() && option.value.Len() > 0 - default: - zeroval := reflect.Zero(option.field.Type) - showdef = !reflect.DeepEqual(zeroval.Interface(), option.value.Interface()) - } - - if showdef { - def, _ = convertToString(option.value, option.tag) - } - } else if len(defs) != 0 { - l := len(defs) - 1 - - for i := 0; i < l; i++ { - def += quoteIfNeeded(defs[i]) + ", " - } - - def += quoteIfNeeded(defs[l]) + if len(option.DefaultMask) != 0 && option.DefaultMask != "-" { + def = option.DefaultMask + } else { + def = option.defaultLiteral } var envDef string diff --git a/help_test.go b/help_test.go index 1843cba..5dbffb6 100644 --- a/help_test.go +++ b/help_test.go @@ -315,3 +315,79 @@ Help Options: assertDiff(t, e.Message, expected, "help message") } } + +func TestHelpDefaults(t *testing.T) { + var expected string + + if runtime.GOOS == "windows" { + expected = `Usage: + TestHelpDefaults [OPTIONS] + +Application Options: + /with-default: With default (default: default-value) + /without-default: Without default + /with-programmatic-default: With programmatic default (default: + default-value) + +Help Options: + /? Show this help message + /h, /help Show this help message +` + } else { + expected = `Usage: + TestHelpDefaults [OPTIONS] + +Application Options: + --with-default= With default (default: default-value) + --without-default= Without default + --with-programmatic-default= With programmatic default (default: + default-value) + +Help Options: + -h, --help Show this help message +` + } + + tests := []struct { + Args []string + Output string + }{ + { + Args: []string{"-h"}, + Output: expected, + }, + { + Args: []string{"--with-default", "other-value", "--with-programmatic-default", "other-value", "-h"}, + Output: expected, + }, + } + + for _, test := range tests { + var opts struct { + WithDefault string `long:"with-default" default:"default-value" description:"With default"` + WithoutDefault string `long:"without-default" description:"Without default"` + WithProgrammaticDefault string `long:"with-programmatic-default" description:"With programmatic default"` + } + + opts.WithProgrammaticDefault = "default-value" + + p := NewNamedParser("TestHelpDefaults", HelpFlag) + p.AddGroup("Application Options", "The application options", &opts) + + _, err := p.ParseArgs(test.Args) + + if err == nil { + t.Fatalf("Expected help error") + } + + if e, ok := err.(*Error); !ok { + t.Fatalf("Expected flags.Error, but got %T", err) + } else { + if e.Type != ErrHelp { + t.Errorf("Expected flags.ErrHelp type, but got %s", e.Type) + } + + assertDiff(t, e.Message, test.Output, "help message") + } + } +} diff --git a/option.go b/option.go index 73c24f5..068ea32 100644 --- a/option.go +++ b/option.go @@ -76,6 +76,8 @@ type Option struct { tag multiTag isSet bool + + defaultLiteral string } // LongNameWithNamespace returns the option's long name with the group namespaces diff --git a/option_private.go b/option_private.go index 06fc206..ab63ba2 100644 --- a/option_private.go +++ b/option_private.go @@ -203,3 +203,38 @@ func (option *Option) call(value *string) error { return nil } + +func (option *Option) updateDefaultLiteral() { + defs := option.Default + def := "" + + if len(defs) == 0 && option.canArgument() { + var showdef bool + + switch option.field.Type.Kind() { + case reflect.Func, reflect.Ptr: + showdef = !option.value.IsNil() + case reflect.Slice, reflect.String, reflect.Array: + showdef = option.value.Len() > 0 + case reflect.Map: + showdef = !option.value.IsNil() && option.value.Len() > 0 + default: + zeroval := reflect.Zero(option.field.Type) + showdef = !reflect.DeepEqual(zeroval.Interface(), option.value.Interface()) + } + + if showdef { + def, _ = convertToString(option.value, option.tag) + } + } else if len(defs) != 0 { + l := len(defs) - 1 + + for i := 0; i < l; i++ { + def += quoteIfNeeded(defs[i]) + ", " + } + + def += quoteIfNeeded(defs[l]) + } + + option.defaultLiteral = def +} diff --git a/parser.go b/parser.go index d403a40..928f225 100644 --- a/parser.go +++ b/parser.go @@ -170,7 +170,10 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { return nil, p.internalError } - p.clearIsSet() + p.eachOption(func(c *Command, g *Group, option *Option) { + option.isSet = false + option.updateDefaultLiteral() + }) // Add built-in help group to all commands if necessary if (p.Options & HelpFlag) != None { @@ -254,17 +257,13 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { } if s.err == nil { - p.eachCommand(func(c *Command) { - c.eachGroup(func(g *Group) { - for _, option := range g.options { - if option.isSet { - continue - } - - option.clearDefault() - } - }) - }, true) + p.eachOption(func(c *Command, g *Group, option *Option) { + if option.isSet { + return + } + + option.clearDefault() + }) s.checkRequired(p) } From 5631e097ad540fad196795a4efad463d3060fbbd Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sat, 10 Oct 2015 21:16:04 +0200 Subject: [PATCH 17/38] Improve documentation of 'optional' --- flags.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flags.go b/flags.go index 6bf736b..641fe01 100644 --- a/flags.go +++ b/flags.go @@ -84,7 +84,9 @@ The following is a list of tags for struct fields supported by go-flags: displayed in generated man pages (optional) no-flag: if non-empty this field is ignored as an option (optional) - optional: whether an argument of the option is optional (optional) + optional: whether an argument of the option is optional. When an + argument is optional it can only be specified using + --option=argument (optional) optional-value: the value of an optional option when the option occurs without an argument. This tag can be specified multiple times in the case of maps or slices (optional) From eff4073cbc4904bce7b38a3fd661b0e0004874fd Mon Sep 17 00:00:00 2001 From: Travis Cline Date: Fri, 29 May 2015 22:34:43 -0700 Subject: [PATCH 18/38] Output a blank .TP section after usage to fix rendering issue --- help_test.go | 2 +- man.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/help_test.go b/help_test.go index 5dbffb6..01792ea 100644 --- a/help_test.go +++ b/help_test.go @@ -254,7 +254,7 @@ A command Longer \fBcommand\fP description \fBUsage\fP: TestMan [OPTIONS] command [command-OPTIONS] - +.TP \fBAliases\fP: cm, cmd diff --git a/man.go b/man.go index 95347d0..cd4cadd 100644 --- a/man.go +++ b/man.go @@ -141,7 +141,7 @@ func writeManPageCommand(wr io.Writer, name string, root *Command, command *Comm } if len(usage) > 0 { - fmt.Fprintf(wr, "\n\\fBUsage\\fP: %s %s\n\n", manQuote(pre), manQuote(usage)) + fmt.Fprintf(wr, "\n\\fBUsage\\fP: %s %s\n.TP\n", manQuote(pre), manQuote(usage)) } if len(command.Aliases) > 0 { From 2e3d1bd2e1fad797cf2f2fae20cc504998eb93f9 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Wed, 14 Oct 2015 23:05:37 +0200 Subject: [PATCH 19/38] Lookup options in all parent commands --- command_private.go | 13 +++++--- command_test.go | 80 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/command_private.go b/command_private.go index 3ae7247..c0d6ce3 100644 --- a/command_private.go +++ b/command_private.go @@ -156,18 +156,21 @@ func (c *Command) makeLookup() lookup { parent := c.parent + var parents []*Command + for parent != nil { if cmd, ok := parent.(*Command); ok { - cmd.fillLookup(&ret, true) - } - - if grp, ok := parent.(*Group); ok { - parent = grp + parents = append(parents, cmd) + parent = cmd.parent } else { parent = nil } } + for i := len(parents) - 1; i >= 0; i-- { + parents[i].fillLookup(&ret, true) + } + c.fillLookup(&ret, false) return ret } diff --git a/command_test.go b/command_test.go index 1d904ae..e64ac45 100644 --- a/command_test.go +++ b/command_test.go @@ -106,6 +106,34 @@ func TestCommandFlagOrder2(t *testing.T) { } } +func TestCommandFlagOrderSub(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Command struct { + G bool `short:"g"` + + SubCommand struct { + B bool `short:"b"` + } `command:"sub"` + } `command:"cmd"` + }{} + + assertParseSuccess(t, &opts, "cmd", "sub", "-v", "-g", "-b") + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if !opts.Command.G { + t.Errorf("Expected Command.G to be true") + } + + if !opts.Command.SubCommand.B { + t.Errorf("Expected Command.SubCommand.B to be true") + } +} + func TestCommandFlagOverride1(t *testing.T) { var opts = struct { Value bool `short:"v"` @@ -146,6 +174,58 @@ func TestCommandFlagOverride2(t *testing.T) { } } +func TestCommandFlagOverrideSub(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Command struct { + Value bool `short:"v"` + + SubCommand struct { + Value bool `short:"v"` + } `command:"sub"` + } `command:"cmd"` + }{} + + assertParseSuccess(t, &opts, "cmd", "sub", "-v") + + if opts.Value { + t.Errorf("Expected Value to be false") + } + + if opts.Command.Value { + t.Errorf("Expected Command.Value to be false") + } + + if !opts.Command.SubCommand.Value { + t.Errorf("Expected Command.Value to be true") + } +} + +func TestCommandFlagOverrideSub2(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Command struct { + Value bool `short:"v"` + + SubCommand struct { + G bool `short:"g"` + } `command:"sub"` + } `command:"cmd"` + }{} + + assertParseSuccess(t, &opts, "cmd", "sub", "-v") + + if opts.Value { + t.Errorf("Expected Value to be false") + } + + if !opts.Command.Value { + t.Errorf("Expected Command.Value to be true") + } +} + func TestCommandEstimate(t *testing.T) { var opts = struct { Value bool `short:"v"` From 741647f1207ec4d4cabaedc03ead54929663c363 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 18 Oct 2015 13:01:18 +0200 Subject: [PATCH 20/38] Remove help flag from rest args Fixes #107. --- help_test.go | 17 +++++++++++++++++ parser.go | 10 +++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/help_test.go b/help_test.go index 01792ea..e31ac25 100644 --- a/help_test.go +++ b/help_test.go @@ -391,3 +391,20 @@ Help Options: } } } + +func TestHelpRestArgs(t *testing.T) { + opts := struct { + Verbose bool `short:"v"` + }{} + + p := NewNamedParser("TestHelpDefaults", HelpFlag) + p.AddGroup("Application Options", "The application options", &opts) + + retargs, err := p.ParseArgs([]string{"-h", "-v", "rest"}) + + if err == nil { + t.Fatalf("Expected help error") + } + + assertStringArray(t, retargs, []string{"-v", "rest"}) +} diff --git a/parser.go b/parser.go index 928f225..df753ca 100644 --- a/parser.go +++ b/parser.go @@ -279,7 +279,15 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { } if reterr != nil { - return append([]string{s.arg}, s.args...), p.printError(reterr) + var retargs []string + + if ourErr, ok := reterr.(*Error); !ok || ourErr.Type != ErrHelp { + retargs = append([]string{s.arg}, s.args...) + } else { + retargs = s.args + } + + return retargs, p.printError(reterr) } return s.retargs, nil From 85383eb98ece8ebf55f152b3b85ae4bd8b16ca0f Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 18 Oct 2015 15:11:43 +0200 Subject: [PATCH 21/38] Allow customized completion handling --- completion.go | 14 +++++--------- parser.go | 14 ++++++++++---- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/completion.go b/completion.go index d0adfe0..894f1d6 100644 --- a/completion.go +++ b/completion.go @@ -43,8 +43,6 @@ type Completer interface { type completion struct { parser *Parser - - ShowDescriptions bool } // Filename is a string alias which provides filename completion. @@ -275,19 +273,17 @@ func (c *completion) complete(args []string) []Completion { return ret } -func (c *completion) execute(args []string) { - ret := c.complete(args) - - if c.ShowDescriptions && len(ret) > 1 { +func (c *completion) print(items []Completion, showDescriptions bool) { + if showDescriptions && len(items) > 1 { maxl := 0 - for _, v := range ret { + for _, v := range items { if len(v.Item) > maxl { maxl = len(v.Item) } } - for _, v := range ret { + for _, v := range items { fmt.Printf("%s", v.Item) if len(v.Description) > 0 { @@ -297,7 +293,7 @@ func (c *completion) execute(args []string) { fmt.Printf("\n") } } else { - for _, v := range ret { + for _, v := range items { fmt.Println(v.Item) } } diff --git a/parser.go b/parser.go index df753ca..9661af3 100644 --- a/parser.go +++ b/parser.go @@ -32,6 +32,11 @@ type Parser struct { // or an error to indicate a parse failure. UnknownOptionHandler func(option string, arg SplitArgument, args []string) ([]string, error) + // CompletionHandler is a function gets called to handle the completion of + // items. By default, the items are printed and the application is exited. + // You can override this default behavior by specifying a custom CompletionHandler. + CompletionHandler func(items []Completion) + internalError error } @@ -184,13 +189,14 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { if len(compval) != 0 { comp := &completion{parser: p} + items := comp.complete(args) - if compval == "verbose" { - comp.ShowDescriptions = true + if p.CompletionHandler != nil { + p.CompletionHandler(items) + } else { + comp.print(items, compval == "verbose") } - comp.execute(args) - return nil, nil } From ded79de36f56fec45af2b4c34166b6e4adc77d3f Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 18 Oct 2015 15:12:19 +0200 Subject: [PATCH 22/38] Exit after completion by default This behavior can be overridden by installing a custom completion handler --- completion_test.go | 5 +++++ parser.go | 1 + 2 files changed, 6 insertions(+) diff --git a/completion_test.go b/completion_test.go index 2d5a97f..f440fd7 100644 --- a/completion_test.go +++ b/completion_test.go @@ -268,6 +268,11 @@ func TestParserCompletion(t *testing.T) { p := NewParser(&completionTestOptions, None) + p.CompletionHandler = func(items []Completion) { + comp := &completion{parser: p} + comp.print(items, test.ShowDescriptions) + } + _, err := p.ParseArgs(test.Args) w.Close() diff --git a/parser.go b/parser.go index 9661af3..8dedd0e 100644 --- a/parser.go +++ b/parser.go @@ -195,6 +195,7 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { p.CompletionHandler(items) } else { comp.print(items, compval == "verbose") + os.Exit(0) } return nil, nil From 6e4f4168f10c0d4076872b2aeeb749de2e6ded4c Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 18 Oct 2015 17:33:16 +0200 Subject: [PATCH 23/38] Make wrapText newline aware --- convert.go | 36 ---------------------------------- convert_test.go | 16 --------------- help.go | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ help_test.go | 40 +++++++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 52 deletions(-) diff --git a/convert.go b/convert.go index 1185737..938c3ac 100644 --- a/convert.go +++ b/convert.go @@ -339,39 +339,3 @@ func unquoteIfPossible(s string) (string, error) { return strconv.Unquote(s) } - -func wrapText(s string, l int, prefix string) string { - // Basic text wrapping of s at spaces to fit in l - var ret string - - s = strings.TrimSpace(s) - - for len(s) > l { - // Try to split on space - suffix := "" - - pos := strings.LastIndex(s[:l], " ") - - if pos < 0 { - pos = l - 1 - suffix = "-\n" - } - - if len(ret) != 0 { - ret += "\n" + prefix - } - - ret += strings.TrimSpace(s[:pos]) + suffix - s = strings.TrimSpace(s[pos:]) - } - - if len(s) > 0 { - if len(ret) != 0 { - ret += "\n" + prefix - } - - return ret + s - } - - return ret -} diff --git a/convert_test.go b/convert_test.go index 0de0eea..ef131dc 100644 --- a/convert_test.go +++ b/convert_test.go @@ -157,19 +157,3 @@ func TestConvertToStringInvalidUintBase(t *testing.T) { assertError(t, err, ErrMarshal, "strconv.ParseInt: parsing \"no\": invalid syntax") } - -func TestWrapText(t *testing.T) { - s := "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." - - got := wrapText(s, 60, " ") - expected := `Lorem ipsum dolor sit amet, consectetur adipisicing elit, - sed do eiusmod tempor incididunt ut labore et dolore magna - aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. - Duis aute irure dolor in reprehenderit in voluptate velit - esse cillum dolore eu fugiat nulla pariatur. Excepteur sint - occaecat cupidatat non proident, sunt in culpa qui officia - deserunt mollit anim id est laborum.` - - assertDiff(t, got, expected, "wrapped text") -} diff --git a/help.go b/help.go index 5abe257..8035ef5 100644 --- a/help.go +++ b/help.go @@ -104,6 +104,58 @@ func (p *Parser) getAlignmentInfo() alignmentInfo { return ret } +func wrapText(s string, l int, prefix string) string { + var ret string + + // Basic text wrapping of s at spaces to fit in l + lines := strings.Split(s, "\n") + + for _, line := range lines { + var retline string + + line = strings.TrimSpace(line) + + for len(line) > l { + // Try to split on space + suffix := "" + + pos := strings.LastIndex(line[:l], " ") + + if pos < 0 { + pos = l - 1 + suffix = "-\n" + } + + if len(retline) != 0 { + retline += "\n" + prefix + } + + retline += strings.TrimSpace(line[:pos]) + suffix + line = strings.TrimSpace(line[pos:]) + } + + if len(line) > 0 { + if len(retline) != 0 { + retline += "\n" + prefix + } + + retline += line + } + + if len(ret) > 0 { + ret += "\n" + + if len(retline) > 0 { + ret += prefix + } + } + + ret += retline + } + + return ret +} + func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alignmentInfo) { line := &bytes.Buffer{} diff --git a/help_test.go b/help_test.go index e31ac25..33f67a3 100644 --- a/help_test.go +++ b/help_test.go @@ -408,3 +408,43 @@ func TestHelpRestArgs(t *testing.T) { assertStringArray(t, retargs, []string{"-v", "rest"}) } + +func TestWrapText(t *testing.T) { + s := "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + + got := wrapText(s, 60, " ") + expected := `Lorem ipsum dolor sit amet, consectetur adipisicing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit + esse cillum dolore eu fugiat nulla pariatur. Excepteur sint + occaecat cupidatat non proident, sunt in culpa qui officia + deserunt mollit anim id est laborum.` + + assertDiff(t, got, expected, "wrapped text") +} + +func TestWrapParagraph(t *testing.T) { + s := "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n\n" + s += "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n\n" + s += "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n\n" + s += "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n" + + got := wrapText(s, 60, " ") + expected := `Lorem ipsum dolor sit amet, consectetur adipisicing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. + + Ut enim ad minim veniam, quis nostrud exercitation ullamco + laboris nisi ut aliquip ex ea commodo consequat. + + Duis aute irure dolor in reprehenderit in voluptate velit + esse cillum dolore eu fugiat nulla pariatur. + + Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum. +` + + assertDiff(t, got, expected, "wrapped paragraph") +} From a15da34f6528f112b097bb6caebae29d7cd9d4b7 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 18 Oct 2015 18:19:40 +0200 Subject: [PATCH 24/38] Set and respect Hidden on group --- command.go | 3 --- command_private.go | 23 +++++++++++++++-------- group.go | 3 +++ group_private.go | 1 + help.go | 12 +++++++----- man.go | 14 +++++++++++--- option.go | 6 +++--- parser_private.go | 2 +- 8 files changed, 41 insertions(+), 23 deletions(-) diff --git a/command.go b/command.go index f8579d2..13332ae 100644 --- a/command.go +++ b/command.go @@ -23,9 +23,6 @@ type Command struct { // Whether positional arguments are required ArgsRequired bool - // If true, the option is not displayed in the help output - Hidden bool - commands []*Command hasBuiltinHelpGroup bool args []*Arg diff --git a/command_private.go b/command_private.go index 0a77ab2..f2a2435 100644 --- a/command_private.go +++ b/command_private.go @@ -79,6 +79,8 @@ func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler { subc, err := c.AddCommand(subcommand, shortDescription, longDescription, ptrval.Interface()) + subc.Hidden = mtag.Get("hidden") != "" + if err != nil { return true, err } @@ -235,18 +237,23 @@ func (c commandList) Swap(i, j int) { c[i], c[j] = c[j], c[i] } -// FIXME: maybe call this sortedVisibleCommands ? -func (c *Command) sortedCommands() []*Command { - ret := make(commandList, 0, len(c.commands)) +func (c *Command) sortedVisibleCommands() []*Command { + ret := commandList(c.visibleCommands()) + sort.Sort(ret) + + return []*Command(ret) +} - for _, e := range c.commands { - if !e.Hidden { - ret = append(ret, e) +func (c *Command) visibleCommands() []*Command { + ret := make([]*Command, 0, len(c.commands)) + + for _, cmd := range c.commands { + if !cmd.Hidden { + ret = append(ret, cmd) } } - sort.Sort(ret) - return []*Command(ret) + return ret } func (c *Command) match(name string) bool { diff --git a/group.go b/group.go index 8b609a3..0e74f92 100644 --- a/group.go +++ b/group.go @@ -32,6 +32,9 @@ type Group struct { // The namespace of the group Namespace string + // If true, the group is not displayed in the help or man page + Hidden bool + // The parent of the group or nil if it has no parent parent interface{} diff --git a/group_private.go b/group_private.go index 96c3619..b54bfd2 100644 --- a/group_private.go +++ b/group_private.go @@ -211,6 +211,7 @@ func (g *Group) scanSubGroupHandler(realval reflect.Value, sfield *reflect.Struc } group.Namespace = mtag.Get("namespace") + group.Hidden = mtag.Get("hidden") != "" return true, nil } diff --git a/help.go b/help.go index fab5450..c0b808d 100644 --- a/help.go +++ b/help.go @@ -340,10 +340,12 @@ func (p *Parser) WriteHelp(writer io.Writer) { co, cc = "<", ">" } - if len(allcmd.commands) > 3 { + visibleCommands := allcmd.visibleCommands() + + if len(visibleCommands) > 3 { fmt.Fprintf(wr, " %scommand%s", co, cc) } else { - subcommands := allcmd.sortedCommands() + subcommands := allcmd.sortedVisibleCommands() names := make([]string, len(subcommands)) for i, subc := range subcommands { @@ -380,12 +382,12 @@ func (p *Parser) WriteHelp(writer io.Writer) { // Skip built-in help group for all commands except the top-level // parser - if grp.isBuiltinHelp && c != p.Command { + if grp.Hidden || (grp.isBuiltinHelp && c != p.Command) { return } for _, info := range grp.options { - if !info.canCli() { + if !info.canCli() || info.Hidden { continue } @@ -435,7 +437,7 @@ func (p *Parser) WriteHelp(writer io.Writer) { c = c.Active } - scommands := cmd.sortedCommands() + scommands := cmd.sortedVisibleCommands() if len(scommands) > 0 { maxnamelen := maxCommandLength(scommands) diff --git a/man.go b/man.go index cd4cadd..5529d92 100644 --- a/man.go +++ b/man.go @@ -38,8 +38,12 @@ func formatForMan(wr io.Writer, s string) { func writeManPageOptions(wr io.Writer, grp *Group) { grp.eachGroup(func(group *Group) { + if grp.Hidden { + return + } + for _, opt := range group.options { - if !opt.canCli() { + if !opt.canCli() || opt.Hidden { continue } @@ -91,11 +95,15 @@ func writeManPageOptions(wr io.Writer, grp *Group) { } func writeManPageSubcommands(wr io.Writer, name string, root *Command) { - commands := root.sortedCommands() + commands := root.sortedVisibleCommands() for _, c := range commands { var nn string + if c.Hidden { + continue + } + if len(name) != 0 { nn = name + " " + c.Name } else { @@ -178,7 +186,7 @@ func (p *Parser) WriteManPage(wr io.Writer) { writeManPageOptions(wr, p.Command.Group) - if len(p.commands) > 0 { + if len(p.visibleCommands()) > 0 { fmt.Fprintln(wr, ".SH COMMANDS") writeManPageSubcommands(wr, "", p.Command) diff --git a/option.go b/option.go index afad7e4..8764e25 100644 --- a/option.go +++ b/option.go @@ -51,9 +51,6 @@ type Option struct { // error. Required bool - // If true, the option is not displayed in the help output - Hidden bool - // A name for the value of an option shown in the Help as --flag [ValueName] ValueName string @@ -65,6 +62,9 @@ type Option struct { // If non empty, only a certain set of values is allowed for an option. Choices []string + // If true, the option is not displayed in the help or man page + Hidden bool + // The group which the option belongs to group *Group diff --git a/parser_private.go b/parser_private.go index 76be4a7..7469c03 100644 --- a/parser_private.go +++ b/parser_private.go @@ -114,7 +114,7 @@ func (p *parseState) checkRequired(parser *Parser) error { } func (p *parseState) estimateCommand() error { - commands := p.command.sortedCommands() + commands := p.command.sortedVisibleCommands() cmdnames := make([]string, len(commands)) for i, v := range commands { From 2e55b6ddb7a9a3eb4276b5f0ddb562df89e25807 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 18 Oct 2015 18:29:58 +0200 Subject: [PATCH 25/38] Add hidden tests --- help_test.go | 12 +++++++++++- ini_private.go | 10 ++++++++-- man.go | 2 +- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/help_test.go b/help_test.go index 33f67a3..33d21bf 100644 --- a/help_test.go +++ b/help_test.go @@ -22,6 +22,7 @@ type helpOptions struct { EnvDefault2 string `long:"env-default2" env:"ENV_DEFAULT" description:"Test env-default2 value"` OptionWithArgName string `long:"opt-with-arg-name" value-name:"something" description:"Option with named argument"` OptionWithChoices string `long:"opt-with-choices" value-name:"choice" choice:"dog" choice:"cat" description:"Option with choices"` + Hidden string `long:"hidden" description:"Hidden option" hidden:"yes"` OnlyIni string `ini-name:"only-ini" description:"Option only available in ini"` @@ -30,8 +31,13 @@ type helpOptions struct { IntMap map[string]int `long:"intmap" default:"a:1" description:"A map from string to int" ini-name:"int-map"` } `group:"Other Options"` + HiddenGroup struct { + InsideHiddenGroup string `long:"inside-hidden-group" description:"Inside hidden group"` + } `group:"Hidden group" hidden:"yes"` + Group struct { - Opt string `long:"opt" description:"This is a subgroup option"` + Opt string `long:"opt" description:"This is a subgroup option"` + HiddenInsideGroup string `long:"hidden-inside-group" description:"Hidden inside group" hidden:"yes"` Group struct { Opt string `long:"opt" description:"This is a subsubgroup option"` @@ -42,6 +48,10 @@ type helpOptions struct { ExtraVerbose []bool `long:"extra-verbose" description:"Use for extra verbosity"` } `command:"command" alias:"cm" alias:"cmd" description:"A command"` + HiddenCommand struct { + ExtraVerbose []bool `long:"extra-verbose" description:"Use for extra verbosity"` + } `command:"hidden-command" description:"A hidden command" hidden:"yes"` + Args struct { Filename string `positional-arg-name:"filename" description:"A filename"` Number int `positional-arg-name:"num" description:"A number"` diff --git a/ini_private.go b/ini_private.go index 887aa76..45434ec 100644 --- a/ini_private.go +++ b/ini_private.go @@ -83,7 +83,7 @@ func writeGroupIni(cmd *Command, group *Group, namespace string, writer io.Write comments := (options & IniIncludeComments) != IniNone for _, option := range group.options { - if option.isFunc() { + if option.isFunc() || option.Hidden { continue } @@ -186,12 +186,18 @@ func writeOption(writer io.Writer, optionName string, optionType reflect.Kind, o func writeCommandIni(command *Command, namespace string, writer io.Writer, options IniOptions) { command.eachGroup(func(group *Group) { - writeGroupIni(command, group, namespace, writer, options) + if !group.Hidden { + writeGroupIni(command, group, namespace, writer, options) + } }) for _, c := range command.commands { var nns string + if c.Hidden { + continue + } + if len(namespace) != 0 { nns = c.Name + "." + nns } else { diff --git a/man.go b/man.go index 5529d92..8e4a8b7 100644 --- a/man.go +++ b/man.go @@ -38,7 +38,7 @@ func formatForMan(wr io.Writer, s string) { func writeManPageOptions(wr io.Writer, grp *Group) { grp.eachGroup(func(group *Group) { - if grp.Hidden { + if group.Hidden { return } From 249769099d35608bc822cd221e82baa5aa4ae59f Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 18 Oct 2015 19:42:44 +0200 Subject: [PATCH 26/38] Clear slice and map option values before set the first time Fixes #121. --- ini_test.go | 48 +++++++++++++++++++++++++++++++++++++++++++++++ option_private.go | 6 ++++++ 2 files changed, 54 insertions(+) diff --git a/ini_test.go b/ini_test.go index aa3b556..c0073e7 100644 --- a/ini_test.go +++ b/ini_test.go @@ -672,6 +672,54 @@ func TestIniParse(t *testing.T) { } } +func TestIniCliOverrides(t *testing.T) { + file, err := ioutil.TempFile("", "") + + if err != nil { + t.Fatalf("Cannot create temporary file: %s", err) + } + + defer os.Remove(file.Name()) + + _, err = file.WriteString("values = 123\n") + _, err = file.WriteString("values = 456\n") + + if err != nil { + t.Fatalf("Cannot write to temporary file: %s", err) + } + + file.Close() + + var opts struct { + Values []int `long:"values"` + } + + p := NewParser(&opts, Default) + err = NewIniParser(p).ParseFile(file.Name()) + + if err != nil { + t.Fatalf("Could not parse ini: %s", err) + } + + _, err = p.ParseArgs([]string{"--values", "111", "--values", "222"}) + + if err != nil { + t.Fatalf("Failed to parse arguments: %s", err) + } + + if len(opts.Values) != 2 { + t.Fatalf("Expected Values to contain two elements, but got %d", len(opts.Values)) + } + + if opts.Values[0] != 111 { + t.Fatalf("Expected Values[0] to be 111, but got '%d'", opts.Values[0]) + } + + if opts.Values[1] != 222 { + t.Fatalf("Expected Values[0] to be 222, but got '%d'", opts.Values[1]) + } +} + func TestWriteFile(t *testing.T) { file, err := ioutil.TempFile("", "") if err != nil { diff --git a/option_private.go b/option_private.go index ab63ba2..444334e 100644 --- a/option_private.go +++ b/option_private.go @@ -10,6 +10,12 @@ import ( // if the specified value could not be converted to the corresponding option // value type. func (option *Option) set(value *string) error { + kind := option.value.Type().Kind() + + if (kind == reflect.Map || kind == reflect.Slice) && !option.isSet { + option.empty() + } + option.isSet = true if len(option.Choices) != 0 { From 05e133dfea4b5604e8437eee652f3761e8cf1317 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 18 Oct 2015 22:25:23 +0200 Subject: [PATCH 27/38] Merge _private files --- command.go | 296 +++++++++++++++++++++++++++++ command_private.go | 297 ----------------------------- group.go | 255 +++++++++++++++++++++++++ group_private.go | 259 ------------------------- ini.go | 453 ++++++++++++++++++++++++++++++++++++++++++++ ini_private.go | 458 --------------------------------------------- option.go | 241 ++++++++++++++++++++++++ option_private.go | 246 ------------------------ parser.go | 335 +++++++++++++++++++++++++++++++++ parser_private.go | 340 --------------------------------- 10 files changed, 1580 insertions(+), 1600 deletions(-) delete mode 100644 command_private.go delete mode 100644 group_private.go delete mode 100644 ini_private.go delete mode 100644 option_private.go delete mode 100644 parser_private.go diff --git a/command.go b/command.go index 13332ae..9184aec 100644 --- a/command.go +++ b/command.go @@ -1,5 +1,12 @@ package flags +import ( + "reflect" + "sort" + "strings" + "unsafe" +) + // Command represents an application command. Commands can be added to the // parser (which itself is a command) and are selected/executed when its name // is specified on the command line. The Command type embeds a Group and @@ -47,6 +54,13 @@ type Usage interface { Usage() string } +type lookup struct { + shortNames map[string]*Option + longNames map[string]*Option + + commands map[string]*Command +} + // AddCommand adds a new command to the parser with the given name and data. The // data needs to be a pointer to a struct from which the fields indicate which // options are in the command. The provided data can implement the Command and @@ -104,3 +118,285 @@ func (c *Command) Args() []*Arg { return ret } + +func newCommand(name string, shortDescription string, longDescription string, data interface{}) *Command { + return &Command{ + Group: newGroup(shortDescription, longDescription, data), + Name: name, + } +} + +func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler { + f := func(realval reflect.Value, sfield *reflect.StructField) (bool, error) { + mtag := newMultiTag(string(sfield.Tag)) + + if err := mtag.Parse(); err != nil { + return true, err + } + + positional := mtag.Get("positional-args") + + if len(positional) != 0 { + stype := realval.Type() + + for i := 0; i < stype.NumField(); i++ { + field := stype.Field(i) + + m := newMultiTag((string(field.Tag))) + + if err := m.Parse(); err != nil { + return true, err + } + + name := m.Get("positional-arg-name") + + if len(name) == 0 { + name = field.Name + } + + arg := &Arg{ + Name: name, + Description: m.Get("description"), + + value: realval.Field(i), + tag: m, + } + + c.args = append(c.args, arg) + + if len(mtag.Get("required")) != 0 { + c.ArgsRequired = true + } + } + + return true, nil + } + + subcommand := mtag.Get("command") + + if len(subcommand) != 0 { + ptrval := reflect.NewAt(realval.Type(), unsafe.Pointer(realval.UnsafeAddr())) + + shortDescription := mtag.Get("description") + longDescription := mtag.Get("long-description") + subcommandsOptional := mtag.Get("subcommands-optional") + aliases := mtag.GetMany("alias") + + subc, err := c.AddCommand(subcommand, shortDescription, longDescription, ptrval.Interface()) + + subc.Hidden = mtag.Get("hidden") != "" + + if err != nil { + return true, err + } + + if len(subcommandsOptional) > 0 { + subc.SubcommandsOptional = true + } + + if len(aliases) > 0 { + subc.Aliases = aliases + } + + return true, nil + } + + return parentg.scanSubGroupHandler(realval, sfield) + } + + return f +} + +func (c *Command) scan() error { + return c.scanType(c.scanSubcommandHandler(c.Group)) +} + +func (c *Command) eachOption(f func(*Command, *Group, *Option)) { + c.eachCommand(func(c *Command) { + c.eachGroup(func(g *Group) { + for _, option := range g.options { + f(c, g, option) + } + }) + }, true) +} + +func (c *Command) eachCommand(f func(*Command), recurse bool) { + f(c) + + for _, cc := range c.commands { + if recurse { + cc.eachCommand(f, true) + } else { + f(cc) + } + } +} + +func (c *Command) eachActiveGroup(f func(cc *Command, g *Group)) { + c.eachGroup(func(g *Group) { + f(c, g) + }) + + if c.Active != nil { + c.Active.eachActiveGroup(f) + } +} + +func (c *Command) addHelpGroups(showHelp func() error) { + if !c.hasBuiltinHelpGroup { + c.addHelpGroup(showHelp) + c.hasBuiltinHelpGroup = true + } + + for _, cc := range c.commands { + cc.addHelpGroups(showHelp) + } +} + +func (c *Command) makeLookup() lookup { + ret := lookup{ + shortNames: make(map[string]*Option), + longNames: make(map[string]*Option), + commands: make(map[string]*Command), + } + + parent := c.parent + + var parents []*Command + + for parent != nil { + if cmd, ok := parent.(*Command); ok { + parents = append(parents, cmd) + parent = cmd.parent + } else { + parent = nil + } + } + + for i := len(parents) - 1; i >= 0; i-- { + parents[i].fillLookup(&ret, true) + } + + c.fillLookup(&ret, false) + return ret +} + +func (c *Command) fillLookup(ret *lookup, onlyOptions bool) { + c.eachGroup(func(g *Group) { + for _, option := range g.options { + if option.ShortName != 0 { + ret.shortNames[string(option.ShortName)] = option + } + + if len(option.LongName) > 0 { + ret.longNames[option.LongNameWithNamespace()] = option + } + } + }) + + if onlyOptions { + return + } + + for _, subcommand := range c.commands { + ret.commands[subcommand.Name] = subcommand + + for _, a := range subcommand.Aliases { + ret.commands[a] = subcommand + } + } +} + +func (c *Command) groupByName(name string) *Group { + if grp := c.Group.groupByName(name); grp != nil { + return grp + } + + for _, subc := range c.commands { + prefix := subc.Name + "." + + if strings.HasPrefix(name, prefix) { + if grp := subc.groupByName(name[len(prefix):]); grp != nil { + return grp + } + } else if name == subc.Name { + return subc.Group + } + } + + return nil +} + +type commandList []*Command + +func (c commandList) Less(i, j int) bool { + return c[i].Name < c[j].Name +} + +func (c commandList) Len() int { + return len(c) +} + +func (c commandList) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +} + +func (c *Command) sortedVisibleCommands() []*Command { + ret := commandList(c.visibleCommands()) + sort.Sort(ret) + + return []*Command(ret) +} + +func (c *Command) visibleCommands() []*Command { + ret := make([]*Command, 0, len(c.commands)) + + for _, cmd := range c.commands { + if !cmd.Hidden { + ret = append(ret, cmd) + } + } + + return ret +} + +func (c *Command) match(name string) bool { + if c.Name == name { + return true + } + + for _, v := range c.Aliases { + if v == name { + return true + } + } + + return false +} + +func (c *Command) hasCliOptions() bool { + ret := false + + c.eachGroup(func(g *Group) { + if g.isBuiltinHelp { + return + } + + for _, opt := range g.options { + if opt.canCli() { + ret = true + } + } + }) + + return ret +} + +func (c *Command) fillParseState(s *parseState) { + s.positional = make([]*Arg, len(c.args)) + copy(s.positional, c.args) + + s.lookup = c.makeLookup() + s.command = c +} diff --git a/command_private.go b/command_private.go deleted file mode 100644 index f2a2435..0000000 --- a/command_private.go +++ /dev/null @@ -1,297 +0,0 @@ -package flags - -import ( - "reflect" - "sort" - "strings" - "unsafe" -) - -type lookup struct { - shortNames map[string]*Option - longNames map[string]*Option - - commands map[string]*Command -} - -func newCommand(name string, shortDescription string, longDescription string, data interface{}) *Command { - return &Command{ - Group: newGroup(shortDescription, longDescription, data), - Name: name, - } -} - -func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler { - f := func(realval reflect.Value, sfield *reflect.StructField) (bool, error) { - mtag := newMultiTag(string(sfield.Tag)) - - if err := mtag.Parse(); err != nil { - return true, err - } - - positional := mtag.Get("positional-args") - - if len(positional) != 0 { - stype := realval.Type() - - for i := 0; i < stype.NumField(); i++ { - field := stype.Field(i) - - m := newMultiTag((string(field.Tag))) - - if err := m.Parse(); err != nil { - return true, err - } - - name := m.Get("positional-arg-name") - - if len(name) == 0 { - name = field.Name - } - - arg := &Arg{ - Name: name, - Description: m.Get("description"), - - value: realval.Field(i), - tag: m, - } - - c.args = append(c.args, arg) - - if len(mtag.Get("required")) != 0 { - c.ArgsRequired = true - } - } - - return true, nil - } - - subcommand := mtag.Get("command") - - if len(subcommand) != 0 { - ptrval := reflect.NewAt(realval.Type(), unsafe.Pointer(realval.UnsafeAddr())) - - shortDescription := mtag.Get("description") - longDescription := mtag.Get("long-description") - subcommandsOptional := mtag.Get("subcommands-optional") - aliases := mtag.GetMany("alias") - - subc, err := c.AddCommand(subcommand, shortDescription, longDescription, ptrval.Interface()) - - subc.Hidden = mtag.Get("hidden") != "" - - if err != nil { - return true, err - } - - if len(subcommandsOptional) > 0 { - subc.SubcommandsOptional = true - } - - if len(aliases) > 0 { - subc.Aliases = aliases - } - - return true, nil - } - - return parentg.scanSubGroupHandler(realval, sfield) - } - - return f -} - -func (c *Command) scan() error { - return c.scanType(c.scanSubcommandHandler(c.Group)) -} - -func (c *Command) eachOption(f func(*Command, *Group, *Option)) { - c.eachCommand(func(c *Command) { - c.eachGroup(func(g *Group) { - for _, option := range g.options { - f(c, g, option) - } - }) - }, true) -} - -func (c *Command) eachCommand(f func(*Command), recurse bool) { - f(c) - - for _, cc := range c.commands { - if recurse { - cc.eachCommand(f, true) - } else { - f(cc) - } - } -} - -func (c *Command) eachActiveGroup(f func(cc *Command, g *Group)) { - c.eachGroup(func(g *Group) { - f(c, g) - }) - - if c.Active != nil { - c.Active.eachActiveGroup(f) - } -} - -func (c *Command) addHelpGroups(showHelp func() error) { - if !c.hasBuiltinHelpGroup { - c.addHelpGroup(showHelp) - c.hasBuiltinHelpGroup = true - } - - for _, cc := range c.commands { - cc.addHelpGroups(showHelp) - } -} - -func (c *Command) makeLookup() lookup { - ret := lookup{ - shortNames: make(map[string]*Option), - longNames: make(map[string]*Option), - commands: make(map[string]*Command), - } - - parent := c.parent - - var parents []*Command - - for parent != nil { - if cmd, ok := parent.(*Command); ok { - parents = append(parents, cmd) - parent = cmd.parent - } else { - parent = nil - } - } - - for i := len(parents) - 1; i >= 0; i-- { - parents[i].fillLookup(&ret, true) - } - - c.fillLookup(&ret, false) - return ret -} - -func (c *Command) fillLookup(ret *lookup, onlyOptions bool) { - c.eachGroup(func(g *Group) { - for _, option := range g.options { - if option.ShortName != 0 { - ret.shortNames[string(option.ShortName)] = option - } - - if len(option.LongName) > 0 { - ret.longNames[option.LongNameWithNamespace()] = option - } - } - }) - - if onlyOptions { - return - } - - for _, subcommand := range c.commands { - ret.commands[subcommand.Name] = subcommand - - for _, a := range subcommand.Aliases { - ret.commands[a] = subcommand - } - } -} - -func (c *Command) groupByName(name string) *Group { - if grp := c.Group.groupByName(name); grp != nil { - return grp - } - - for _, subc := range c.commands { - prefix := subc.Name + "." - - if strings.HasPrefix(name, prefix) { - if grp := subc.groupByName(name[len(prefix):]); grp != nil { - return grp - } - } else if name == subc.Name { - return subc.Group - } - } - - return nil -} - -type commandList []*Command - -func (c commandList) Less(i, j int) bool { - return c[i].Name < c[j].Name -} - -func (c commandList) Len() int { - return len(c) -} - -func (c commandList) Swap(i, j int) { - c[i], c[j] = c[j], c[i] -} - -func (c *Command) sortedVisibleCommands() []*Command { - ret := commandList(c.visibleCommands()) - sort.Sort(ret) - - return []*Command(ret) -} - -func (c *Command) visibleCommands() []*Command { - ret := make([]*Command, 0, len(c.commands)) - - for _, cmd := range c.commands { - if !cmd.Hidden { - ret = append(ret, cmd) - } - } - - return ret -} - -func (c *Command) match(name string) bool { - if c.Name == name { - return true - } - - for _, v := range c.Aliases { - if v == name { - return true - } - } - - return false -} - -func (c *Command) hasCliOptions() bool { - ret := false - - c.eachGroup(func(g *Group) { - if g.isBuiltinHelp { - return - } - - for _, opt := range g.options { - if opt.canCli() { - ret = true - } - } - }) - - return ret -} - -func (c *Command) fillParseState(s *parseState) { - s.positional = make([]*Arg, len(c.args)) - copy(s.positional, c.args) - - s.lookup = c.makeLookup() - s.command = c -} diff --git a/group.go b/group.go index 0e74f92..8ab809e 100644 --- a/group.go +++ b/group.go @@ -6,7 +6,10 @@ package flags import ( "errors" + "reflect" "strings" + "unicode/utf8" + "unsafe" ) // ErrNotPointerToStruct indicates that a provided data container is not @@ -50,6 +53,8 @@ type Group struct { data interface{} } +type scanHandler func(reflect.Value, *reflect.StructField) (bool, error) + // AddGroup adds a new group to the command with the given name and data. The // data needs to be a pointer to a struct from which the fields indicate which // options are in the group. @@ -92,3 +97,253 @@ func (g *Group) Find(shortDescription string) *Group { return ret } + +func newGroup(shortDescription string, longDescription string, data interface{}) *Group { + return &Group{ + ShortDescription: shortDescription, + LongDescription: longDescription, + + data: data, + } +} + +func (g *Group) optionByName(name string, namematch func(*Option, string) bool) *Option { + prio := 0 + var retopt *Option + + for _, opt := range g.options { + if namematch != nil && namematch(opt, name) && prio < 4 { + retopt = opt + prio = 4 + } + + if name == opt.field.Name && prio < 3 { + retopt = opt + prio = 3 + } + + if name == opt.LongNameWithNamespace() && prio < 2 { + retopt = opt + prio = 2 + } + + if opt.ShortName != 0 && name == string(opt.ShortName) && prio < 1 { + retopt = opt + prio = 1 + } + } + + return retopt +} + +func (g *Group) eachGroup(f func(*Group)) { + f(g) + + for _, gg := range g.groups { + gg.eachGroup(f) + } +} + +func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, handler scanHandler) error { + stype := realval.Type() + + if sfield != nil { + if ok, err := handler(realval, sfield); err != nil { + return err + } else if ok { + return nil + } + } + + for i := 0; i < stype.NumField(); i++ { + field := stype.Field(i) + + // PkgName is set only for non-exported fields, which we ignore + if field.PkgPath != "" { + continue + } + + mtag := newMultiTag(string(field.Tag)) + + if err := mtag.Parse(); err != nil { + return err + } + + // Skip fields with the no-flag tag + if mtag.Get("no-flag") != "" { + continue + } + + // Dive deep into structs or pointers to structs + kind := field.Type.Kind() + fld := realval.Field(i) + + if kind == reflect.Struct { + if err := g.scanStruct(fld, &field, handler); err != nil { + return err + } + } else if kind == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct { + if fld.IsNil() { + fld.Set(reflect.New(fld.Type().Elem())) + } + + if err := g.scanStruct(reflect.Indirect(fld), &field, handler); err != nil { + return err + } + } + + longname := mtag.Get("long") + shortname := mtag.Get("short") + + // Need at least either a short or long name + if longname == "" && shortname == "" && mtag.Get("ini-name") == "" { + continue + } + + short := rune(0) + rc := utf8.RuneCountInString(shortname) + + if rc > 1 { + return newErrorf(ErrShortNameTooLong, + "short names can only be 1 character long, not `%s'", + shortname) + + } else if rc == 1 { + short, _ = utf8.DecodeRuneInString(shortname) + } + + description := mtag.Get("description") + def := mtag.GetMany("default") + + optionalValue := mtag.GetMany("optional-value") + valueName := mtag.Get("value-name") + defaultMask := mtag.Get("default-mask") + + optional := (mtag.Get("optional") != "") + required := (mtag.Get("required") != "") + choices := mtag.GetMany("choice") + hidden := (mtag.Get("hidden") != "") + + option := &Option{ + Description: description, + ShortName: short, + LongName: longname, + Default: def, + EnvDefaultKey: mtag.Get("env"), + EnvDefaultDelim: mtag.Get("env-delim"), + OptionalArgument: optional, + OptionalValue: optionalValue, + Required: required, + ValueName: valueName, + DefaultMask: defaultMask, + Choices: choices, + Hidden: hidden, + + group: g, + + field: field, + value: realval.Field(i), + tag: mtag, + } + + g.options = append(g.options, option) + } + + return nil +} + +func (g *Group) checkForDuplicateFlags() *Error { + shortNames := make(map[rune]*Option) + longNames := make(map[string]*Option) + + var duplicateError *Error + + g.eachGroup(func(g *Group) { + for _, option := range g.options { + if option.LongName != "" { + longName := option.LongNameWithNamespace() + + if otherOption, ok := longNames[longName]; ok { + duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same long name as option `%s'", option, otherOption) + return + } + longNames[longName] = option + } + if option.ShortName != 0 { + if otherOption, ok := shortNames[option.ShortName]; ok { + duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same short name as option `%s'", option, otherOption) + return + } + shortNames[option.ShortName] = option + } + } + }) + + return duplicateError +} + +func (g *Group) scanSubGroupHandler(realval reflect.Value, sfield *reflect.StructField) (bool, error) { + mtag := newMultiTag(string(sfield.Tag)) + + if err := mtag.Parse(); err != nil { + return true, err + } + + subgroup := mtag.Get("group") + + if len(subgroup) != 0 { + ptrval := reflect.NewAt(realval.Type(), unsafe.Pointer(realval.UnsafeAddr())) + description := mtag.Get("description") + + group, err := g.AddGroup(subgroup, description, ptrval.Interface()) + if err != nil { + return true, err + } + + group.Namespace = mtag.Get("namespace") + group.Hidden = mtag.Get("hidden") != "" + + return true, nil + } + + return false, nil +} + +func (g *Group) scanType(handler scanHandler) error { + // Get all the public fields in the data struct + ptrval := reflect.ValueOf(g.data) + + if ptrval.Type().Kind() != reflect.Ptr { + panic(ErrNotPointerToStruct) + } + + stype := ptrval.Type().Elem() + + if stype.Kind() != reflect.Struct { + panic(ErrNotPointerToStruct) + } + + realval := reflect.Indirect(ptrval) + + if err := g.scanStruct(realval, nil, handler); err != nil { + return err + } + + if err := g.checkForDuplicateFlags(); err != nil { + return err + } + + return nil +} + +func (g *Group) scan() error { + return g.scanType(g.scanSubGroupHandler) +} + +func (g *Group) groupByName(name string) *Group { + if len(name) == 0 { + return g + } + + return g.Find(name) +} diff --git a/group_private.go b/group_private.go deleted file mode 100644 index b54bfd2..0000000 --- a/group_private.go +++ /dev/null @@ -1,259 +0,0 @@ -package flags - -import ( - "reflect" - "unicode/utf8" - "unsafe" -) - -type scanHandler func(reflect.Value, *reflect.StructField) (bool, error) - -func newGroup(shortDescription string, longDescription string, data interface{}) *Group { - return &Group{ - ShortDescription: shortDescription, - LongDescription: longDescription, - - data: data, - } -} - -func (g *Group) optionByName(name string, namematch func(*Option, string) bool) *Option { - prio := 0 - var retopt *Option - - for _, opt := range g.options { - if namematch != nil && namematch(opt, name) && prio < 4 { - retopt = opt - prio = 4 - } - - if name == opt.field.Name && prio < 3 { - retopt = opt - prio = 3 - } - - if name == opt.LongNameWithNamespace() && prio < 2 { - retopt = opt - prio = 2 - } - - if opt.ShortName != 0 && name == string(opt.ShortName) && prio < 1 { - retopt = opt - prio = 1 - } - } - - return retopt -} - -func (g *Group) eachGroup(f func(*Group)) { - f(g) - - for _, gg := range g.groups { - gg.eachGroup(f) - } -} - -func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, handler scanHandler) error { - stype := realval.Type() - - if sfield != nil { - if ok, err := handler(realval, sfield); err != nil { - return err - } else if ok { - return nil - } - } - - for i := 0; i < stype.NumField(); i++ { - field := stype.Field(i) - - // PkgName is set only for non-exported fields, which we ignore - if field.PkgPath != "" { - continue - } - - mtag := newMultiTag(string(field.Tag)) - - if err := mtag.Parse(); err != nil { - return err - } - - // Skip fields with the no-flag tag - if mtag.Get("no-flag") != "" { - continue - } - - // Dive deep into structs or pointers to structs - kind := field.Type.Kind() - fld := realval.Field(i) - - if kind == reflect.Struct { - if err := g.scanStruct(fld, &field, handler); err != nil { - return err - } - } else if kind == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct { - if fld.IsNil() { - fld.Set(reflect.New(fld.Type().Elem())) - } - - if err := g.scanStruct(reflect.Indirect(fld), &field, handler); err != nil { - return err - } - } - - longname := mtag.Get("long") - shortname := mtag.Get("short") - - // Need at least either a short or long name - if longname == "" && shortname == "" && mtag.Get("ini-name") == "" { - continue - } - - short := rune(0) - rc := utf8.RuneCountInString(shortname) - - if rc > 1 { - return newErrorf(ErrShortNameTooLong, - "short names can only be 1 character long, not `%s'", - shortname) - - } else if rc == 1 { - short, _ = utf8.DecodeRuneInString(shortname) - } - - description := mtag.Get("description") - def := mtag.GetMany("default") - - optionalValue := mtag.GetMany("optional-value") - valueName := mtag.Get("value-name") - defaultMask := mtag.Get("default-mask") - - optional := (mtag.Get("optional") != "") - required := (mtag.Get("required") != "") - choices := mtag.GetMany("choice") - hidden := (mtag.Get("hidden") != "") - - option := &Option{ - Description: description, - ShortName: short, - LongName: longname, - Default: def, - EnvDefaultKey: mtag.Get("env"), - EnvDefaultDelim: mtag.Get("env-delim"), - OptionalArgument: optional, - OptionalValue: optionalValue, - Required: required, - ValueName: valueName, - DefaultMask: defaultMask, - Choices: choices, - Hidden: hidden, - - group: g, - - field: field, - value: realval.Field(i), - tag: mtag, - } - - g.options = append(g.options, option) - } - - return nil -} - -func (g *Group) checkForDuplicateFlags() *Error { - shortNames := make(map[rune]*Option) - longNames := make(map[string]*Option) - - var duplicateError *Error - - g.eachGroup(func(g *Group) { - for _, option := range g.options { - if option.LongName != "" { - longName := option.LongNameWithNamespace() - - if otherOption, ok := longNames[longName]; ok { - duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same long name as option `%s'", option, otherOption) - return - } - longNames[longName] = option - } - if option.ShortName != 0 { - if otherOption, ok := shortNames[option.ShortName]; ok { - duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same short name as option `%s'", option, otherOption) - return - } - shortNames[option.ShortName] = option - } - } - }) - - return duplicateError -} - -func (g *Group) scanSubGroupHandler(realval reflect.Value, sfield *reflect.StructField) (bool, error) { - mtag := newMultiTag(string(sfield.Tag)) - - if err := mtag.Parse(); err != nil { - return true, err - } - - subgroup := mtag.Get("group") - - if len(subgroup) != 0 { - ptrval := reflect.NewAt(realval.Type(), unsafe.Pointer(realval.UnsafeAddr())) - description := mtag.Get("description") - - group, err := g.AddGroup(subgroup, description, ptrval.Interface()) - if err != nil { - return true, err - } - - group.Namespace = mtag.Get("namespace") - group.Hidden = mtag.Get("hidden") != "" - - return true, nil - } - - return false, nil -} - -func (g *Group) scanType(handler scanHandler) error { - // Get all the public fields in the data struct - ptrval := reflect.ValueOf(g.data) - - if ptrval.Type().Kind() != reflect.Ptr { - panic(ErrNotPointerToStruct) - } - - stype := ptrval.Type().Elem() - - if stype.Kind() != reflect.Struct { - panic(ErrNotPointerToStruct) - } - - realval := reflect.Indirect(ptrval) - - if err := g.scanStruct(realval, nil, handler); err != nil { - return err - } - - if err := g.checkForDuplicateFlags(); err != nil { - return err - } - - return nil -} - -func (g *Group) scan() error { - return g.scanType(g.scanSubGroupHandler) -} - -func (g *Group) groupByName(name string) *Group { - if len(name) == 0 { - return g - } - - return g.Find(name) -} diff --git a/ini.go b/ini.go index 7225052..cfdf57c 100644 --- a/ini.go +++ b/ini.go @@ -1,8 +1,14 @@ package flags import ( + "bufio" "fmt" "io" + "os" + "reflect" + "sort" + "strconv" + "strings" ) // IniError contains location information on where an error occured. @@ -55,6 +61,20 @@ type IniParser struct { parser *Parser } +type iniValue struct { + Name string + Value string + Quoted bool + LineNumber uint +} + +type iniSection []iniValue + +type ini struct { + File string + Sections map[string]iniSection +} + // NewIniParser creates a new ini parser for a given Parser. func NewIniParser(p *Parser) *IniParser { return &IniParser{ @@ -138,3 +158,436 @@ func (i *IniParser) WriteFile(filename string, options IniOptions) error { func (i *IniParser) Write(writer io.Writer, options IniOptions) { writeIni(i, writer, options) } + +func readFullLine(reader *bufio.Reader) (string, error) { + var line []byte + + for { + l, more, err := reader.ReadLine() + + if err != nil { + return "", err + } + + if line == nil && !more { + return string(l), nil + } + + line = append(line, l...) + + if !more { + break + } + } + + return string(line), nil +} + +func optionIniName(option *Option) string { + name := option.tag.Get("_read-ini-name") + + if len(name) != 0 { + return name + } + + name = option.tag.Get("ini-name") + + if len(name) != 0 { + return name + } + + return option.field.Name +} + +func writeGroupIni(cmd *Command, group *Group, namespace string, writer io.Writer, options IniOptions) { + var sname string + + if len(namespace) != 0 { + sname = namespace + } + + if cmd.Group != group && len(group.ShortDescription) != 0 { + if len(sname) != 0 { + sname += "." + } + + sname += group.ShortDescription + } + + sectionwritten := false + comments := (options & IniIncludeComments) != IniNone + + for _, option := range group.options { + if option.isFunc() || option.Hidden { + continue + } + + if len(option.tag.Get("no-ini")) != 0 { + continue + } + + val := option.value + + if (options&IniIncludeDefaults) == IniNone && option.valueIsDefault() { + continue + } + + if !sectionwritten { + fmt.Fprintf(writer, "[%s]\n", sname) + sectionwritten = true + } + + if comments && len(option.Description) != 0 { + fmt.Fprintf(writer, "; %s\n", option.Description) + } + + oname := optionIniName(option) + + commentOption := (options&(IniIncludeDefaults|IniCommentDefaults)) == IniIncludeDefaults|IniCommentDefaults && option.valueIsDefault() + + kind := val.Type().Kind() + switch kind { + case reflect.Slice: + kind = val.Type().Elem().Kind() + + if val.Len() == 0 { + writeOption(writer, oname, kind, "", "", true, option.iniQuote) + } else { + for idx := 0; idx < val.Len(); idx++ { + v, _ := convertToString(val.Index(idx), option.tag) + + writeOption(writer, oname, kind, "", v, commentOption, option.iniQuote) + } + } + case reflect.Map: + kind = val.Type().Elem().Kind() + + if val.Len() == 0 { + writeOption(writer, oname, kind, "", "", true, option.iniQuote) + } else { + mkeys := val.MapKeys() + keys := make([]string, len(val.MapKeys())) + kkmap := make(map[string]reflect.Value) + + for i, k := range mkeys { + keys[i], _ = convertToString(k, option.tag) + kkmap[keys[i]] = k + } + + sort.Strings(keys) + + for _, k := range keys { + v, _ := convertToString(val.MapIndex(kkmap[k]), option.tag) + + writeOption(writer, oname, kind, k, v, commentOption, option.iniQuote) + } + } + default: + v, _ := convertToString(val, option.tag) + + writeOption(writer, oname, kind, "", v, commentOption, option.iniQuote) + } + + if comments { + fmt.Fprintln(writer) + } + } + + if sectionwritten && !comments { + fmt.Fprintln(writer) + } +} + +func writeOption(writer io.Writer, optionName string, optionType reflect.Kind, optionKey string, optionValue string, commentOption bool, forceQuote bool) { + if forceQuote || (optionType == reflect.String && !isPrint(optionValue)) { + optionValue = strconv.Quote(optionValue) + } + + comment := "" + if commentOption { + comment = "; " + } + + fmt.Fprintf(writer, "%s%s =", comment, optionName) + + if optionKey != "" { + fmt.Fprintf(writer, " %s:%s", optionKey, optionValue) + } else if optionValue != "" { + fmt.Fprintf(writer, " %s", optionValue) + } + + fmt.Fprintln(writer) +} + +func writeCommandIni(command *Command, namespace string, writer io.Writer, options IniOptions) { + command.eachGroup(func(group *Group) { + if !group.Hidden { + writeGroupIni(command, group, namespace, writer, options) + } + }) + + for _, c := range command.commands { + var nns string + + if c.Hidden { + continue + } + + if len(namespace) != 0 { + nns = c.Name + "." + nns + } else { + nns = c.Name + } + + writeCommandIni(c, nns, writer, options) + } +} + +func writeIni(parser *IniParser, writer io.Writer, options IniOptions) { + writeCommandIni(parser.parser.Command, "", writer, options) +} + +func writeIniToFile(parser *IniParser, filename string, options IniOptions) error { + file, err := os.Create(filename) + + if err != nil { + return err + } + + defer file.Close() + + writeIni(parser, file, options) + + return nil +} + +func readIniFromFile(filename string) (*ini, error) { + file, err := os.Open(filename) + + if err != nil { + return nil, err + } + + defer file.Close() + + return readIni(file, filename) +} + +func readIni(contents io.Reader, filename string) (*ini, error) { + ret := &ini{ + File: filename, + Sections: make(map[string]iniSection), + } + + reader := bufio.NewReader(contents) + + // Empty global section + section := make(iniSection, 0, 10) + sectionname := "" + + ret.Sections[sectionname] = section + + var lineno uint + + for { + line, err := readFullLine(reader) + + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + + lineno++ + line = strings.TrimSpace(line) + + // Skip empty lines and lines starting with ; (comments) + if len(line) == 0 || line[0] == ';' || line[0] == '#' { + continue + } + + if line[0] == '[' { + if line[0] != '[' || line[len(line)-1] != ']' { + return nil, &IniError{ + Message: "malformed section header", + File: filename, + LineNumber: lineno, + } + } + + name := strings.TrimSpace(line[1 : len(line)-1]) + + if len(name) == 0 { + return nil, &IniError{ + Message: "empty section name", + File: filename, + LineNumber: lineno, + } + } + + sectionname = name + section = ret.Sections[name] + + if section == nil { + section = make(iniSection, 0, 10) + ret.Sections[name] = section + } + + continue + } + + // Parse option here + keyval := strings.SplitN(line, "=", 2) + + if len(keyval) != 2 { + return nil, &IniError{ + Message: fmt.Sprintf("malformed key=value (%s)", line), + File: filename, + LineNumber: lineno, + } + } + + name := strings.TrimSpace(keyval[0]) + value := strings.TrimSpace(keyval[1]) + quoted := false + + if len(value) != 0 && value[0] == '"' { + if v, err := strconv.Unquote(value); err == nil { + value = v + + quoted = true + } else { + return nil, &IniError{ + Message: err.Error(), + File: filename, + LineNumber: lineno, + } + } + } + + section = append(section, iniValue{ + Name: name, + Value: value, + Quoted: quoted, + LineNumber: lineno, + }) + + ret.Sections[sectionname] = section + } + + return ret, nil +} + +func (i *IniParser) matchingGroups(name string) []*Group { + if len(name) == 0 { + var ret []*Group + + i.parser.eachGroup(func(g *Group) { + ret = append(ret, g) + }) + + return ret + } + + g := i.parser.groupByName(name) + + if g != nil { + return []*Group{g} + } + + return nil +} + +func (i *IniParser) parse(ini *ini) error { + p := i.parser + + var quotesLookup = make(map[*Option]bool) + + for name, section := range ini.Sections { + groups := i.matchingGroups(name) + + if len(groups) == 0 { + return newErrorf(ErrUnknownGroup, "could not find option group `%s'", name) + } + + for _, inival := range section { + var opt *Option + + for _, group := range groups { + opt = group.optionByName(inival.Name, func(o *Option, n string) bool { + return strings.ToLower(o.tag.Get("ini-name")) == strings.ToLower(n) + }) + + if opt != nil && len(opt.tag.Get("no-ini")) != 0 { + opt = nil + } + + if opt != nil { + break + } + } + + if opt == nil { + if (p.Options & IgnoreUnknown) == None { + return &IniError{ + Message: fmt.Sprintf("unknown option: %s", inival.Name), + File: ini.File, + LineNumber: inival.LineNumber, + } + } + + continue + } + + pval := &inival.Value + + if !opt.canArgument() && len(inival.Value) == 0 { + pval = nil + } else { + if opt.value.Type().Kind() == reflect.Map { + parts := strings.SplitN(inival.Value, ":", 2) + + // only handle unquoting + if len(parts) == 2 && parts[1][0] == '"' { + if v, err := strconv.Unquote(parts[1]); err == nil { + parts[1] = v + + inival.Quoted = true + } else { + return &IniError{ + Message: err.Error(), + File: ini.File, + LineNumber: inival.LineNumber, + } + } + + s := parts[0] + ":" + parts[1] + + pval = &s + } + } + } + + if err := opt.set(pval); err != nil { + return &IniError{ + Message: err.Error(), + File: ini.File, + LineNumber: inival.LineNumber, + } + } + + // either all INI values are quoted or only values who need quoting + if _, ok := quotesLookup[opt]; !inival.Quoted || !ok { + quotesLookup[opt] = inival.Quoted + } + + opt.tag.Set("_read-ini-name", inival.Name) + } + } + + for opt, quoted := range quotesLookup { + opt.iniQuote = quoted + } + + return nil +} diff --git a/ini_private.go b/ini_private.go deleted file mode 100644 index 45434ec..0000000 --- a/ini_private.go +++ /dev/null @@ -1,458 +0,0 @@ -package flags - -import ( - "bufio" - "fmt" - "io" - "os" - "reflect" - "sort" - "strconv" - "strings" -) - -type iniValue struct { - Name string - Value string - Quoted bool - LineNumber uint -} - -type iniSection []iniValue -type ini struct { - File string - Sections map[string]iniSection -} - -func readFullLine(reader *bufio.Reader) (string, error) { - var line []byte - - for { - l, more, err := reader.ReadLine() - - if err != nil { - return "", err - } - - if line == nil && !more { - return string(l), nil - } - - line = append(line, l...) - - if !more { - break - } - } - - return string(line), nil -} - -func optionIniName(option *Option) string { - name := option.tag.Get("_read-ini-name") - - if len(name) != 0 { - return name - } - - name = option.tag.Get("ini-name") - - if len(name) != 0 { - return name - } - - return option.field.Name -} - -func writeGroupIni(cmd *Command, group *Group, namespace string, writer io.Writer, options IniOptions) { - var sname string - - if len(namespace) != 0 { - sname = namespace - } - - if cmd.Group != group && len(group.ShortDescription) != 0 { - if len(sname) != 0 { - sname += "." - } - - sname += group.ShortDescription - } - - sectionwritten := false - comments := (options & IniIncludeComments) != IniNone - - for _, option := range group.options { - if option.isFunc() || option.Hidden { - continue - } - - if len(option.tag.Get("no-ini")) != 0 { - continue - } - - val := option.value - - if (options&IniIncludeDefaults) == IniNone && option.valueIsDefault() { - continue - } - - if !sectionwritten { - fmt.Fprintf(writer, "[%s]\n", sname) - sectionwritten = true - } - - if comments && len(option.Description) != 0 { - fmt.Fprintf(writer, "; %s\n", option.Description) - } - - oname := optionIniName(option) - - commentOption := (options&(IniIncludeDefaults|IniCommentDefaults)) == IniIncludeDefaults|IniCommentDefaults && option.valueIsDefault() - - kind := val.Type().Kind() - switch kind { - case reflect.Slice: - kind = val.Type().Elem().Kind() - - if val.Len() == 0 { - writeOption(writer, oname, kind, "", "", true, option.iniQuote) - } else { - for idx := 0; idx < val.Len(); idx++ { - v, _ := convertToString(val.Index(idx), option.tag) - - writeOption(writer, oname, kind, "", v, commentOption, option.iniQuote) - } - } - case reflect.Map: - kind = val.Type().Elem().Kind() - - if val.Len() == 0 { - writeOption(writer, oname, kind, "", "", true, option.iniQuote) - } else { - mkeys := val.MapKeys() - keys := make([]string, len(val.MapKeys())) - kkmap := make(map[string]reflect.Value) - - for i, k := range mkeys { - keys[i], _ = convertToString(k, option.tag) - kkmap[keys[i]] = k - } - - sort.Strings(keys) - - for _, k := range keys { - v, _ := convertToString(val.MapIndex(kkmap[k]), option.tag) - - writeOption(writer, oname, kind, k, v, commentOption, option.iniQuote) - } - } - default: - v, _ := convertToString(val, option.tag) - - writeOption(writer, oname, kind, "", v, commentOption, option.iniQuote) - } - - if comments { - fmt.Fprintln(writer) - } - } - - if sectionwritten && !comments { - fmt.Fprintln(writer) - } -} - -func writeOption(writer io.Writer, optionName string, optionType reflect.Kind, optionKey string, optionValue string, commentOption bool, forceQuote bool) { - if forceQuote || (optionType == reflect.String && !isPrint(optionValue)) { - optionValue = strconv.Quote(optionValue) - } - - comment := "" - if commentOption { - comment = "; " - } - - fmt.Fprintf(writer, "%s%s =", comment, optionName) - - if optionKey != "" { - fmt.Fprintf(writer, " %s:%s", optionKey, optionValue) - } else if optionValue != "" { - fmt.Fprintf(writer, " %s", optionValue) - } - - fmt.Fprintln(writer) -} - -func writeCommandIni(command *Command, namespace string, writer io.Writer, options IniOptions) { - command.eachGroup(func(group *Group) { - if !group.Hidden { - writeGroupIni(command, group, namespace, writer, options) - } - }) - - for _, c := range command.commands { - var nns string - - if c.Hidden { - continue - } - - if len(namespace) != 0 { - nns = c.Name + "." + nns - } else { - nns = c.Name - } - - writeCommandIni(c, nns, writer, options) - } -} - -func writeIni(parser *IniParser, writer io.Writer, options IniOptions) { - writeCommandIni(parser.parser.Command, "", writer, options) -} - -func writeIniToFile(parser *IniParser, filename string, options IniOptions) error { - file, err := os.Create(filename) - - if err != nil { - return err - } - - defer file.Close() - - writeIni(parser, file, options) - - return nil -} - -func readIniFromFile(filename string) (*ini, error) { - file, err := os.Open(filename) - - if err != nil { - return nil, err - } - - defer file.Close() - - return readIni(file, filename) -} - -func readIni(contents io.Reader, filename string) (*ini, error) { - ret := &ini{ - File: filename, - Sections: make(map[string]iniSection), - } - - reader := bufio.NewReader(contents) - - // Empty global section - section := make(iniSection, 0, 10) - sectionname := "" - - ret.Sections[sectionname] = section - - var lineno uint - - for { - line, err := readFullLine(reader) - - if err == io.EOF { - break - } else if err != nil { - return nil, err - } - - lineno++ - line = strings.TrimSpace(line) - - // Skip empty lines and lines starting with ; (comments) - if len(line) == 0 || line[0] == ';' || line[0] == '#' { - continue - } - - if line[0] == '[' { - if line[0] != '[' || line[len(line)-1] != ']' { - return nil, &IniError{ - Message: "malformed section header", - File: filename, - LineNumber: lineno, - } - } - - name := strings.TrimSpace(line[1 : len(line)-1]) - - if len(name) == 0 { - return nil, &IniError{ - Message: "empty section name", - File: filename, - LineNumber: lineno, - } - } - - sectionname = name - section = ret.Sections[name] - - if section == nil { - section = make(iniSection, 0, 10) - ret.Sections[name] = section - } - - continue - } - - // Parse option here - keyval := strings.SplitN(line, "=", 2) - - if len(keyval) != 2 { - return nil, &IniError{ - Message: fmt.Sprintf("malformed key=value (%s)", line), - File: filename, - LineNumber: lineno, - } - } - - name := strings.TrimSpace(keyval[0]) - value := strings.TrimSpace(keyval[1]) - quoted := false - - if len(value) != 0 && value[0] == '"' { - if v, err := strconv.Unquote(value); err == nil { - value = v - - quoted = true - } else { - return nil, &IniError{ - Message: err.Error(), - File: filename, - LineNumber: lineno, - } - } - } - - section = append(section, iniValue{ - Name: name, - Value: value, - Quoted: quoted, - LineNumber: lineno, - }) - - ret.Sections[sectionname] = section - } - - return ret, nil -} - -func (i *IniParser) matchingGroups(name string) []*Group { - if len(name) == 0 { - var ret []*Group - - i.parser.eachGroup(func(g *Group) { - ret = append(ret, g) - }) - - return ret - } - - g := i.parser.groupByName(name) - - if g != nil { - return []*Group{g} - } - - return nil -} - -func (i *IniParser) parse(ini *ini) error { - p := i.parser - - var quotesLookup = make(map[*Option]bool) - - for name, section := range ini.Sections { - groups := i.matchingGroups(name) - - if len(groups) == 0 { - return newErrorf(ErrUnknownGroup, "could not find option group `%s'", name) - } - - for _, inival := range section { - var opt *Option - - for _, group := range groups { - opt = group.optionByName(inival.Name, func(o *Option, n string) bool { - return strings.ToLower(o.tag.Get("ini-name")) == strings.ToLower(n) - }) - - if opt != nil && len(opt.tag.Get("no-ini")) != 0 { - opt = nil - } - - if opt != nil { - break - } - } - - if opt == nil { - if (p.Options & IgnoreUnknown) == None { - return &IniError{ - Message: fmt.Sprintf("unknown option: %s", inival.Name), - File: ini.File, - LineNumber: inival.LineNumber, - } - } - - continue - } - - pval := &inival.Value - - if !opt.canArgument() && len(inival.Value) == 0 { - pval = nil - } else { - if opt.value.Type().Kind() == reflect.Map { - parts := strings.SplitN(inival.Value, ":", 2) - - // only handle unquoting - if len(parts) == 2 && parts[1][0] == '"' { - if v, err := strconv.Unquote(parts[1]); err == nil { - parts[1] = v - - inival.Quoted = true - } else { - return &IniError{ - Message: err.Error(), - File: ini.File, - LineNumber: inival.LineNumber, - } - } - - s := parts[0] + ":" + parts[1] - - pval = &s - } - } - } - - if err := opt.set(pval); err != nil { - return &IniError{ - Message: err.Error(), - File: ini.File, - LineNumber: inival.LineNumber, - } - } - - // either all INI values are quoted or only values who need quoting - if _, ok := quotesLookup[opt]; !inival.Quoted || !ok { - quotesLookup[opt] = inival.Quoted - } - - opt.tag.Set("_read-ini-name", inival.Name) - } - } - - for opt, quoted := range quotesLookup { - opt.iniQuote = quoted - } - - return nil -} diff --git a/option.go b/option.go index 8764e25..b4b867d 100644 --- a/option.go +++ b/option.go @@ -3,6 +3,8 @@ package flags import ( "fmt" "reflect" + "strings" + "syscall" "unicode/utf8" ) @@ -168,3 +170,242 @@ func (option *Option) Value() interface{} { func (option *Option) IsSet() bool { return option.isSet } + +// Set the value of an option to the specified value. An error will be returned +// if the specified value could not be converted to the corresponding option +// value type. +func (option *Option) set(value *string) error { + kind := option.value.Type().Kind() + + if (kind == reflect.Map || kind == reflect.Slice) && !option.isSet { + option.empty() + } + + option.isSet = true + + if len(option.Choices) != 0 { + found := false + + for _, choice := range option.Choices { + if choice == *value { + found = true + break + } + } + + if !found { + allowed := strings.Join(option.Choices[0:len(option.Choices)-1], ", ") + + if len(option.Choices) > 1 { + allowed += " or " + option.Choices[len(option.Choices)-1] + } + + return newErrorf(ErrInvalidChoice, + "Invalid value `%s' for option `%s'. Allowed values are: %s", + *value, option, allowed) + } + } + + if option.isFunc() { + return option.call(value) + } else if value != nil { + return convert(*value, option.value, option.tag) + } + + return convert("", option.value, option.tag) +} + +func (option *Option) canCli() bool { + return option.ShortName != 0 || len(option.LongName) != 0 +} + +func (option *Option) canArgument() bool { + if u := option.isUnmarshaler(); u != nil { + return true + } + + return !option.isBool() +} + +func (option *Option) emptyValue() reflect.Value { + tp := option.value.Type() + + if tp.Kind() == reflect.Map { + return reflect.MakeMap(tp) + } + + return reflect.Zero(tp) +} + +func (option *Option) empty() { + if !option.isFunc() { + option.value.Set(option.emptyValue()) + } +} + +func (option *Option) clearDefault() { + usedDefault := option.Default + if envKey := option.EnvDefaultKey; envKey != "" { + // os.Getenv() makes no distinction between undefined and + // empty values, so we use syscall.Getenv() + if value, ok := syscall.Getenv(envKey); ok { + if option.EnvDefaultDelim != "" { + usedDefault = strings.Split(value, + option.EnvDefaultDelim) + } else { + usedDefault = []string{value} + } + } + } + + if len(usedDefault) > 0 { + option.empty() + + for _, d := range usedDefault { + option.set(&d) + } + } else { + tp := option.value.Type() + + switch tp.Kind() { + case reflect.Map: + if option.value.IsNil() { + option.empty() + } + case reflect.Slice: + if option.value.IsNil() { + option.empty() + } + } + } +} + +func (option *Option) valueIsDefault() bool { + // Check if the value of the option corresponds to its + // default value + emptyval := option.emptyValue() + + checkvalptr := reflect.New(emptyval.Type()) + checkval := reflect.Indirect(checkvalptr) + + checkval.Set(emptyval) + + if len(option.Default) != 0 { + for _, v := range option.Default { + convert(v, checkval, option.tag) + } + } + + return reflect.DeepEqual(option.value.Interface(), checkval.Interface()) +} + +func (option *Option) isUnmarshaler() Unmarshaler { + v := option.value + + for { + if !v.CanInterface() { + break + } + + i := v.Interface() + + if u, ok := i.(Unmarshaler); ok { + return u + } + + if !v.CanAddr() { + break + } + + v = v.Addr() + } + + return nil +} + +func (option *Option) isBool() bool { + tp := option.value.Type() + + for { + switch tp.Kind() { + case reflect.Bool: + return true + case reflect.Slice: + return (tp.Elem().Kind() == reflect.Bool) + case reflect.Func: + return tp.NumIn() == 0 + case reflect.Ptr: + tp = tp.Elem() + default: + return false + } + } +} + +func (option *Option) isFunc() bool { + return option.value.Type().Kind() == reflect.Func +} + +func (option *Option) call(value *string) error { + var retval []reflect.Value + + if value == nil { + retval = option.value.Call(nil) + } else { + tp := option.value.Type().In(0) + + val := reflect.New(tp) + val = reflect.Indirect(val) + + if err := convert(*value, val, option.tag); err != nil { + return err + } + + retval = option.value.Call([]reflect.Value{val}) + } + + if len(retval) == 1 && retval[0].Type() == reflect.TypeOf((*error)(nil)).Elem() { + if retval[0].Interface() == nil { + return nil + } + + return retval[0].Interface().(error) + } + + return nil +} + +func (option *Option) updateDefaultLiteral() { + defs := option.Default + def := "" + + if len(defs) == 0 && option.canArgument() { + var showdef bool + + switch option.field.Type.Kind() { + case reflect.Func, reflect.Ptr: + showdef = !option.value.IsNil() + case reflect.Slice, reflect.String, reflect.Array: + showdef = option.value.Len() > 0 + case reflect.Map: + showdef = !option.value.IsNil() && option.value.Len() > 0 + default: + zeroval := reflect.Zero(option.field.Type) + showdef = !reflect.DeepEqual(zeroval.Interface(), option.value.Interface()) + } + + if showdef { + def, _ = convertToString(option.value, option.tag) + } + } else if len(defs) != 0 { + l := len(defs) - 1 + + for i := 0; i < l; i++ { + def += quoteIfNeeded(defs[i]) + ", " + } + + def += quoteIfNeeded(defs[l]) + } + + option.defaultLiteral = def +} diff --git a/option_private.go b/option_private.go deleted file mode 100644 index 444334e..0000000 --- a/option_private.go +++ /dev/null @@ -1,246 +0,0 @@ -package flags - -import ( - "reflect" - "strings" - "syscall" -) - -// Set the value of an option to the specified value. An error will be returned -// if the specified value could not be converted to the corresponding option -// value type. -func (option *Option) set(value *string) error { - kind := option.value.Type().Kind() - - if (kind == reflect.Map || kind == reflect.Slice) && !option.isSet { - option.empty() - } - - option.isSet = true - - if len(option.Choices) != 0 { - found := false - - for _, choice := range option.Choices { - if choice == *value { - found = true - break - } - } - - if !found { - allowed := strings.Join(option.Choices[0:len(option.Choices)-1], ", ") - - if len(option.Choices) > 1 { - allowed += " or " + option.Choices[len(option.Choices)-1] - } - - return newErrorf(ErrInvalidChoice, - "Invalid value `%s' for option `%s'. Allowed values are: %s", - *value, option, allowed) - } - } - - if option.isFunc() { - return option.call(value) - } else if value != nil { - return convert(*value, option.value, option.tag) - } - - return convert("", option.value, option.tag) -} - -func (option *Option) canCli() bool { - return option.ShortName != 0 || len(option.LongName) != 0 -} - -func (option *Option) canArgument() bool { - if u := option.isUnmarshaler(); u != nil { - return true - } - - return !option.isBool() -} - -func (option *Option) emptyValue() reflect.Value { - tp := option.value.Type() - - if tp.Kind() == reflect.Map { - return reflect.MakeMap(tp) - } - - return reflect.Zero(tp) -} - -func (option *Option) empty() { - if !option.isFunc() { - option.value.Set(option.emptyValue()) - } -} - -func (option *Option) clearDefault() { - usedDefault := option.Default - if envKey := option.EnvDefaultKey; envKey != "" { - // os.Getenv() makes no distinction between undefined and - // empty values, so we use syscall.Getenv() - if value, ok := syscall.Getenv(envKey); ok { - if option.EnvDefaultDelim != "" { - usedDefault = strings.Split(value, - option.EnvDefaultDelim) - } else { - usedDefault = []string{value} - } - } - } - - if len(usedDefault) > 0 { - option.empty() - - for _, d := range usedDefault { - option.set(&d) - } - } else { - tp := option.value.Type() - - switch tp.Kind() { - case reflect.Map: - if option.value.IsNil() { - option.empty() - } - case reflect.Slice: - if option.value.IsNil() { - option.empty() - } - } - } -} - -func (option *Option) valueIsDefault() bool { - // Check if the value of the option corresponds to its - // default value - emptyval := option.emptyValue() - - checkvalptr := reflect.New(emptyval.Type()) - checkval := reflect.Indirect(checkvalptr) - - checkval.Set(emptyval) - - if len(option.Default) != 0 { - for _, v := range option.Default { - convert(v, checkval, option.tag) - } - } - - return reflect.DeepEqual(option.value.Interface(), checkval.Interface()) -} - -func (option *Option) isUnmarshaler() Unmarshaler { - v := option.value - - for { - if !v.CanInterface() { - break - } - - i := v.Interface() - - if u, ok := i.(Unmarshaler); ok { - return u - } - - if !v.CanAddr() { - break - } - - v = v.Addr() - } - - return nil -} - -func (option *Option) isBool() bool { - tp := option.value.Type() - - for { - switch tp.Kind() { - case reflect.Bool: - return true - case reflect.Slice: - return (tp.Elem().Kind() == reflect.Bool) - case reflect.Func: - return tp.NumIn() == 0 - case reflect.Ptr: - tp = tp.Elem() - default: - return false - } - } -} - -func (option *Option) isFunc() bool { - return option.value.Type().Kind() == reflect.Func -} - -func (option *Option) call(value *string) error { - var retval []reflect.Value - - if value == nil { - retval = option.value.Call(nil) - } else { - tp := option.value.Type().In(0) - - val := reflect.New(tp) - val = reflect.Indirect(val) - - if err := convert(*value, val, option.tag); err != nil { - return err - } - - retval = option.value.Call([]reflect.Value{val}) - } - - if len(retval) == 1 && retval[0].Type() == reflect.TypeOf((*error)(nil)).Elem() { - if retval[0].Interface() == nil { - return nil - } - - return retval[0].Interface().(error) - } - - return nil -} - -func (option *Option) updateDefaultLiteral() { - defs := option.Default - def := "" - - if len(defs) == 0 && option.canArgument() { - var showdef bool - - switch option.field.Type.Kind() { - case reflect.Func, reflect.Ptr: - showdef = !option.value.IsNil() - case reflect.Slice, reflect.String, reflect.Array: - showdef = option.value.Len() > 0 - case reflect.Map: - showdef = !option.value.IsNil() && option.value.Len() > 0 - default: - zeroval := reflect.Zero(option.field.Type) - showdef = !reflect.DeepEqual(zeroval.Interface(), option.value.Interface()) - } - - if showdef { - def, _ = convertToString(option.value, option.tag) - } - } else if len(defs) != 0 { - l := len(defs) - 1 - - for i := 0; i < l; i++ { - def += quoteIfNeeded(defs[i]) + ", " - } - - def += quoteIfNeeded(defs[l]) - } - - option.defaultLiteral = def -} diff --git a/parser.go b/parser.go index 8dedd0e..2f74b9a 100644 --- a/parser.go +++ b/parser.go @@ -5,8 +5,13 @@ package flags import ( + "bytes" + "fmt" "os" "path" + "sort" + "strings" + "unicode/utf8" ) // A Parser provides command line option parsing. It can contain several @@ -98,6 +103,17 @@ const ( Default = HelpFlag | PrintErrors | PassDoubleDash ) +type parseState struct { + arg string + args []string + retargs []string + positional []*Arg + err error + + command *Command + lookup lookup +} + // Parse is a convenience function to parse command line options with default // settings. The provided data is a pointer to a struct representing the // default option group (named "Application Options"). For more control, use @@ -299,3 +315,322 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { return s.retargs, nil } + +func (p *parseState) eof() bool { + return len(p.args) == 0 +} + +func (p *parseState) pop() string { + if p.eof() { + return "" + } + + p.arg = p.args[0] + p.args = p.args[1:] + + return p.arg +} + +func (p *parseState) peek() string { + if p.eof() { + return "" + } + + return p.args[0] +} + +func (p *parseState) checkRequired(parser *Parser) error { + c := parser.Command + + var required []*Option + + for c != nil { + c.eachGroup(func(g *Group) { + for _, option := range g.options { + if !option.isSet && option.Required { + required = append(required, option) + } + } + }) + + c = c.Active + } + + if len(required) == 0 { + if len(p.positional) > 0 && p.command.ArgsRequired { + var reqnames []string + + for _, arg := range p.positional { + if arg.isRemaining() { + break + } + + reqnames = append(reqnames, "`"+arg.Name+"`") + } + + if len(reqnames) == 0 { + return nil + } + + var msg string + + if len(reqnames) == 1 { + msg = fmt.Sprintf("the required argument %s was not provided", reqnames[0]) + } else { + msg = fmt.Sprintf("the required arguments %s and %s were not provided", + strings.Join(reqnames[:len(reqnames)-1], ", "), reqnames[len(reqnames)-1]) + } + + p.err = newError(ErrRequired, msg) + return p.err + } + + return nil + } + + names := make([]string, 0, len(required)) + + for _, k := range required { + names = append(names, "`"+k.String()+"'") + } + + sort.Strings(names) + + var msg string + + if len(names) == 1 { + msg = fmt.Sprintf("the required flag %s was not specified", names[0]) + } else { + msg = fmt.Sprintf("the required flags %s and %s were not specified", + strings.Join(names[:len(names)-1], ", "), names[len(names)-1]) + } + + p.err = newError(ErrRequired, msg) + return p.err +} + +func (p *parseState) estimateCommand() error { + commands := p.command.sortedVisibleCommands() + cmdnames := make([]string, len(commands)) + + for i, v := range commands { + cmdnames[i] = v.Name + } + + var msg string + var errtype ErrorType + + if len(p.retargs) != 0 { + c, l := closestChoice(p.retargs[0], cmdnames) + msg = fmt.Sprintf("Unknown command `%s'", p.retargs[0]) + errtype = ErrUnknownCommand + + if float32(l)/float32(len(c)) < 0.5 { + msg = fmt.Sprintf("%s, did you mean `%s'?", msg, c) + } else if len(cmdnames) == 1 { + msg = fmt.Sprintf("%s. You should use the %s command", + msg, + cmdnames[0]) + } else { + msg = fmt.Sprintf("%s. Please specify one command of: %s or %s", + msg, + strings.Join(cmdnames[:len(cmdnames)-1], ", "), + cmdnames[len(cmdnames)-1]) + } + } else { + errtype = ErrCommandRequired + + if len(cmdnames) == 1 { + msg = fmt.Sprintf("Please specify the %s command", cmdnames[0]) + } else { + msg = fmt.Sprintf("Please specify one command of: %s or %s", + strings.Join(cmdnames[:len(cmdnames)-1], ", "), + cmdnames[len(cmdnames)-1]) + } + } + + return newError(errtype, msg) +} + +func (p *Parser) parseOption(s *parseState, name string, option *Option, canarg bool, argument *string) (err error) { + if !option.canArgument() { + if argument != nil { + return newErrorf(ErrNoArgumentForBool, "bool flag `%s' cannot have an argument", option) + } + + err = option.set(nil) + } else if argument != nil || (canarg && !s.eof()) { + var arg string + + if argument != nil { + arg = *argument + } else { + arg = s.pop() + + if argumentIsOption(arg) { + return newErrorf(ErrExpectedArgument, "expected argument for flag `%s', but got option `%s'", option, arg) + } else if p.Options&PassDoubleDash != 0 && arg == "--" { + return newErrorf(ErrExpectedArgument, "expected argument for flag `%s', but got double dash `--'", option) + } + } + + if option.tag.Get("unquote") != "false" { + arg, err = unquoteIfPossible(arg) + } + + if err == nil { + err = option.set(&arg) + } + } else if option.OptionalArgument { + option.empty() + + for _, v := range option.OptionalValue { + err = option.set(&v) + + if err != nil { + break + } + } + } else { + err = newErrorf(ErrExpectedArgument, "expected argument for flag `%s'", option) + } + + if err != nil { + if _, ok := err.(*Error); !ok { + err = newErrorf(ErrMarshal, "invalid argument for flag `%s' (expected %s): %s", + option, + option.value.Type(), + err.Error()) + } + } + + return err +} + +func (p *Parser) parseLong(s *parseState, name string, argument *string) error { + if option := s.lookup.longNames[name]; 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 newErrorf(ErrUnknownFlag, "unknown flag `%s'", name) +} + +func (p *Parser) splitShortConcatArg(s *parseState, optname string) (string, *string) { + c, n := utf8.DecodeRuneInString(optname) + + if n == len(optname) { + return optname, nil + } + + first := string(c) + + if option := s.lookup.shortNames[first]; option != nil && option.canArgument() { + arg := optname[n:] + return first, &arg + } + + return optname, nil +} + +func (p *Parser) parseShort(s *parseState, optname string, argument *string) error { + if argument == nil { + optname, argument = p.splitShortConcatArg(s, optname) + } + + for i, c := range optname { + shortname := string(c) + + 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 + canarg := (i+utf8.RuneLen(c) == len(optname)) && !option.OptionalArgument + + if err := p.parseOption(s, shortname, option, canarg, argument); err != nil { + return err + } + } else { + return newErrorf(ErrUnknownFlag, "unknown flag `%s'", shortname) + } + + // Only the first option can have a concatted argument, so just + // clear argument here + argument = nil + } + + return nil +} + +func (p *parseState) addArgs(args ...string) error { + for len(p.positional) > 0 && len(args) > 0 { + arg := p.positional[0] + + if err := convert(args[0], arg.value, arg.tag); err != nil { + return err + } + + if !arg.isRemaining() { + p.positional = p.positional[1:] + } + + args = args[1:] + } + + p.retargs = append(p.retargs, args...) + return nil +} + +func (p *Parser) parseNonOption(s *parseState) error { + if len(s.positional) > 0 { + return s.addArgs(s.arg) + } + + if cmd := s.lookup.commands[s.arg]; cmd != nil { + s.command.Active = cmd + cmd.fillParseState(s) + } else if (p.Options & PassAfterNonOption) != None { + // If PassAfterNonOption is set then all remaining arguments + // are considered positional + if err := s.addArgs(s.arg); err != nil { + return err + } + + if err := s.addArgs(s.args...); err != nil { + return err + } + + s.args = []string{} + } else { + return s.addArgs(s.arg) + } + + return nil +} + +func (p *Parser) showBuiltinHelp() error { + var b bytes.Buffer + + p.WriteHelp(&b) + return newError(ErrHelp, b.String()) +} + +func (p *Parser) printError(err error) error { + if err != nil && (p.Options&PrintErrors) != None { + fmt.Fprintln(os.Stderr, err) + } + + return err +} + +func (p *Parser) clearIsSet() { + p.eachCommand(func(c *Command) { + c.eachGroup(func(g *Group) { + for _, option := range g.options { + option.isSet = false + } + }) + }, true) +} diff --git a/parser_private.go b/parser_private.go deleted file mode 100644 index 7469c03..0000000 --- a/parser_private.go +++ /dev/null @@ -1,340 +0,0 @@ -package flags - -import ( - "bytes" - "fmt" - "os" - "sort" - "strings" - "unicode/utf8" -) - -type parseState struct { - arg string - args []string - retargs []string - positional []*Arg - err error - - command *Command - lookup lookup -} - -func (p *parseState) eof() bool { - return len(p.args) == 0 -} - -func (p *parseState) pop() string { - if p.eof() { - return "" - } - - p.arg = p.args[0] - p.args = p.args[1:] - - return p.arg -} - -func (p *parseState) peek() string { - if p.eof() { - return "" - } - - return p.args[0] -} - -func (p *parseState) checkRequired(parser *Parser) error { - c := parser.Command - - var required []*Option - - for c != nil { - c.eachGroup(func(g *Group) { - for _, option := range g.options { - if !option.isSet && option.Required { - required = append(required, option) - } - } - }) - - c = c.Active - } - - if len(required) == 0 { - if len(p.positional) > 0 && p.command.ArgsRequired { - var reqnames []string - - for _, arg := range p.positional { - if arg.isRemaining() { - break - } - - reqnames = append(reqnames, "`"+arg.Name+"`") - } - - if len(reqnames) == 0 { - return nil - } - - var msg string - - if len(reqnames) == 1 { - msg = fmt.Sprintf("the required argument %s was not provided", reqnames[0]) - } else { - msg = fmt.Sprintf("the required arguments %s and %s were not provided", - strings.Join(reqnames[:len(reqnames)-1], ", "), reqnames[len(reqnames)-1]) - } - - p.err = newError(ErrRequired, msg) - return p.err - } - - return nil - } - - names := make([]string, 0, len(required)) - - for _, k := range required { - names = append(names, "`"+k.String()+"'") - } - - sort.Strings(names) - - var msg string - - if len(names) == 1 { - msg = fmt.Sprintf("the required flag %s was not specified", names[0]) - } else { - msg = fmt.Sprintf("the required flags %s and %s were not specified", - strings.Join(names[:len(names)-1], ", "), names[len(names)-1]) - } - - p.err = newError(ErrRequired, msg) - return p.err -} - -func (p *parseState) estimateCommand() error { - commands := p.command.sortedVisibleCommands() - cmdnames := make([]string, len(commands)) - - for i, v := range commands { - cmdnames[i] = v.Name - } - - var msg string - var errtype ErrorType - - if len(p.retargs) != 0 { - c, l := closestChoice(p.retargs[0], cmdnames) - msg = fmt.Sprintf("Unknown command `%s'", p.retargs[0]) - errtype = ErrUnknownCommand - - if float32(l)/float32(len(c)) < 0.5 { - msg = fmt.Sprintf("%s, did you mean `%s'?", msg, c) - } else if len(cmdnames) == 1 { - msg = fmt.Sprintf("%s. You should use the %s command", - msg, - cmdnames[0]) - } else { - msg = fmt.Sprintf("%s. Please specify one command of: %s or %s", - msg, - strings.Join(cmdnames[:len(cmdnames)-1], ", "), - cmdnames[len(cmdnames)-1]) - } - } else { - errtype = ErrCommandRequired - - if len(cmdnames) == 1 { - msg = fmt.Sprintf("Please specify the %s command", cmdnames[0]) - } else { - msg = fmt.Sprintf("Please specify one command of: %s or %s", - strings.Join(cmdnames[:len(cmdnames)-1], ", "), - cmdnames[len(cmdnames)-1]) - } - } - - return newError(errtype, msg) -} - -func (p *Parser) parseOption(s *parseState, name string, option *Option, canarg bool, argument *string) (err error) { - if !option.canArgument() { - if argument != nil { - return newErrorf(ErrNoArgumentForBool, "bool flag `%s' cannot have an argument", option) - } - - err = option.set(nil) - } else if argument != nil || (canarg && !s.eof()) { - var arg string - - if argument != nil { - arg = *argument - } else { - arg = s.pop() - - if argumentIsOption(arg) { - return newErrorf(ErrExpectedArgument, "expected argument for flag `%s', but got option `%s'", option, arg) - } else if p.Options&PassDoubleDash != 0 && arg == "--" { - return newErrorf(ErrExpectedArgument, "expected argument for flag `%s', but got double dash `--'", option) - } - } - - if option.tag.Get("unquote") != "false" { - arg, err = unquoteIfPossible(arg) - } - - if err == nil { - err = option.set(&arg) - } - } else if option.OptionalArgument { - option.empty() - - for _, v := range option.OptionalValue { - err = option.set(&v) - - if err != nil { - break - } - } - } else { - err = newErrorf(ErrExpectedArgument, "expected argument for flag `%s'", option) - } - - if err != nil { - if _, ok := err.(*Error); !ok { - err = newErrorf(ErrMarshal, "invalid argument for flag `%s' (expected %s): %s", - option, - option.value.Type(), - err.Error()) - } - } - - return err -} - -func (p *Parser) parseLong(s *parseState, name string, argument *string) error { - if option := s.lookup.longNames[name]; 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 newErrorf(ErrUnknownFlag, "unknown flag `%s'", name) -} - -func (p *Parser) splitShortConcatArg(s *parseState, optname string) (string, *string) { - c, n := utf8.DecodeRuneInString(optname) - - if n == len(optname) { - return optname, nil - } - - first := string(c) - - if option := s.lookup.shortNames[first]; option != nil && option.canArgument() { - arg := optname[n:] - return first, &arg - } - - return optname, nil -} - -func (p *Parser) parseShort(s *parseState, optname string, argument *string) error { - if argument == nil { - optname, argument = p.splitShortConcatArg(s, optname) - } - - for i, c := range optname { - shortname := string(c) - - 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 - canarg := (i+utf8.RuneLen(c) == len(optname)) && !option.OptionalArgument - - if err := p.parseOption(s, shortname, option, canarg, argument); err != nil { - return err - } - } else { - return newErrorf(ErrUnknownFlag, "unknown flag `%s'", shortname) - } - - // Only the first option can have a concatted argument, so just - // clear argument here - argument = nil - } - - return nil -} - -func (p *parseState) addArgs(args ...string) error { - for len(p.positional) > 0 && len(args) > 0 { - arg := p.positional[0] - - if err := convert(args[0], arg.value, arg.tag); err != nil { - return err - } - - if !arg.isRemaining() { - p.positional = p.positional[1:] - } - - args = args[1:] - } - - p.retargs = append(p.retargs, args...) - return nil -} - -func (p *Parser) parseNonOption(s *parseState) error { - if len(s.positional) > 0 { - return s.addArgs(s.arg) - } - - if cmd := s.lookup.commands[s.arg]; cmd != nil { - s.command.Active = cmd - cmd.fillParseState(s) - } else if (p.Options & PassAfterNonOption) != None { - // If PassAfterNonOption is set then all remaining arguments - // are considered positional - if err := s.addArgs(s.arg); err != nil { - return err - } - - if err := s.addArgs(s.args...); err != nil { - return err - } - - s.args = []string{} - } else { - return s.addArgs(s.arg) - } - - return nil -} - -func (p *Parser) showBuiltinHelp() error { - var b bytes.Buffer - - p.WriteHelp(&b) - return newError(ErrHelp, b.String()) -} - -func (p *Parser) printError(err error) error { - if err != nil && (p.Options&PrintErrors) != None { - fmt.Fprintln(os.Stderr, err) - } - - return err -} - -func (p *Parser) clearIsSet() { - p.eachCommand(func(c *Command) { - c.eachGroup(func(g *Group) { - for _, option := range g.options { - option.isSet = false - } - }) - }, true) -} From 31c5fc5210266f193818bee5e01f94645a4f3dcb Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 18 Oct 2015 22:34:10 +0200 Subject: [PATCH 28/38] Do not apply defaults if option value was set from ini Fixes #120. --- ini_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ option.go | 7 +++++-- parser.go | 2 +- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/ini_test.go b/ini_test.go index c0073e7..941735e 100644 --- a/ini_test.go +++ b/ini_test.go @@ -720,6 +720,46 @@ func TestIniCliOverrides(t *testing.T) { } } +func TestIniOverrides(t *testing.T) { + file, err := ioutil.TempFile("", "") + + if err != nil { + t.Fatalf("Cannot create temporary file: %s", err) + } + + defer os.Remove(file.Name()) + + _, err = file.WriteString("value-with-default = \"ini-value\"\n") + _, err = file.WriteString("value-with-default-override-cli = \"ini-value\"\n") + + if err != nil { + t.Fatalf("Cannot write to temporary file: %s", err) + } + + file.Close() + + var opts struct { + ValueWithDefault string `long:"value-with-default" default:"value"` + ValueWithDefaultOverrideCli string `long:"value-with-default-override-cli" default:"value"` + } + + p := NewParser(&opts, Default) + err = NewIniParser(p).ParseFile(file.Name()) + + if err != nil { + t.Fatalf("Could not parse ini: %s", err) + } + + _, err = p.ParseArgs([]string{"--value-with-default-override-cli", "cli-value"}) + + if err != nil { + t.Fatalf("Failed to parse arguments: %s", err) + } + + assertString(t, opts.ValueWithDefault, "ini-value") + assertString(t, opts.ValueWithDefaultOverrideCli, "cli-value") +} + func TestWriteFile(t *testing.T) { file, err := ioutil.TempFile("", "") if err != nil { diff --git a/option.go b/option.go index b4b867d..a7f4f9a 100644 --- a/option.go +++ b/option.go @@ -79,8 +79,9 @@ type Option struct { // Determines if the option will be always quoted in the INI output iniQuote bool - tag multiTag - isSet bool + tag multiTag + isSet bool + preventDefault bool defaultLiteral string } @@ -182,6 +183,7 @@ func (option *Option) set(value *string) error { } option.isSet = true + option.preventDefault = true if len(option.Choices) != 0 { found := false @@ -245,6 +247,7 @@ func (option *Option) empty() { func (option *Option) clearDefault() { usedDefault := option.Default + if envKey := option.EnvDefaultKey; envKey != "" { // os.Getenv() makes no distinction between undefined and // empty values, so we use syscall.Getenv() diff --git a/parser.go b/parser.go index 2f74b9a..f2de72c 100644 --- a/parser.go +++ b/parser.go @@ -281,7 +281,7 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { if s.err == nil { p.eachOption(func(c *Command, g *Group, option *Option) { - if option.isSet { + if option.preventDefault { return } From 64336ff666d6485f95a82b74902b9d8a4860e97f Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 18 Oct 2015 23:13:07 +0200 Subject: [PATCH 29/38] Add required minimum rest arguments constraint You can now specify `required:"$n"` for rest arguments --- arg.go | 3 ++ arg_test.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++ command.go | 14 ++++++++++ parser.go | 24 +++++++++++++--- 4 files changed, 117 insertions(+), 4 deletions(-) diff --git a/arg.go b/arg.go index fd8db9c..d160644 100644 --- a/arg.go +++ b/arg.go @@ -12,6 +12,9 @@ type Arg struct { // A description of the positional argument (used in the help) Description string + // Whether a positional argument is required + Required int + value reflect.Value tag multiTag } diff --git a/arg_test.go b/arg_test.go index faea280..117e90e 100644 --- a/arg_test.go +++ b/arg_test.go @@ -51,3 +51,83 @@ func TestPositionalRequired(t *testing.T) { assertError(t, err, ErrRequired, "the required argument `Filename` was not provided") } + +func TestPositionalRequiredRest1Fail(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Positional struct { + Rest []string `required:"yes"` + } `positional-args:"yes"` + }{} + + p := NewParser(&opts, None) + _, err := p.ParseArgs([]string{}) + + assertError(t, err, ErrRequired, "the required argument `Rest (at least 1 argument)` was not provided") +} + +func TestPositionalRequiredRest1Pass(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Positional struct { + Rest []string `required:"yes"` + } `positional-args:"yes"` + }{} + + p := NewParser(&opts, None) + _, err := p.ParseArgs([]string{"rest1"}) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + return + } + + if len(opts.Positional.Rest) != 1 { + t.Fatalf("Expected 1 positional rest argument") + } + + assertString(t, opts.Positional.Rest[0], "rest1") +} + +func TestPositionalRequiredRest2Fail(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Positional struct { + Rest []string `required:"2"` + } `positional-args:"yes"` + }{} + + p := NewParser(&opts, None) + _, err := p.ParseArgs([]string{"rest1"}) + + assertError(t, err, ErrRequired, "the required argument `Rest (at least 2 arguments, but got only 1)` was not provided") +} + +func TestPositionalRequiredRest2Pass(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Positional struct { + Rest []string `required:"2"` + } `positional-args:"yes"` + }{} + + p := NewParser(&opts, None) + _, err := p.ParseArgs([]string{"rest1", "rest2", "rest3"}) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + return + } + + if len(opts.Positional.Rest) != 3 { + t.Fatalf("Expected 3 positional rest argument") + } + + assertString(t, opts.Positional.Rest[0], "rest1") + assertString(t, opts.Positional.Rest[1], "rest2") + assertString(t, opts.Positional.Rest[2], "rest3") +} diff --git a/command.go b/command.go index 9184aec..5e63ecb 100644 --- a/command.go +++ b/command.go @@ -3,6 +3,7 @@ package flags import ( "reflect" "sort" + "strconv" "strings" "unsafe" ) @@ -154,9 +155,22 @@ func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler { name = field.Name } + var required int + + sreq := m.Get("required") + + if sreq != "" { + required = 1 + + if preq, err := strconv.ParseInt(sreq, 10, 32); err == nil { + required = int(preq) + } + } + arg := &Arg{ Name: name, Description: m.Get("description"), + Required: required, value: realval.Field(i), tag: m, diff --git a/parser.go b/parser.go index f2de72c..f9e07ee 100644 --- a/parser.go +++ b/parser.go @@ -357,15 +357,31 @@ func (p *parseState) checkRequired(parser *Parser) error { } if len(required) == 0 { - if len(p.positional) > 0 && p.command.ArgsRequired { + if len(p.positional) > 0 { var reqnames []string for _, arg := range p.positional { - if arg.isRemaining() { - break + argRequired := (!arg.isRemaining() && p.command.ArgsRequired) || arg.Required != 0 + + if !argRequired { + continue } - reqnames = append(reqnames, "`"+arg.Name+"`") + if arg.isRemaining() { + if arg.value.Len() < arg.Required { + var arguments string + + if arg.Required > 1 { + arguments = "arguments, but got only " + fmt.Sprintf("%d", arg.value.Len()) + } else { + arguments = "argument" + } + + reqnames = append(reqnames, "`"+arg.Name+" (at least "+fmt.Sprintf("%d", arg.Required)+" "+arguments+")`") + } + } else { + reqnames = append(reqnames, "`"+arg.Name+"`") + } } if len(reqnames) == 0 { From 4047bd797dd935ae2b557a79cc43f223066c9659 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 18 Oct 2015 23:15:10 +0200 Subject: [PATCH 30/38] Add doc for required rest positional arguments --- flags.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/flags.go b/flags.go index 6656848..757d42a 100644 --- a/flags.go +++ b/flags.go @@ -138,7 +138,13 @@ The following is a list of tags for struct fields supported by go-flags: then all remaining arguments will be added to it. Positional arguments are optional by default, unless the "required" tag is specified together - with the "positional-args" tag (optional) + with the "positional-args" tag. The "required" tag + can also be set on the individual rest argument + fields, to require only the first N positional + arguments. If the "required" tag is set on the + rest arguments slice, then its value determines + the minimum amount of rest arguments that needs to + be provided (e.g. `required:"2"`) (optional) positional-arg-name: used on a field in a positional argument struct; name of the positional argument placeholder to be shown in the help (optional) From fc93116606d0a71d7e9de0ad5734fdb4b8eae834 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Wed, 28 Oct 2015 21:52:43 +0100 Subject: [PATCH 31/38] Add FindOptionByLongName and FindOptionByShortName --- command.go | 26 +++++++++++++++++++ command_test.go | 62 ++++++++++++++++++++++++++++++++++++++++++++ group.go | 28 ++++++++++++++++++++ group_test.go | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+) diff --git a/command.go b/command.go index 5e63ecb..df6bf71 100644 --- a/command.go +++ b/command.go @@ -112,6 +112,32 @@ func (c *Command) Find(name string) *Command { return nil } +// Find an option that is part of the command, or any of its +// parent commands, by matching its long name +// (including the option namespace). +func (c *Command) FindOptionByLongName(longName string) (option *Option) { + for option == nil && c != nil { + option = c.Group.FindOptionByLongName(longName) + + c, _ = c.parent.(*Command) + } + + return option +} + +// Find an option that is part of the command, or any of its +// parent commands, by matching its long name +// (including the option namespace). +func (c *Command) FindOptionByShortName(shortName rune) (option *Option) { + for option == nil && c != nil { + option = c.Group.FindOptionByShortName(shortName) + + c, _ = c.parent.(*Command) + } + + return option +} + // Args returns a list of positional arguments associated with this command. func (c *Command) Args() []*Arg { ret := make([]*Arg, len(c.args)) diff --git a/command_test.go b/command_test.go index e64ac45..72d397d 100644 --- a/command_test.go +++ b/command_test.go @@ -480,3 +480,65 @@ func TestCommandAlias(t *testing.T) { t.Errorf("Expected G to be true") } } + +func TestSubCommandFindOptionByLongFlag(t *testing.T) { + var opts struct { + Testing bool `long:"testing" description:"Testing"` + } + + var cmd struct { + Other bool `long:"other" description:"Other"` + } + + p := NewParser(&opts, Default) + c, _ := p.AddCommand("command", "Short", "Long", &cmd) + + opt := c.FindOptionByLongName("other") + + if opt == nil { + t.Errorf("Expected option, but found none") + } + + assertString(t, opt.LongName, "other") + + opt = c.FindOptionByLongName("testing") + + if opt == nil { + t.Errorf("Expected option, but found none") + } + + assertString(t, opt.LongName, "testing") +} + +func TestSubCommandFindOptionByShortFlag(t *testing.T) { + var opts struct { + Testing bool `short:"t" description:"Testing"` + } + + var cmd struct { + Other bool `short:"o" description:"Other"` + } + + p := NewParser(&opts, Default) + c, _ := p.AddCommand("command", "Short", "Long", &cmd) + + opt := c.FindOptionByShortName('o') + + if opt == nil { + t.Errorf("Expected option, but found none") + } + + if opt.ShortName != 'o' { + t.Errorf("Expected 'o', but got %v", opt.ShortName) + } + + opt = c.FindOptionByShortName('t') + + if opt == nil { + t.Errorf("Expected option, but found none") + } + + if opt.ShortName != 't' { + t.Errorf("Expected 'o', but got %v", opt.ShortName) + } +} diff --git a/group.go b/group.go index 8ab809e..0a40b39 100644 --- a/group.go +++ b/group.go @@ -98,6 +98,34 @@ func (g *Group) Find(shortDescription string) *Group { return ret } +func (g *Group) findOption(matcher func(*Option) bool) (option *Option) { + g.eachGroup(func(g *Group) { + for _, opt := range g.options { + if option == nil && matcher(opt) { + option = opt + } + } + }) + + return option +} + +// Find an option that is part of the group, or any of its subgroups, +// by matching its long name (including the option namespace). +func (g *Group) FindOptionByLongName(longName string) *Option { + return g.findOption(func(option *Option) bool { + return option.LongNameWithNamespace() == longName + }) +} + +// Find an option that is part of the group, or any of its subgroups, +// by matching its short name. +func (g *Group) FindOptionByShortName(shortName rune) *Option { + return g.findOption(func(option *Option) bool { + return option.ShortName == shortName + }) +} + func newGroup(shortDescription string, longDescription string, data interface{}) *Group { return &Group{ ShortDescription: shortDescription, diff --git a/group_test.go b/group_test.go index b5ed9d4..18cd6c1 100644 --- a/group_test.go +++ b/group_test.go @@ -185,3 +185,71 @@ func TestDuplicateLongFlags(t *testing.T) { } } } + +func TestFindOptionByLongFlag(t *testing.T) { + var opts struct { + Testing bool `long:"testing" description:"Testing"` + } + + p := NewParser(&opts, Default) + opt := p.FindOptionByLongName("testing") + + if opt == nil { + t.Errorf("Expected option, but found none") + } + + assertString(t, opt.LongName, "testing") +} + +func TestFindOptionByShortFlag(t *testing.T) { + var opts struct { + Testing bool `short:"t" description:"Testing"` + } + + p := NewParser(&opts, Default) + opt := p.FindOptionByShortName('t') + + if opt == nil { + t.Errorf("Expected option, but found none") + } + + if opt.ShortName != 't' { + t.Errorf("Expected 't', but got %v", opt.ShortName) + } +} + +func TestFindOptionByLongFlagInSubGroup(t *testing.T) { + var opts struct { + Group struct { + Testing bool `long:"testing" description:"Testing"` + } `group:"sub-group"` + } + + p := NewParser(&opts, Default) + opt := p.FindOptionByLongName("testing") + + if opt == nil { + t.Errorf("Expected option, but found none") + } + + assertString(t, opt.LongName, "testing") +} + +func TestFindOptionByShortFlagInSubGroup(t *testing.T) { + var opts struct { + Group struct { + Testing bool `short:"t" description:"Testing"` + } `group:"sub-group"` + } + + p := NewParser(&opts, Default) + opt := p.FindOptionByShortName('t') + + if opt == nil { + t.Errorf("Expected option, but found none") + } + + if opt.ShortName != 't' { + t.Errorf("Expected 't', but got %v", opt.ShortName) + } +} From e1bd911359b4156d803229d68071cdb9f8123712 Mon Sep 17 00:00:00 2001 From: dmitriy kalinin Date: Sun, 15 Nov 2015 10:09:56 -0800 Subject: [PATCH 32/38] check err before using subc --- command.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/command.go b/command.go index df6bf71..a30f560 100644 --- a/command.go +++ b/command.go @@ -223,13 +223,12 @@ func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler { aliases := mtag.GetMany("alias") subc, err := c.AddCommand(subcommand, shortDescription, longDescription, ptrval.Interface()) - - subc.Hidden = mtag.Get("hidden") != "" - if err != nil { return true, err } + subc.Hidden = mtag.Get("hidden") != "" + if len(subcommandsOptional) > 0 { subc.SubcommandsOptional = true } From cfa9457254276436b3df23e6aeb10d1ddbb41644 Mon Sep 17 00:00:00 2001 From: Steven Wilkin Date: Thu, 10 Dec 2015 16:06:40 +0000 Subject: [PATCH 33/38] Highlight code examples as Go --- README.md | 176 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 90 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index b6faef6..9378b76 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,11 @@ The flags package uses structs, reflection and struct field tags to allow users to specify command line options. This results in very simple and concise specification of your application options. For example: - type Options struct { - Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"` - } +```go +type Options struct { + Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"` +} +``` This specifies one option with a short name -v and a long name --verbose. When either -v or --verbose is found on the command line, a 'true' value @@ -44,88 +46,90 @@ resulting value of Verbose will be {[true, true, true]}. Example: -------- - var opts struct { - // Slice of bool will append 'true' each time the option - // is encountered (can be set multiple times, like -vvv) - Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"` - - // Example of automatic marshalling to desired type (uint) - Offset uint `long:"offset" description:"Offset"` - - // Example of a callback, called each time the option is found. - Call func(string) `short:"c" description:"Call phone number"` - - // Example of a required flag - Name string `short:"n" long:"name" description:"A name" required:"true"` - - // Example of a value name - File string `short:"f" long:"file" description:"A file" value-name:"FILE"` - - // Example of a pointer - Ptr *int `short:"p" description:"A pointer to an integer"` - - // Example of a slice of strings - StringSlice []string `short:"s" description:"A slice of strings"` - - // Example of a slice of pointers - PtrSlice []*string `long:"ptrslice" description:"A slice of pointers to string"` - - // Example of a map - IntMap map[string]int `long:"intmap" description:"A map from string to int"` - } - - // Callback which will invoke callto: to call a number. - // Note that this works just on OS X (and probably only with - // Skype) but it shows the idea. - opts.Call = func(num string) { - cmd := exec.Command("open", "callto:"+num) - cmd.Start() - cmd.Process.Release() - } - - // Make some fake arguments to parse. - args := []string{ - "-vv", - "--offset=5", - "-n", "Me", - "-p", "3", - "-s", "hello", - "-s", "world", - "--ptrslice", "hello", - "--ptrslice", "world", - "--intmap", "a:1", - "--intmap", "b:5", - "arg1", - "arg2", - "arg3", - } - - // Parse flags from `args'. Note that here we use flags.ParseArgs for - // the sake of making a working example. Normally, you would simply use - // flags.Parse(&opts) which uses os.Args - args, err := flags.ParseArgs(&opts, args) - - if err != nil { - panic(err) - os.Exit(1) - } - - fmt.Printf("Verbosity: %v\n", opts.Verbose) - fmt.Printf("Offset: %d\n", opts.Offset) - fmt.Printf("Name: %s\n", opts.Name) - fmt.Printf("Ptr: %d\n", *opts.Ptr) - fmt.Printf("StringSlice: %v\n", opts.StringSlice) - fmt.Printf("PtrSlice: [%v %v]\n", *opts.PtrSlice[0], *opts.PtrSlice[1]) - fmt.Printf("IntMap: [a:%v b:%v]\n", opts.IntMap["a"], opts.IntMap["b"]) - fmt.Printf("Remaining args: %s\n", strings.Join(args, " ")) - - // Output: Verbosity: [true true] - // Offset: 5 - // Name: Me - // Ptr: 3 - // StringSlice: [hello world] - // PtrSlice: [hello world] - // IntMap: [a:1 b:5] - // Remaining args: arg1 arg2 arg3 +```go +var opts struct { + // Slice of bool will append 'true' each time the option + // is encountered (can be set multiple times, like -vvv) + Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"` + + // Example of automatic marshalling to desired type (uint) + Offset uint `long:"offset" description:"Offset"` + + // Example of a callback, called each time the option is found. + Call func(string) `short:"c" description:"Call phone number"` + + // Example of a required flag + Name string `short:"n" long:"name" description:"A name" required:"true"` + + // Example of a value name + File string `short:"f" long:"file" description:"A file" value-name:"FILE"` + + // Example of a pointer + Ptr *int `short:"p" description:"A pointer to an integer"` + + // Example of a slice of strings + StringSlice []string `short:"s" description:"A slice of strings"` + + // Example of a slice of pointers + PtrSlice []*string `long:"ptrslice" description:"A slice of pointers to string"` + + // Example of a map + IntMap map[string]int `long:"intmap" description:"A map from string to int"` +} + +// Callback which will invoke callto: to call a number. +// Note that this works just on OS X (and probably only with +// Skype) but it shows the idea. +opts.Call = func(num string) { + cmd := exec.Command("open", "callto:"+num) + cmd.Start() + cmd.Process.Release() +} + +// Make some fake arguments to parse. +args := []string{ + "-vv", + "--offset=5", + "-n", "Me", + "-p", "3", + "-s", "hello", + "-s", "world", + "--ptrslice", "hello", + "--ptrslice", "world", + "--intmap", "a:1", + "--intmap", "b:5", + "arg1", + "arg2", + "arg3", +} + +// Parse flags from `args'. Note that here we use flags.ParseArgs for +// the sake of making a working example. Normally, you would simply use +// flags.Parse(&opts) which uses os.Args +args, err := flags.ParseArgs(&opts, args) + +if err != nil { + panic(err) + os.Exit(1) +} + +fmt.Printf("Verbosity: %v\n", opts.Verbose) +fmt.Printf("Offset: %d\n", opts.Offset) +fmt.Printf("Name: %s\n", opts.Name) +fmt.Printf("Ptr: %d\n", *opts.Ptr) +fmt.Printf("StringSlice: %v\n", opts.StringSlice) +fmt.Printf("PtrSlice: [%v %v]\n", *opts.PtrSlice[0], *opts.PtrSlice[1]) +fmt.Printf("IntMap: [a:%v b:%v]\n", opts.IntMap["a"], opts.IntMap["b"]) +fmt.Printf("Remaining args: %s\n", strings.Join(args, " ")) + +// Output: Verbosity: [true true] +// Offset: 5 +// Name: Me +// Ptr: 3 +// StringSlice: [hello world] +// PtrSlice: [hello world] +// IntMap: [a:1 b:5] +// Remaining args: arg1 arg2 arg3 +``` More information can be found in the godocs: From 46e39c771cca46f81405548ef4eef911917cba32 Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Tue, 22 Dec 2015 00:11:22 -0800 Subject: [PATCH 34/38] Parse unexported embedded option struct fields in Go 1.6 (consistent w/pre-Go 1.6) Pre-Go 1.6, the newly added TestEmbeddedUnexported test passes. In Go 1.6beta1, it fails: ``` $ go test --- FAIL: TestEmbedded (0.00s) assert_test.go:92: Unexpected parse error: unknown flag `v' ``` This commit makes the behavior consistent across Go versions. Related: https://github.com/golang/go/issues/12367, specifically the comments about how "code that assumes `f.PkgPath != nil` means a field is unexported and must be ignored must now be revised to check for `f.PkgPath != nil && !f.Anonymous` for it to walk into the embedded structs to look for exported fields contained within." --- group.go | 2 +- parser_test.go | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/group.go b/group.go index 0a40b39..078a765 100644 --- a/group.go +++ b/group.go @@ -187,7 +187,7 @@ func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, h field := stype.Field(i) // PkgName is set only for non-exported fields, which we ignore - if field.PkgPath != "" { + if field.PkgPath != "" && !field.Anonymous { continue } diff --git a/parser_test.go b/parser_test.go index fe6865c..b57dbee 100644 --- a/parser_test.go +++ b/parser_test.go @@ -471,3 +471,17 @@ func TestChoices(t *testing.T) { assertParseSuccess(t, &opts, "--choose", "v2") assertString(t, opts.Choice, "v2") } + +func TestEmbedded(t *testing.T) { + type embedded struct { + V bool `short:"v"` + } + var opts struct { + embedded + } + + assertParseSuccess(t, &opts, "-v") + if !opts.V { + t.Errorf("Expected V to be true") + } +} From 36760f67cd132b71a951106cd3feea05338035a0 Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Thu, 31 Dec 2015 08:22:44 -0800 Subject: [PATCH 35/38] ini: allow specifying group opts directly on subcommands Simplifies the mental mapping of CLI flag name to INI config property name. See https://github.com/jessevdk/go-flags/issues/157 for more information. --- group.go | 36 +++++++++++----------- ini_test.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 17 deletions(-) diff --git a/group.go b/group.go index 078a765..debb8de 100644 --- a/group.go +++ b/group.go @@ -139,27 +139,29 @@ func (g *Group) optionByName(name string, namematch func(*Option, string) bool) prio := 0 var retopt *Option - for _, opt := range g.options { - if namematch != nil && namematch(opt, name) && prio < 4 { - retopt = opt - prio = 4 - } + g.eachGroup(func(g *Group) { + for _, opt := range g.options { + if namematch != nil && namematch(opt, name) && prio < 4 { + retopt = opt + prio = 4 + } - if name == opt.field.Name && prio < 3 { - retopt = opt - prio = 3 - } + if name == opt.field.Name && prio < 3 { + retopt = opt + prio = 3 + } - if name == opt.LongNameWithNamespace() && prio < 2 { - retopt = opt - prio = 2 - } + if name == opt.LongNameWithNamespace() && prio < 2 { + retopt = opt + prio = 2 + } - if opt.ShortName != 0 && name == string(opt.ShortName) && prio < 1 { - retopt = opt - prio = 1 + if opt.ShortName != 0 && name == string(opt.ShortName) && prio < 1 { + retopt = opt + prio = 1 + } } - } + }) return retopt } diff --git a/ini_test.go b/ini_test.go index 941735e..dd7fe33 100644 --- a/ini_test.go +++ b/ini_test.go @@ -248,6 +248,92 @@ EnvDefault2 = env-def } } +func TestReadIni_flagEquivalent(t *testing.T) { + type options struct { + Opt1 bool `long:"opt1"` + + Group1 struct { + Opt2 bool `long:"opt2"` + } `group:"group1"` + + Group2 struct { + Opt3 bool `long:"opt3"` + } `group:"group2" namespace:"ns1"` + + Cmd1 struct { + Opt4 bool `long:"opt4"` + Opt5 bool `long:"foo.opt5"` + + Group1 struct { + Opt6 bool `long:"opt6"` + Opt7 bool `long:"foo.opt7"` + } `group:"group1"` + + Group2 struct { + Opt8 bool `long:"opt8"` + } `group:"group2" namespace:"ns1"` + } `command:"cmd1"` + } + + a := ` +opt1=true + +[group1] +opt2=true + +[group2] +ns1.opt3=true + +[cmd1] +opt4=true +foo.opt5=true + +[cmd1.group1] +opt6=true +foo.opt7=true + +[cmd1.group2] +ns1.opt8=true +` + b := ` +opt1=true +opt2=true +ns1.opt3=true + +[cmd1] +opt4=true +foo.opt5=true +opt6=true +foo.opt7=true +ns1.opt8=true +` + + parse := func(readIni string) (opts options, writeIni string) { + p := NewNamedParser("TestIni", Default) + p.AddGroup("Application Options", "The application options", &opts) + + inip := NewIniParser(p) + err := inip.Parse(strings.NewReader(readIni)) + + if err != nil { + t.Fatalf("Unexpected error: %s\n\nFile:\n%s", err, readIni) + } + + var b bytes.Buffer + inip.Write(&b, Default) + + return opts, b.String() + } + + aOpt, aIni := parse(a) + bOpt, bIni := parse(b) + + assertDiff(t, aIni, bIni, "") + if !reflect.DeepEqual(aOpt, bOpt) { + t.Errorf("not equal") + } +} + func TestReadIni(t *testing.T) { var opts helpOptions From 8b13cca09c34bf4d82080e4ee3ab3cc7d50f8277 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 8 Feb 2016 12:29:30 +0100 Subject: [PATCH 36/38] Don't display description-less positional args in --help This patch tweaks the behavior of --help so that positional arguments that don't have a description are not listed. This allows certain applications, that include a separate description of what each positional argument does, to suppress otherwise duplicated help entry. Signed-off-by: Zygmunt Krynicki --- help.go | 11 +++++++++-- help_test.go | 10 ++++++---- ini_test.go | 8 ++++---- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/help.go b/help.go index c0b808d..aac78de 100644 --- a/help.go +++ b/help.go @@ -412,7 +412,14 @@ func (p *Parser) WriteHelp(writer io.Writer) { } }) - if len(c.args) > 0 { + var args []*Arg + for _, arg := range c.args { + if arg.Description != "" { + args = append(args, arg) + } + } + + if len(args) > 0 { if c == p.Command { fmt.Fprintf(wr, "\nArguments:\n") } else { @@ -421,7 +428,7 @@ func (p *Parser) WriteHelp(writer io.Writer) { maxlen := aligninfo.descriptionStart() - for _, arg := range c.args { + for _, arg := range args { prefix := strings.Repeat(" ", paddingBeforeOption) fmt.Fprintf(wr, "%s%s", prefix, arg.Name) diff --git a/help_test.go b/help_test.go index 33d21bf..1dcbe7f 100644 --- a/help_test.go +++ b/help_test.go @@ -53,8 +53,9 @@ type helpOptions struct { } `command:"hidden-command" description:"A hidden command" hidden:"yes"` Args struct { - Filename string `positional-arg-name:"filename" description:"A filename"` - Number int `positional-arg-name:"num" description:"A number"` + Filename string `positional-arg-name:"filename" description:"A filename"` + Number int `positional-arg-name:"num" description:"A number"` + HiddenInHelp float32 `positional-arg-name:"hidden-in-help" required:"yes"` } `positional-args:"yes"` } @@ -84,7 +85,8 @@ func TestHelp(t *testing.T) { if runtime.GOOS == "windows" { expected = `Usage: - TestHelp [OPTIONS] [filename] [num] + TestHelp [OPTIONS] [filename] [num] [hidden-in-help] + Application Options: /v, /verbose Show verbose debug information @@ -129,7 +131,7 @@ Available commands: ` } else { expected = `Usage: - TestHelp [OPTIONS] [filename] [num] + TestHelp [OPTIONS] [filename] [num] [hidden-in-help] Application Options: -v, --verbose Show verbose debug information diff --git a/ini_test.go b/ini_test.go index dd7fe33..dfe49ce 100644 --- a/ini_test.go +++ b/ini_test.go @@ -21,7 +21,7 @@ func TestWriteIni(t *testing.T) { expected string }{ { - []string{"-vv", "--intmap=a:2", "--intmap", "b:3", "filename", "0", "command"}, + []string{"-vv", "--intmap=a:2", "--intmap", "b:3", "filename", "0", "3.14", "command"}, IniDefault, `[Application Options] ; Show verbose debug information @@ -42,7 +42,7 @@ int-map = b:3 `, }, { - []string{"-vv", "--intmap=a:2", "--intmap", "b:3", "filename", "0", "command"}, + []string{"-vv", "--intmap=a:2", "--intmap", "b:3", "filename", "0", "3.14", "command"}, IniDefault | IniIncludeDefaults, `[Application Options] ; Show verbose debug information @@ -104,7 +104,7 @@ Opt = `, }, { - []string{"filename", "0", "command"}, + []string{"filename", "0", "3.14", "command"}, IniDefault | IniIncludeDefaults | IniCommentDefaults, `[Application Options] ; Show verbose debug information @@ -164,7 +164,7 @@ EnvDefault2 = env-def `, }, { - []string{"--default=New value", "--default-array=New value", "--default-map=new:value", "filename", "0", "command"}, + []string{"--default=New value", "--default-array=New value", "--default-map=new:value", "filename", "0", "3.14", "command"}, IniDefault | IniIncludeDefaults | IniCommentDefaults, `[Application Options] ; Show verbose debug information From 7f2ab82552ae1b12e070838918ac056507c0f114 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sat, 27 Feb 2016 09:59:03 +0100 Subject: [PATCH 37/38] Do not allow default tag on boolean flags Fixes #159. --- command_test.go | 12 ++++++------ error.go | 5 +++++ group.go | 6 ++++++ marshal_test.go | 20 ++++++++++---------- option.go | 20 ++++++++++++++++++++ parser_test.go | 15 ++++++++++++++- 6 files changed, 61 insertions(+), 17 deletions(-) diff --git a/command_test.go b/command_test.go index 72d397d..e7e3089 100644 --- a/command_test.go +++ b/command_test.go @@ -430,14 +430,14 @@ func TestRequiredAllOnCommand(t *testing.T) { func TestDefaultOnCommand(t *testing.T) { var opts = struct { Command struct { - G bool `short:"g" default:"true"` + G string `short:"g" default:"value"` } `command:"cmd"` }{} assertParseSuccess(t, &opts, "cmd") - if !opts.Command.G { - t.Errorf("Expected G to be true") + if opts.Command.G != "value" { + t.Errorf("Expected G to be \"value\"") } } @@ -470,14 +470,14 @@ func TestSubcommandsOptional(t *testing.T) { func TestCommandAlias(t *testing.T) { var opts = struct { Command struct { - G bool `short:"g" default:"true"` + G string `short:"g" default:"value"` } `command:"cmd" alias:"cm"` }{} assertParseSuccess(t, &opts, "cm") - if !opts.Command.G { - t.Errorf("Expected G to be true") + if opts.Command.G != "value" { + t.Errorf("Expected G to be \"value\"") } } diff --git a/error.go b/error.go index 2f27aee..05528d8 100644 --- a/error.go +++ b/error.go @@ -55,6 +55,9 @@ const ( // ErrInvalidChoice indicates an invalid option value which only allows // a certain number of choices. ErrInvalidChoice + + // ErrInvalidTag indicates an invalid tag or invalid use of an existing tag + ErrInvalidTag ) func (e ErrorType) String() string { @@ -87,6 +90,8 @@ func (e ErrorType) String() string { return "unknown command" case ErrInvalidChoice: return "invalid choice" + case ErrInvalidTag: + return "invalid tag" } return "unrecognized error type" diff --git a/group.go b/group.go index debb8de..27d2d72 100644 --- a/group.go +++ b/group.go @@ -276,6 +276,12 @@ func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, h tag: mtag, } + if 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 off", + option.shortAndLongName()) + } + g.options = append(g.options, option) } diff --git a/marshal_test.go b/marshal_test.go index 59c9cce..095e9e4 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -5,13 +5,13 @@ import ( "testing" ) -type marshalled bool +type marshalled string func (m *marshalled) UnmarshalFlag(value string) error { if value == "yes" { - *m = true + *m = "true" } else if value == "no" { - *m = false + *m = "false" } else { return fmt.Errorf("`%s' is not a valid value, please specify `yes' or `no'", value) } @@ -20,7 +20,7 @@ func (m *marshalled) UnmarshalFlag(value string) error { } func (m marshalled) MarshalFlag() (string, error) { - if m { + if m == "true" { return "yes", nil } @@ -42,8 +42,8 @@ func TestUnmarshal(t *testing.T) { assertStringArray(t, ret, []string{}) - if !opts.Value { - t.Errorf("Expected Value to be true") + if opts.Value != "true" { + t.Errorf("Expected Value to be \"true\"") } } @@ -56,8 +56,8 @@ func TestUnmarshalDefault(t *testing.T) { assertStringArray(t, ret, []string{}) - if !opts.Value { - t.Errorf("Expected Value to be true") + if opts.Value != "true" { + t.Errorf("Expected Value to be \"true\"") } } @@ -70,8 +70,8 @@ func TestUnmarshalOptional(t *testing.T) { assertStringArray(t, ret, []string{}) - if !opts.Value { - t.Errorf("Expected Value to be true") + if opts.Value != "true" { + t.Errorf("Expected Value to be \"true\"") } } diff --git a/option.go b/option.go index a7f4f9a..b2a69c7 100644 --- a/option.go +++ b/option.go @@ -1,6 +1,7 @@ package flags import ( + "bytes" "fmt" "reflect" "strings" @@ -412,3 +413,22 @@ func (option *Option) updateDefaultLiteral() { option.defaultLiteral = def } + +func (option *Option) shortAndLongName() string { + ret := &bytes.Buffer{} + + if option.ShortName != 0 { + ret.WriteRune(defaultShortOptDelimiter) + ret.WriteRune(option.ShortName) + } + + if len(option.LongName) != 0 { + if option.ShortName != 0 { + ret.WriteRune('/') + } + + ret.WriteString(option.LongName) + } + + return ret.String() +} diff --git a/parser_test.go b/parser_test.go index b57dbee..a81983f 100644 --- a/parser_test.go +++ b/parser_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "reflect" + "runtime" "strconv" "strings" "testing" @@ -17,7 +18,7 @@ type defaultOptions struct { Float64 float64 `long:"f"` Float64Default float64 `long:"fd" default:"-3.14"` - NumericFlag bool `short:"3" default:"false"` + NumericFlag bool `short:"3"` String string `long:"str"` StringDefault string `long:"strd" default:"abc"` @@ -132,6 +133,18 @@ func TestDefaults(t *testing.T) { } } +func TestNoDefaultsForBools(t *testing.T) { + var opts struct { + DefaultBool bool `short:"d" default:"true"` + } + + if runtime.GOOS == "windows" { + assertParseFail(t, ErrInvalidTag, "boolean flag `/d' may not have default values, they always default to `false' and can only be turned off", &opts) + } else { + assertParseFail(t, ErrInvalidTag, "boolean flag `-d' may not have default values, they always default to `false' and can only be turned off", &opts) + } +} + func TestUnquoting(t *testing.T) { var tests = []struct { arg string From 6b9493b3cb60367edd942144879646604089e3f7 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sat, 27 Feb 2016 10:34:14 +0100 Subject: [PATCH 38/38] Fix error message for invalid use of default tag --- group.go | 2 +- parser_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/group.go b/group.go index 27d2d72..6472420 100644 --- a/group.go +++ b/group.go @@ -278,7 +278,7 @@ func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, h if 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 off", + "boolean flag `%s' may not have default values, they always default to `false' and can only be turned on", option.shortAndLongName()) } diff --git a/parser_test.go b/parser_test.go index a81983f..32afc69 100644 --- a/parser_test.go +++ b/parser_test.go @@ -139,9 +139,9 @@ func TestNoDefaultsForBools(t *testing.T) { } if runtime.GOOS == "windows" { - assertParseFail(t, ErrInvalidTag, "boolean flag `/d' may not have default values, they always default to `false' and can only be turned off", &opts) + assertParseFail(t, ErrInvalidTag, "boolean flag `/d' may not have default values, they always default to `false' and can only be turned on", &opts) } else { - assertParseFail(t, ErrInvalidTag, "boolean flag `-d' may not have default values, they always default to `false' and can only be turned off", &opts) + assertParseFail(t, ErrInvalidTag, "boolean flag `-d' may not have default values, they always default to `false' and can only be turned on", &opts) } }