From 3c0b5a69cda1bca403bd88a123ae8134ffbbe34c Mon Sep 17 00:00:00 2001 From: Hugo Chargois Date: Sat, 17 Dec 2016 04:29:52 +0100 Subject: [PATCH 01/71] Fix PassAfterNonOption not working with positional args --- options_test.go | 98 +++++++++++++++++++++++++++++++++++++++++++++++++ parser.go | 30 ++++++--------- 2 files changed, 109 insertions(+), 19 deletions(-) diff --git a/options_test.go b/options_test.go index b0fe9f4..110fe2f 100644 --- a/options_test.go +++ b/options_test.go @@ -1,6 +1,7 @@ package flags import ( + "strings" "testing" ) @@ -43,3 +44,100 @@ func TestPassAfterNonOption(t *testing.T) { assertStringArray(t, ret, []string{"arg", "-v", "-g"}) } + +func TestPassAfterNonOptionWithPositional(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Positional struct { + Rest []string `required:"yes"` + } `positional-args:"yes"` + }{} + + p := NewParser(&opts, PassAfterNonOption) + ret, err := p.ParseArgs([]string{"-v", "arg", "-v", "-g"}) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + return + } + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + assertStringArray(t, ret, []string{}) + assertStringArray(t, opts.Positional.Rest, []string{"arg", "-v", "-g"}) +} + +func TestPassAfterNonOptionWithPositionalIntPass(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Positional struct { + Rest []int `required:"yes"` + } `positional-args:"yes"` + }{} + + p := NewParser(&opts, PassAfterNonOption) + ret, err := p.ParseArgs([]string{"-v", "1", "2", "3"}) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + return + } + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + assertStringArray(t, ret, []string{}) + for i, rest := range opts.Positional.Rest { + if rest != i+1 { + assertErrorf(t, "Expected %v got %v", i+1, rest) + } + } +} + +func TestPassAfterNonOptionWithPositionalIntFail(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Positional struct { + Rest []int `required:"yes"` + } `positional-args:"yes"` + }{} + + tests := []struct { + opts []string + errContains string + ret []string + }{ + { + []string{"-v", "notint1", "notint2", "notint3"}, + "notint1", + []string{"notint1", "notint2", "notint3"}, + }, + { + []string{"-v", "1", "notint2", "notint3"}, + "notint2", + []string{"1", "notint2", "notint3"}, + }, + } + + for _, test := range tests { + p := NewParser(&opts, PassAfterNonOption) + ret, err := p.ParseArgs(test.opts) + + if err == nil { + assertErrorf(t, "Expected error") + return + } + + if !strings.Contains(err.Error(), test.errContains) { + assertErrorf(t, "Expected the first illegal argument in the error") + } + + assertStringArray(t, ret, test.ret) + } +} diff --git a/parser.go b/parser.go index fd2fd5f..ab21158 100644 --- a/parser.go +++ b/parser.go @@ -237,6 +237,7 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { p.fillParseState(s) for !s.eof() { + var err error arg := s.pop() // When PassDoubleDash is set and we encounter a --, then @@ -247,6 +248,15 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { } if !argumentIsOption(arg) { + if (p.Options & PassAfterNonOption) != None { + // If PassAfterNonOption is set then all remaining arguments + // are considered positional + if err = s.addArgs(s.arg); err != nil { + break + } + s.addArgs(s.args...) + break + } // Note: this also sets s.err, so we can just check for // nil here and use s.err later if p.parseNonOption(s) != nil { @@ -256,8 +266,6 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { continue } - var err error - prefix, optname, islong := stripOptionPrefix(arg) optname, _, argument := splitOption(prefix, optname, islong) @@ -649,23 +657,7 @@ func (p *Parser) parseNonOption(s *parseState) error { } } - 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 + return s.addArgs(s.arg) } func (p *Parser) showBuiltinHelp() error { From f2a18a7cbedfe56b69c79f234fa8b018825e2066 Mon Sep 17 00:00:00 2001 From: Romain Baugue Date: Wed, 23 Aug 2017 16:29:15 +0200 Subject: [PATCH 02/71] Add env namespace handling --- flags.go | 4 ++++ group.go | 4 ++++ help.go | 6 +++--- man.go | 6 +++--- option.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++- parser.go | 10 ++++++--- parser_test.go | 56 ++++++++++++++++++++++++++++++++++---------------- 7 files changed, 111 insertions(+), 28 deletions(-) diff --git a/flags.go b/flags.go index 889762d..e627c99 100644 --- a/flags.go +++ b/flags.go @@ -125,6 +125,10 @@ The following is a list of tags for struct fields supported by go-flags: gets prepended to every option's long name and subgroup's namespace of this group, separated by the parser's namespace delimiter (optional) + env-namespace: when specified on a group struct field, the env-namespace + gets prepended to every option's env key and + subgroup's env-namespace of this group, separated by + the parser's env-namespace delimiter (optional) command: when specified on a struct field, makes the struct field a (sub)command with the given name (optional) subcommands-optional: when specified on a command struct field, makes diff --git a/group.go b/group.go index 9e057ab..9341d23 100644 --- a/group.go +++ b/group.go @@ -34,6 +34,9 @@ type Group struct { // The namespace of the group Namespace string + // The environment namespace of the group + EnvNamespace string + // If true, the group is not displayed in the help or man page Hidden bool @@ -358,6 +361,7 @@ func (g *Group) scanSubGroupHandler(realval reflect.Value, sfield *reflect.Struc } group.Namespace = mtag.Get("namespace") + group.EnvNamespace = mtag.Get("env-namespace") group.Hidden = mtag.Get("hidden") != "" return true, nil diff --git a/help.go b/help.go index d380305..8e3eba9 100644 --- a/help.go +++ b/help.go @@ -225,12 +225,12 @@ func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alig } var envDef string - if option.EnvDefaultKey != "" { + if option.EnvKeyWithNamespace() != "" { var envPrintable string if runtime.GOOS == "windows" { - envPrintable = "%" + option.EnvDefaultKey + "%" + envPrintable = "%" + option.EnvKeyWithNamespace() + "%" } else { - envPrintable = "$" + option.EnvDefaultKey + envPrintable = "$" + option.EnvKeyWithNamespace() } envDef = fmt.Sprintf(" [%s]", envPrintable) } diff --git a/man.go b/man.go index 0cb114e..c2cebae 100644 --- a/man.go +++ b/man.go @@ -83,11 +83,11 @@ func writeManPageOptions(wr io.Writer, grp *Group) { if len(opt.Default) != 0 { fmt.Fprintf(wr, " ", manQuote(strings.Join(quoteV(opt.Default), ", "))) - } else if len(opt.EnvDefaultKey) != 0 { + } else if len(opt.EnvKeyWithNamespace()) != 0 { if runtime.GOOS == "windows" { - fmt.Fprintf(wr, " ", manQuote(opt.EnvDefaultKey)) + fmt.Fprintf(wr, " ", manQuote(opt.EnvKeyWithNamespace())) } else { - fmt.Fprintf(wr, " ", manQuote(opt.EnvDefaultKey)) + fmt.Fprintf(wr, " ", manQuote(opt.EnvKeyWithNamespace())) } } diff --git a/option.go b/option.go index ea09fb4..4db052f 100644 --- a/option.go +++ b/option.go @@ -139,6 +139,57 @@ func (option *Option) LongNameWithNamespace() string { return longName } +// EnvKeyWithNamespace returns the option's env key with the group namespaces +// prepended by walking up the option's group tree. Namespaces and the env key +// itself are separated by the parser's namespace delimiter. If the env key is +// empty an empty string is returned. +func (option *Option) EnvKeyWithNamespace() string { + if len(option.EnvDefaultKey) == 0 { + return "" + } + + // fetch the namespace delimiter from the parser which is always at the + // end of the group hierarchy + namespaceDelimiter := "" + g := option.group + + for { + if p, ok := g.parent.(*Parser); ok { + namespaceDelimiter = p.EnvNamespaceDelimiter + + break + } + + switch i := g.parent.(type) { + case *Command: + g = i.Group + case *Group: + g = i + } + } + + // concatenate long name with namespace + key := option.EnvDefaultKey + g = option.group + + for g != nil { + if g.EnvNamespace != "" { + key = g.EnvNamespace + namespaceDelimiter + key + } + + switch i := g.parent.(type) { + case *Command: + g = i.Group + case *Group: + g = i + case *Parser: + g = nil + } + } + + return key +} + // String converts an option to a human friendly readable string describing the // option. func (option *Option) String() string { @@ -260,7 +311,7 @@ func (option *Option) empty() { func (option *Option) clearDefault() { usedDefault := option.Default - if envKey := option.EnvDefaultKey; envKey != "" { + if envKey := option.EnvKeyWithNamespace(); envKey != "" { // os.Getenv() makes no distinction between undefined and // empty values, so we use syscall.Getenv() if value, ok := syscall.Getenv(envKey); ok { diff --git a/parser.go b/parser.go index 0a7922a..042930c 100644 --- a/parser.go +++ b/parser.go @@ -29,6 +29,9 @@ type Parser struct { // NamespaceDelimiter separates group namespaces and option long names NamespaceDelimiter string + // EnvNamespaceDelimiter separates group env namespaces and env keys + EnvNamespaceDelimiter string + // UnknownOptionsHandler is a function which gets called when the parser // encounters an unknown option. The function receives the unknown option // name, a SplitArgument which specifies its value if set with an argument @@ -170,9 +173,10 @@ func NewParser(data interface{}, options Options) *Parser { // be added to this parser by using AddGroup and AddCommand. func NewNamedParser(appname string, options Options) *Parser { p := &Parser{ - Command: newCommand(appname, "", "", nil), - Options: options, - NamespaceDelimiter: ".", + Command: newCommand(appname, "", "", nil), + Options: options, + NamespaceDelimiter: ".", + EnvNamespaceDelimiter: "_", } p.Command.parent = p diff --git a/parser_test.go b/parser_test.go index 374f21c..f0c768d 100644 --- a/parser_test.go +++ b/parser_test.go @@ -245,11 +245,16 @@ func EnvSnapshot() *EnvRestorer { return &r } +type envNestedOptions struct { + Foo string `long:"foo" default:"z" env:"FOO"` +} + type envDefaultOptions struct { - Int int `long:"i" default:"1" env:"TEST_I"` - Time time.Duration `long:"t" default:"1m" env:"TEST_T"` - Map map[string]int `long:"m" default:"a:1" env:"TEST_M" env-delim:";"` - Slice []int `long:"s" default:"1" default:"2" env:"TEST_S" env-delim:","` + Int int `long:"i" default:"1" env:"TEST_I"` + Time time.Duration `long:"t" default:"1m" env:"TEST_T"` + Map map[string]int `long:"m" default:"a:1" env:"TEST_M" env-delim:";"` + Slice []int `long:"s" default:"1" default:"2" env:"TEST_S" env-delim:","` + Nested envNestedOptions `group:"nested" namespace:"nested" env-namespace:"NESTED"` } func TestEnvDefaults(t *testing.T) { @@ -267,6 +272,9 @@ func TestEnvDefaults(t *testing.T) { Time: time.Minute, Map: map[string]int{"a": 1}, Slice: []int{1, 2}, + Nested: envNestedOptions{ + Foo: "z", + }, }, }, { @@ -277,44 +285,56 @@ func TestEnvDefaults(t *testing.T) { Time: 2 * time.Minute, Map: map[string]int{"a": 2, "b": 3}, Slice: []int{4, 5, 6}, + Nested: envNestedOptions{ + Foo: "a", + }, }, env: map[string]string{ - "TEST_I": "2", - "TEST_T": "2m", - "TEST_M": "a:2;b:3", - "TEST_S": "4,5,6", + "TEST_I": "2", + "TEST_T": "2m", + "TEST_M": "a:2;b:3", + "TEST_S": "4,5,6", + "NESTED_FOO": "a", }, }, { msg: "non-zero value arguments, expecting overwritten arguments", - args: []string{"--i=3", "--t=3ms", "--m=c:3", "--s=3"}, + args: []string{"--i=3", "--t=3ms", "--m=c:3", "--s=3", "--nested.foo=\"p\""}, expected: envDefaultOptions{ Int: 3, Time: 3 * time.Millisecond, Map: map[string]int{"c": 3}, Slice: []int{3}, + Nested: envNestedOptions{ + Foo: "p", + }, }, env: map[string]string{ - "TEST_I": "2", - "TEST_T": "2m", - "TEST_M": "a:2;b:3", - "TEST_S": "4,5,6", + "TEST_I": "2", + "TEST_T": "2m", + "TEST_M": "a:2;b:3", + "TEST_S": "4,5,6", + "NESTED_FOO": "a", }, }, { msg: "zero value arguments, expecting overwritten arguments", - args: []string{"--i=0", "--t=0ms", "--m=:0", "--s=0"}, + args: []string{"--i=0", "--t=0ms", "--m=:0", "--s=0", "--nested.foo=\"\""}, expected: envDefaultOptions{ Int: 0, Time: 0, Map: map[string]int{"": 0}, Slice: []int{0}, + Nested: envNestedOptions{ + Foo: "", + }, }, env: map[string]string{ - "TEST_I": "2", - "TEST_T": "2m", - "TEST_M": "a:2;b:3", - "TEST_S": "4,5,6", + "TEST_I": "2", + "TEST_T": "2m", + "TEST_M": "a:2;b:3", + "TEST_S": "4,5,6", + "NESTED_FOO": "a", }, }, } From 1ba780057e80fcc1205f6ef0d885425e44b6d10d Mon Sep 17 00:00:00 2001 From: Peter Ebden Date: Wed, 6 Dec 2017 15:20:10 +0000 Subject: [PATCH 03/71] Add filepath as an equivalent to filename which completes into subdirectories. I have some cases where this was the behaviour I wanted; obviously users can implement this themselves but it seems pretty generally useful (probably as much so as Filename?). --- completion.go | 16 ++++++++++++++++ completion_test.go | 22 ++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/completion.go b/completion.go index 7a7a08b..4e4b002 100644 --- a/completion.go +++ b/completion.go @@ -2,6 +2,7 @@ package flags import ( "fmt" + "os" "path/filepath" "reflect" "sort" @@ -48,6 +49,9 @@ type completion struct { // Filename is a string alias which provides filename completion. type Filename string +// Filepath is a string alias which provides completion for file paths into subdirectories. +type Filepath string + func completionsWithoutDescriptions(items []string) []Completion { ret := make([]Completion, len(items)) @@ -65,6 +69,18 @@ func (f *Filename) Complete(match string) []Completion { return completionsWithoutDescriptions(ret) } +// Complete returns a list of existing files with the given prefix. +// If it completes to a single directory, the contents of that directory are interrogated. +func (f *Filepath) Complete(match string) []Completion { + ret, _ := filepath.Glob(match + "*") + if len(ret) == 1 { + if info, err := os.Stat(ret[0]); err == nil && info.IsDir() { + ret, _ = filepath.Glob(ret[0] + "/*") + } + } + return completionsWithoutDescriptions(ret) +} + func (c *completion) skipPositional(s *parseState, n int) { if n >= len(s.positional) { s.positional = nil diff --git a/completion_test.go b/completion_test.go index 26f70e4..9bcf40b 100644 --- a/completion_test.go +++ b/completion_test.go @@ -63,6 +63,7 @@ var completionTestOptions struct { RemoveCommand struct { Other bool `short:"o"` File Filename `short:"f" long:"filename"` + Dir Filepath `short:"d" long:"dir"` } `command:"rm" description:"remove an item"` RenameCommand struct { @@ -84,6 +85,13 @@ func init() { completionTestFilename := []string{filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion_test.go")} + completionTestFilepath := []string{ + filepath.Join(completionTestSourcedir, "examples/add.go"), + filepath.Join(completionTestSourcedir, "examples/bash-completion"), + filepath.Join(completionTestSourcedir, "examples/main.go"), + filepath.Join(completionTestSourcedir, "examples/rm.go"), + } + completionTests = []completionTest{ { // Short names @@ -227,6 +235,20 @@ func init() { false, }, + { + // Flag filepath file + []string{"rm", "-d", path.Join(completionTestSourcedir, "completion")}, + completionTestFilename, + false, + }, + + { + // Flag filepath dir + []string{"rm", "-d", path.Join(completionTestSourcedir, "examples")}, + completionTestFilepath, + false, + }, + { // Custom completed []string{"rename", "-c", "hello un"}, From 919b882502014d3846d5d24bdb263a9a9497cadf Mon Sep 17 00:00:00 2001 From: Peter Ebden Date: Sat, 28 Apr 2018 08:25:31 +0100 Subject: [PATCH 04/71] Add it to Filename instead --- completion.go | 10 ---------- completion_test.go | 16 ++++------------ 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/completion.go b/completion.go index 4e4b002..59bd61c 100644 --- a/completion.go +++ b/completion.go @@ -49,9 +49,6 @@ type completion struct { // Filename is a string alias which provides filename completion. type Filename string -// Filepath is a string alias which provides completion for file paths into subdirectories. -type Filepath string - func completionsWithoutDescriptions(items []string) []Completion { ret := make([]Completion, len(items)) @@ -65,13 +62,6 @@ func completionsWithoutDescriptions(items []string) []Completion { // Complete returns a list of existing files with the given // prefix. func (f *Filename) Complete(match string) []Completion { - ret, _ := filepath.Glob(match + "*") - return completionsWithoutDescriptions(ret) -} - -// Complete returns a list of existing files with the given prefix. -// If it completes to a single directory, the contents of that directory are interrogated. -func (f *Filepath) Complete(match string) []Completion { ret, _ := filepath.Glob(match + "*") if len(ret) == 1 { if info, err := os.Stat(ret[0]); err == nil && info.IsDir() { diff --git a/completion_test.go b/completion_test.go index 9bcf40b..dba5458 100644 --- a/completion_test.go +++ b/completion_test.go @@ -63,7 +63,6 @@ var completionTestOptions struct { RemoveCommand struct { Other bool `short:"o"` File Filename `short:"f" long:"filename"` - Dir Filepath `short:"d" long:"dir"` } `command:"rm" description:"remove an item"` RenameCommand struct { @@ -85,7 +84,7 @@ func init() { completionTestFilename := []string{filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion_test.go")} - completionTestFilepath := []string{ + completionTestSubdir := []string{ filepath.Join(completionTestSourcedir, "examples/add.go"), filepath.Join(completionTestSourcedir, "examples/bash-completion"), filepath.Join(completionTestSourcedir, "examples/main.go"), @@ -236,16 +235,9 @@ func init() { }, { - // Flag filepath file - []string{"rm", "-d", path.Join(completionTestSourcedir, "completion")}, - completionTestFilename, - false, - }, - - { - // Flag filepath dir - []string{"rm", "-d", path.Join(completionTestSourcedir, "examples")}, - completionTestFilepath, + // Subdirectory + []string{"rm", "--filename", path.Join(completionTestSourcedir, "examples")}, + completionTestSubdir, false, }, From b87a73dcfd266a0042e072c82587a615669e3703 Mon Sep 17 00:00:00 2001 From: Denis V Date: Sun, 20 May 2018 09:14:59 +0200 Subject: [PATCH 05/71] Support windows console width detection --- termsize_nosysioctl.go | 2 +- termsize_windows.go | 85 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 termsize_windows.go diff --git a/termsize_nosysioctl.go b/termsize_nosysioctl.go index 3d5385b..68a6fa6 100644 --- a/termsize_nosysioctl.go +++ b/termsize_nosysioctl.go @@ -1,4 +1,4 @@ -// +build windows plan9 solaris appengine +// +build plan9 solaris appengine package flags diff --git a/termsize_windows.go b/termsize_windows.go new file mode 100644 index 0000000..5c0fa6b --- /dev/null +++ b/termsize_windows.go @@ -0,0 +1,85 @@ +// +build windows + +package flags + +import ( + "syscall" + "unsafe" +) + +type ( + SHORT int16 + WORD uint16 + + SMALL_RECT struct { + Left SHORT + Top SHORT + Right SHORT + Bottom SHORT + } + + COORD struct { + X SHORT + Y SHORT + } + + CONSOLE_SCREEN_BUFFER_INFO struct { + Size COORD + CursorPosition COORD + Attributes WORD + Window SMALL_RECT + MaximumWindowSize COORD + } +) + +var kernel32DLL = syscall.NewLazyDLL("kernel32.dll") +var getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo") + +func getError(r1, r2 uintptr, lastErr error) error { + // If the function fails, the return value is zero. + if r1 == 0 { + if lastErr != nil { + return lastErr + } + return syscall.EINVAL + } + return nil +} + +func getStdHandle(stdhandle int) (uintptr, error) { + handle, err := syscall.GetStdHandle(stdhandle) + if err != nil { + return 0, err + } + return uintptr(handle), nil +} + +// GetConsoleScreenBufferInfo retrieves information about the specified console screen buffer. +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx +func GetConsoleScreenBufferInfo(handle uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) { + var info CONSOLE_SCREEN_BUFFER_INFO + if err := getError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0)); err != nil { + return nil, err + } + return &info, nil +} + +func getTerminalColumns() int { + defaultWidth := 80 + + stdoutHandle, err := getStdHandle(syscall.STD_OUTPUT_HANDLE) + if err != nil { + return defaultWidth + } + + info, err := GetConsoleScreenBufferInfo(stdoutHandle) + if err != nil { + return defaultWidth + } + + if info.MaximumWindowSize.X > 0 { + return int(info.MaximumWindowSize.X) + } + + return defaultWidth +} From 8766aaeab5ca44d0eceae49614346e28ab11c439 Mon Sep 17 00:00:00 2001 From: iamgnat Date: Thu, 21 Jun 2018 12:23:39 -0400 Subject: [PATCH 06/71] Adding WroteHelp() helper --- help.go | 20 ++++++++++++++++++++ help_test.go | 23 +++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/help.go b/help.go index 8e3eba9..4743cc9 100644 --- a/help.go +++ b/help.go @@ -489,3 +489,23 @@ func (p *Parser) WriteHelp(writer io.Writer) { wr.Flush() } + +// WroteHelp is a helper to test the error from ParseArgs() to +// determine if the help message was written. It is safe to +// call without first checking that error is nil. +func WroteHelp(err error) bool { + if err == nil { // No error + return false + } + + flagError, ok := err.(*Error) + if !ok { // Not a go-flag error + return false + } + + if flagError.Type != ErrHelp { // Did not print the help message + return false + } + + return true +} diff --git a/help_test.go b/help_test.go index bb76640..7faea34 100644 --- a/help_test.go +++ b/help_test.go @@ -3,6 +3,7 @@ package flags import ( "bufio" "bytes" + "errors" "fmt" "os" "runtime" @@ -536,3 +537,25 @@ func TestHelpDefaultMask(t *testing.T) { } } } + +func TestWroteHelp(t *testing.T) { + type testInfo struct { + value error + isHelp bool + } + tests := map[string]testInfo{ + "No error": testInfo{value: nil, isHelp: false}, + "Plain error": testInfo{value: errors.New("an error"), isHelp: false}, + "ErrUnknown": testInfo{value: newError(ErrUnknown, "an error"), isHelp: false}, + "ErrHelp": testInfo{value: newError(ErrHelp, "an error"), isHelp: true}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + res := WroteHelp(test.value) + if test.isHelp != res { + t.Errorf("Expected %t, got %t", test.isHelp, res) + } + }) + } +} From 19532f25ea64b1064e5e7a93380993732ddc321b Mon Sep 17 00:00:00 2001 From: Tim Abell Date: Wed, 29 Aug 2018 08:45:51 +0100 Subject: [PATCH 07/71] Explain how to use choices --- flags.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flags.go b/flags.go index e627c99..ac2157d 100644 --- a/flags.go +++ b/flags.go @@ -109,7 +109,8 @@ The following is a list of tags for struct fields supported by go-flags: 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 multiple times (optional) + Repeat this tag once for each allowable value. + e.g. `long:"animal" choice:"cat" choice:"dog"` hidden: if non-empty, the option is not visible in the help or man page. base: a base (radix) used to convert strings to integer values, the From cb19eb63476c1d6fa327370cb7eaf4b6b84a90e1 Mon Sep 17 00:00:00 2001 From: Tim Abell Date: Wed, 29 Aug 2018 08:53:06 +0100 Subject: [PATCH 08/71] Add choice example to readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 3b02394..f8d8eb7 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,9 @@ var opts struct { // Example of a required flag Name string `short:"n" long:"name" description:"A name" required:"true"` + // Example of a flag restricted to a pre-defined set of strings + Name string `long:"animal" choice:"cat" choice:"dog"` + // Example of a value name File string `short:"f" long:"file" description:"A file" value-name:"FILE"` From bf684559d1194590c2ad87ee58a8eb8c98bb7995 Mon Sep 17 00:00:00 2001 From: Matheus Degiovani Date: Thu, 6 Sep 2018 14:41:27 -0300 Subject: [PATCH 09/71] Fix termsize in wasm arch --- termsize.go | 2 +- termsize_nosysioctl.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/termsize.go b/termsize.go index 1ca6a85..29b1110 100644 --- a/termsize.go +++ b/termsize.go @@ -1,4 +1,4 @@ -// +build !windows,!plan9,!solaris,!appengine +// +build !windows,!plan9,!solaris,!appengine,!wasm package flags diff --git a/termsize_nosysioctl.go b/termsize_nosysioctl.go index 3d5385b..ba6c309 100644 --- a/termsize_nosysioctl.go +++ b/termsize_nosysioctl.go @@ -1,4 +1,4 @@ -// +build windows plan9 solaris appengine +// +build windows plan9 solaris appengine wasm package flags From 14b8957836290f46f473f39b104761889b2f0af0 Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Sat, 22 Sep 2018 21:47:51 +0100 Subject: [PATCH 10/71] This fixes issue #269 and #270 It changes the definition of `(*Option).canCli()` to also check whether an option is hidden. There's still some work to be done in this area as a group with no non-hidden options will also not render right, but I'll get to that later. --- help.go | 2 +- help_test.go | 20 +++++++++++++++++--- man.go | 2 +- option.go | 2 +- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/help.go b/help.go index 8e3eba9..c73e492 100644 --- a/help.go +++ b/help.go @@ -393,7 +393,7 @@ func (p *Parser) WriteHelp(writer io.Writer) { } for _, info := range grp.options { - if !info.canCli() || info.Hidden { + if !info.canCli() { continue } diff --git a/help_test.go b/help_test.go index bb76640..bc10f0f 100644 --- a/help_test.go +++ b/help_test.go @@ -26,6 +26,8 @@ type helpOptions struct { 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"` + HiddenOptionWithVeryLongName bool `long:"this-hidden-option-has-a-ridiculously-long-name" hidden:"yes"` + OnlyIni string `ini-name:"only-ini" description:"Option only available in ini"` Other struct { @@ -47,6 +49,10 @@ type helpOptions struct { } `group:"Subsubgroup" namespace:"sap"` } `group:"Subgroup" namespace:"sip"` + Bommand struct { + Hidden bool `long:"hidden" description:"A hidden option" hidden:"yes"` + } `command:"bommand" description:"A command with only hidden options"` + Command struct { ExtraVerbose []bool `long:"extra-verbose" description:"Use for extra verbosity"` } `command:"command" alias:"cm" alias:"cmd" description:"A command"` @@ -88,7 +94,7 @@ func TestHelp(t *testing.T) { if runtime.GOOS == "windows" { expected = `Usage: - TestHelp [OPTIONS] [filename] [num] [hidden-in-help] + TestHelp [OPTIONS] [filename] [num] [hidden-in-help] Application Options: /v, /verbose Show verbose debug information @@ -131,11 +137,12 @@ Arguments: num: A number Available commands: + bommand A command with only hidden options command A command (aliases: cm, cmd) ` } else { expected = `Usage: - TestHelp [OPTIONS] [filename] [num] [hidden-in-help] + TestHelp [OPTIONS] [filename] [num] [hidden-in-help] Application Options: -v, --verbose Show verbose debug information @@ -177,6 +184,7 @@ Arguments: num: A number Available commands: + bommand A command with only hidden options command A command (aliases: cm, cmd) ` } @@ -196,7 +204,9 @@ func TestMan(t *testing.T) { p.LongDescription = "This is a somewhat `longer' description of what this does" p.AddGroup("Application Options", "The application options", &opts) - p.Commands()[0].LongDescription = "Longer `command' description" + for _, cmd := range p.Commands() { + cmd.LongDescription = fmt.Sprintf("Longer `%s' description", cmd.Name) + } var buf bytes.Buffer p.WriteManPage(&buf) @@ -274,6 +284,10 @@ Not hidden inside group \fB\fB\-\-sip.sap.opt\fR\fP This is a subsubgroup option .SH COMMANDS +.SS bommand +A command with only hidden options + +Longer \fBbommand\fP description .SS command A command diff --git a/man.go b/man.go index c2cebae..ad1e939 100644 --- a/man.go +++ b/man.go @@ -54,7 +54,7 @@ func writeManPageOptions(wr io.Writer, grp *Group) { } for _, opt := range group.options { - if !opt.canCli() || opt.Hidden { + if !opt.canCli() { continue } diff --git a/option.go b/option.go index c681c39..f33cd8c 100644 --- a/option.go +++ b/option.go @@ -281,7 +281,7 @@ func (option *Option) set(value *string) error { } func (option *Option) canCli() bool { - return option.ShortName != 0 || len(option.LongName) != 0 + return !option.Hidden && (option.ShortName != 0 || len(option.LongName) != 0) } func (option *Option) canArgument() bool { From fe2106393318e27f95efc9d54a567a79b9e73ded Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 23 Sep 2018 08:42:30 +0200 Subject: [PATCH 11/71] Recurse completion of single directory down --- completion.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/completion.go b/completion.go index 59bd61c..26700cc 100644 --- a/completion.go +++ b/completion.go @@ -65,7 +65,7 @@ func (f *Filename) Complete(match string) []Completion { ret, _ := filepath.Glob(match + "*") if len(ret) == 1 { if info, err := os.Stat(ret[0]); err == nil && info.IsDir() { - ret, _ = filepath.Glob(ret[0] + "/*") + return f.Complete(ret[0] + "/") } } return completionsWithoutDescriptions(ret) From 8af184c9a05f7155f272cbdda7e86ea77ff3791d Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 23 Sep 2018 08:54:08 +0200 Subject: [PATCH 12/71] Revert "Recurse completion of single directory down" This reverts commit fe2106393318e27f95efc9d54a567a79b9e73ded. --- completion.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/completion.go b/completion.go index 26700cc..59bd61c 100644 --- a/completion.go +++ b/completion.go @@ -65,7 +65,7 @@ func (f *Filename) Complete(match string) []Completion { ret, _ := filepath.Glob(match + "*") if len(ret) == 1 { if info, err := os.Stat(ret[0]); err == nil && info.IsDir() { - return f.Complete(ret[0] + "/") + ret, _ = filepath.Glob(ret[0] + "/*") } } return completionsWithoutDescriptions(ret) From 96f66a7c6aab3b4d63231a1f317fe0d8948a4b33 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 23 Sep 2018 08:59:06 +0200 Subject: [PATCH 13/71] Add trailing slash when completing single directory --- completion.go | 2 +- completion_test.go | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/completion.go b/completion.go index 59bd61c..813a672 100644 --- a/completion.go +++ b/completion.go @@ -65,7 +65,7 @@ func (f *Filename) Complete(match string) []Completion { ret, _ := filepath.Glob(match + "*") if len(ret) == 1 { if info, err := os.Stat(ret[0]); err == nil && info.IsDir() { - ret, _ = filepath.Glob(ret[0] + "/*") + ret[0] = ret[0] + "/" } } return completionsWithoutDescriptions(ret) diff --git a/completion_test.go b/completion_test.go index dba5458..4bd758d 100644 --- a/completion_test.go +++ b/completion_test.go @@ -234,9 +234,16 @@ func init() { false, }, + { + // To subdir + []string{"rm", "--filename", path.Join(completionTestSourcedir, "examples/bash-")}, + []string{path.Join(completionTestSourcedir, "examples/bash-completion/")}, + false, + }, + { // Subdirectory - []string{"rm", "--filename", path.Join(completionTestSourcedir, "examples")}, + []string{"rm", "--filename", path.Join(completionTestSourcedir, "examples") + "/"}, completionTestSubdir, false, }, From d761e615ee63248ed6332c774a0bc2dbf3d1ae48 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 23 Sep 2018 09:16:17 +0200 Subject: [PATCH 14/71] Rename canCli to showInHelp --- command.go | 4 ++-- help.go | 6 +++--- man.go | 6 +++--- option.go | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/command.go b/command.go index 486bacb..879465d 100644 --- a/command.go +++ b/command.go @@ -438,7 +438,7 @@ func (c *Command) match(name string) bool { return false } -func (c *Command) hasCliOptions() bool { +func (c *Command) hasHelpOptions() bool { ret := false c.eachGroup(func(g *Group) { @@ -447,7 +447,7 @@ func (c *Command) hasCliOptions() bool { } for _, opt := range g.options { - if opt.canCli() { + if opt.showInHelp() { ret = true } } diff --git a/help.go b/help.go index c73e492..d2754fa 100644 --- a/help.go +++ b/help.go @@ -79,7 +79,7 @@ func (p *Parser) getAlignmentInfo() alignmentInfo { } for _, info := range grp.options { - if !info.canCli() { + if !info.showInHelp() { continue } @@ -305,7 +305,7 @@ func (p *Parser) WriteHelp(writer io.Writer) { } } else if us, ok := allcmd.data.(Usage); ok { usage = us.Usage() - } else if allcmd.hasCliOptions() { + } else if allcmd.hasHelpOptions() { usage = fmt.Sprintf("[%s-OPTIONS]", allcmd.Name) } @@ -393,7 +393,7 @@ func (p *Parser) WriteHelp(writer io.Writer) { } for _, info := range grp.options { - if !info.canCli() { + if !info.showInHelp() { continue } diff --git a/man.go b/man.go index ad1e939..e3cc723 100644 --- a/man.go +++ b/man.go @@ -54,7 +54,7 @@ func writeManPageOptions(wr io.Writer, grp *Group) { } for _, opt := range group.options { - if !opt.canCli() { + if !opt.showInHelp() { continue } @@ -148,12 +148,12 @@ func writeManPageCommand(wr io.Writer, name string, root *Command, command *Comm var usage string if us, ok := command.data.(Usage); ok { usage = us.Usage() - } else if command.hasCliOptions() { + } else if command.hasHelpOptions() { usage = fmt.Sprintf("[%s-OPTIONS]", command.Name) } var pre string - if root.hasCliOptions() { + if root.hasHelpOptions() { pre = fmt.Sprintf("%s [OPTIONS] %s", root.Name, command.Name) } else { pre = fmt.Sprintf("%s %s", root.Name, command.Name) diff --git a/option.go b/option.go index f33cd8c..8e306d9 100644 --- a/option.go +++ b/option.go @@ -280,7 +280,7 @@ func (option *Option) set(value *string) error { return convert("", option.value, option.tag) } -func (option *Option) canCli() bool { +func (option *Option) showInHelp() bool { return !option.Hidden && (option.ShortName != 0 || len(option.LongName) != 0) } From 5e95b3a0401b96a18250d374726bc654214ece8f Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 23 Sep 2018 09:30:17 +0200 Subject: [PATCH 15/71] Catch error on s.addArgs --- parser.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/parser.go b/parser.go index 8c8216a..d75a17c 100644 --- a/parser.go +++ b/parser.go @@ -258,9 +258,14 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { if err = s.addArgs(s.arg); err != nil { break } - s.addArgs(s.args...) + + if err = s.addArgs(s.args...); err != nil { + break + } + break } + // Note: this also sets s.err, so we can just check for // nil here and use s.err later if p.parseNonOption(s) != nil { From 426f942845048a5a015df8801a51553fd791b3fd Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Mon, 24 Sep 2018 09:46:34 +0100 Subject: [PATCH 16/71] Fix issue #273 Without this change, a non-hidden group with only hidden options will still appear in the generated manpage. This fixes that. --- group.go | 12 ++++++++++++ help_test.go | 4 ++++ man.go | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/group.go b/group.go index 9341d23..ec909e5 100644 --- a/group.go +++ b/group.go @@ -168,6 +168,18 @@ func (g *Group) optionByName(name string, namematch func(*Option, string) bool) return retopt } +func (g *Group) showInHelp() bool { + if g.Hidden { + return false + } + for _, opt := range g.options { + if opt.showInHelp() { + return true + } + } + return false +} + func (g *Group) eachGroup(f func(*Group)) { f(g) diff --git a/help_test.go b/help_test.go index 1ed4397..352e41c 100644 --- a/help_test.go +++ b/help_test.go @@ -40,6 +40,10 @@ type helpOptions struct { InsideHiddenGroup string `long:"inside-hidden-group" description:"Inside hidden group"` } `group:"Hidden group" hidden:"yes"` + GroupWithOnlyHiddenOptions struct { + SecretFlag bool `long:"secret" description:"Hidden flag in a non-hidden group" hidden:"yes"` + } `group:"Non-hidden group with only hidden options"` + Group struct { Opt string `long:"opt" description:"This is a subgroup option"` HiddenInsideGroup string `long:"hidden-inside-group" description:"Hidden inside group" hidden:"yes"` diff --git a/man.go b/man.go index e3cc723..a986c6e 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 group.Hidden || len(group.options) == 0 { + if !group.showInHelp() { return } From fb5b1ac074966e47591e75ec7af40ea197a12f90 Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Wed, 26 Sep 2018 15:44:00 +0100 Subject: [PATCH 17/71] Fix issue #275 This change restores the old behaviour of PassAfterNonOption working together with commands so that a command would (also) get everything after the first non-option, while also preserving the new behaviour of those non-options landing in the positional array if provided. The behaviour for when a command and a positional array are both present continues to be weird (but it's unclear whether there's a non-weird way out of that one other than failing with a "don't do that"). --- options_test.go | 35 +++++++++++++++++++++++++++++++++++ parser.go | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/options_test.go b/options_test.go index 110fe2f..0b14205 100644 --- a/options_test.go +++ b/options_test.go @@ -45,6 +45,41 @@ func TestPassAfterNonOption(t *testing.T) { assertStringArray(t, ret, []string{"arg", "-v", "-g"}) } +type fooCmd struct { + Flag bool `short:"f"` + args []string +} + +func (foo *fooCmd) Execute(s []string) error { + foo.args = s + return nil +} + +func TestPassAfterNonOptionWithCommand(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + Foo fooCmd `command:"foo"` + }{} + p := NewParser(&opts, PassAfterNonOption) + ret, err := p.ParseArgs([]string{"-v", "foo", "-f", "bar", "-v", "-g"}) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + return + } + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if !opts.Foo.Flag { + t.Errorf("Expected Foo.Flag to be true") + } + + assertStringArray(t, ret, []string{"bar", "-v", "-g"}) + assertStringArray(t, opts.Foo.args, []string{"bar", "-v", "-g"}) +} + func TestPassAfterNonOptionWithPositional(t *testing.T) { var opts = struct { Value bool `short:"v"` diff --git a/parser.go b/parser.go index d75a17c..54816a6 100644 --- a/parser.go +++ b/parser.go @@ -252,7 +252,7 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { } if !argumentIsOption(arg) { - if (p.Options & PassAfterNonOption) != None { + if (p.Options&PassAfterNonOption) != None && s.lookup.commands[arg] == nil { // If PassAfterNonOption is set then all remaining arguments // are considered positional if err = s.addArgs(s.arg); err != nil { From 9a38bfa8fedf0fdfa03666007497c987daba6330 Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Wed, 26 Sep 2018 15:52:32 +0100 Subject: [PATCH 18/71] added test to check for new command + positional behaviour --- options_test.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/options_test.go b/options_test.go index 0b14205..5b07099 100644 --- a/options_test.go +++ b/options_test.go @@ -80,6 +80,39 @@ func TestPassAfterNonOptionWithCommand(t *testing.T) { assertStringArray(t, opts.Foo.args, []string{"bar", "-v", "-g"}) } +type barCmd struct { + fooCmd + Positional struct { + Args []string + } `positional-args:"yes"` +} + +func TestPassAfterNonOptionWithCommandWithPositional(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + Bar barCmd `command:"bar"` + }{} + p := NewParser(&opts, PassAfterNonOption) + ret, err := p.ParseArgs([]string{"-v", "bar", "-f", "baz", "-v", "-g"}) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + return + } + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if !opts.Bar.Flag { + t.Errorf("Expected Bar.Flag to be true") + } + + assertStringArray(t, ret, []string{}) + assertStringArray(t, opts.Bar.args, []string{}) + assertStringArray(t, opts.Bar.Positional.Args, []string{"baz", "-v", "-g"}) +} + func TestPassAfterNonOptionWithPositional(t *testing.T) { var opts = struct { Value bool `short:"v"` From 3688adccfdcc8cd7725299f6687f10e7f0f71629 Mon Sep 17 00:00:00 2001 From: Mike Kenyon Date: Wed, 26 Sep 2018 15:06:50 -0700 Subject: [PATCH 19/71] Clarify that an error was expected but did not occur Signed-off-by: Anand Gaitonde --- assert_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assert_test.go b/assert_test.go index 8e06636..cfb7487 100644 --- a/assert_test.go +++ b/assert_test.go @@ -103,7 +103,7 @@ func assertParseSuccess(t *testing.T, data interface{}, args ...string) []string func assertError(t *testing.T, err error, typ ErrorType, msg string) { if err == nil { - assertFatalf(t, "Expected error: %s", msg) + assertFatalf(t, "Expected error: \"%s\", but no error occurred", msg) return } From e7ac6cd8efdaaad25ca45e5394ead835b6b1e19f Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Thu, 27 Sep 2018 09:01:41 +0100 Subject: [PATCH 20/71] move tests for PassAfterNonOption with commands to command_test --- command_test.go | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ options_test.go | 68 ------------------------------------------------- 2 files changed, 68 insertions(+), 68 deletions(-) diff --git a/command_test.go b/command_test.go index dc04b66..80d56be 100644 --- a/command_test.go +++ b/command_test.go @@ -580,3 +580,71 @@ func TestSubCommandFindOptionByShortFlag(t *testing.T) { t.Errorf("Expected 'o', but got %v", opt.ShortName) } } + +type fooCmd struct { + Flag bool `short:"f"` + args []string +} + +func (foo *fooCmd) Execute(s []string) error { + foo.args = s + return nil +} + +func TestCommandPassAfterNonOption(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + Foo fooCmd `command:"foo"` + }{} + p := NewParser(&opts, PassAfterNonOption) + ret, err := p.ParseArgs([]string{"-v", "foo", "-f", "bar", "-v", "-g"}) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + return + } + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if !opts.Foo.Flag { + t.Errorf("Expected Foo.Flag to be true") + } + + assertStringArray(t, ret, []string{"bar", "-v", "-g"}) + assertStringArray(t, opts.Foo.args, []string{"bar", "-v", "-g"}) +} + +type barCmd struct { + fooCmd + Positional struct { + Args []string + } `positional-args:"yes"` +} + +func TestCommandPassAfterNonOptionWithPositional(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + Bar barCmd `command:"bar"` + }{} + p := NewParser(&opts, PassAfterNonOption) + ret, err := p.ParseArgs([]string{"-v", "bar", "-f", "baz", "-v", "-g"}) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + return + } + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if !opts.Bar.Flag { + t.Errorf("Expected Bar.Flag to be true") + } + + assertStringArray(t, ret, []string{}) + assertStringArray(t, opts.Bar.args, []string{}) + assertStringArray(t, opts.Bar.Positional.Args, []string{"baz", "-v", "-g"}) +} diff --git a/options_test.go b/options_test.go index 5b07099..110fe2f 100644 --- a/options_test.go +++ b/options_test.go @@ -45,74 +45,6 @@ func TestPassAfterNonOption(t *testing.T) { assertStringArray(t, ret, []string{"arg", "-v", "-g"}) } -type fooCmd struct { - Flag bool `short:"f"` - args []string -} - -func (foo *fooCmd) Execute(s []string) error { - foo.args = s - return nil -} - -func TestPassAfterNonOptionWithCommand(t *testing.T) { - var opts = struct { - Value bool `short:"v"` - Foo fooCmd `command:"foo"` - }{} - p := NewParser(&opts, PassAfterNonOption) - ret, err := p.ParseArgs([]string{"-v", "foo", "-f", "bar", "-v", "-g"}) - - if err != nil { - t.Fatalf("Unexpected error: %v", err) - return - } - - if !opts.Value { - t.Errorf("Expected Value to be true") - } - - if !opts.Foo.Flag { - t.Errorf("Expected Foo.Flag to be true") - } - - assertStringArray(t, ret, []string{"bar", "-v", "-g"}) - assertStringArray(t, opts.Foo.args, []string{"bar", "-v", "-g"}) -} - -type barCmd struct { - fooCmd - Positional struct { - Args []string - } `positional-args:"yes"` -} - -func TestPassAfterNonOptionWithCommandWithPositional(t *testing.T) { - var opts = struct { - Value bool `short:"v"` - Bar barCmd `command:"bar"` - }{} - p := NewParser(&opts, PassAfterNonOption) - ret, err := p.ParseArgs([]string{"-v", "bar", "-f", "baz", "-v", "-g"}) - - if err != nil { - t.Fatalf("Unexpected error: %v", err) - return - } - - if !opts.Value { - t.Errorf("Expected Value to be true") - } - - if !opts.Bar.Flag { - t.Errorf("Expected Bar.Flag to be true") - } - - assertStringArray(t, ret, []string{}) - assertStringArray(t, opts.Bar.args, []string{}) - assertStringArray(t, opts.Bar.Positional.Args, []string{"baz", "-v", "-g"}) -} - func TestPassAfterNonOptionWithPositional(t *testing.T) { var opts = struct { Value bool `short:"v"` From 71064e1638ea2c34ad561fd1ec92a23944673ff4 Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Mon, 1 Oct 2018 10:17:59 +0100 Subject: [PATCH 21/71] fix #278 WriteHelp wasn't checking a group was shown in the help output before looping over it to check alignment. This fixes that, and thus #278. --- help.go | 3 +++ help_test.go | 1 + 2 files changed, 4 insertions(+) diff --git a/help.go b/help.go index 1b3b1ee..068fce1 100644 --- a/help.go +++ b/help.go @@ -72,6 +72,9 @@ func (p *Parser) getAlignmentInfo() alignmentInfo { var prevcmd *Command p.eachActiveGroup(func(c *Command, grp *Group) { + if !grp.showInHelp() { + return + } if c != prevcmd { for _, arg := range c.args { ret.updateLen(arg.Name, c != p.Command) diff --git a/help_test.go b/help_test.go index 352e41c..6d7798d 100644 --- a/help_test.go +++ b/help_test.go @@ -38,6 +38,7 @@ type helpOptions struct { HiddenGroup struct { InsideHiddenGroup string `long:"inside-hidden-group" description:"Inside hidden group"` + Padder bool `long:"this-option-in-a-hidden-group-has-a-ridiculously-long-name"` } `group:"Hidden group" hidden:"yes"` GroupWithOnlyHiddenOptions struct { From 91547d59ac68092d96aaac439aeef995d437b965 Mon Sep 17 00:00:00 2001 From: Anand Gaitonde Date: Wed, 24 Oct 2018 11:08:59 -0700 Subject: [PATCH 22/71] update path of golint install - the "go get" was failing in travis, switching to path in golint readme. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0f0728d..f46c43e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ install: - go build -v ./... # linting - - go get github.com/golang/lint/golint + - go get golang.org/x/lint/golint # code coverage - go get golang.org/x/tools/cmd/cover From 53e309bf179cd92ce11706d3840b3ba4ce5449d1 Mon Sep 17 00:00:00 2001 From: Anand Gaitonde Date: Wed, 24 Oct 2018 11:40:10 -0700 Subject: [PATCH 23/71] add verbose to debug go get errors --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f46c43e..9fed753 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ install: - go build -v ./... # linting - - go get golang.org/x/lint/golint + - go get -v golang.org/x/lint/golint # code coverage - go get golang.org/x/tools/cmd/cover From 9fcdf99d90dc4875b29db2e29005f1a4e70edb63 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Mon, 29 Oct 2018 13:24:29 +0100 Subject: [PATCH 24/71] Remove checking support for 1.7.x and 1.8.x --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9fed753..06dbe72 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,6 @@ os: go: - 1.x - - 1.7.x - - 1.8.x - 1.9.x - 1.10.x From 7fd817b182f2683be5972481eb580f4a65e01b25 Mon Sep 17 00:00:00 2001 From: Anand Gaitonde Date: Tue, 16 Oct 2018 22:07:49 -0700 Subject: [PATCH 25/71] Allow custom flags to read '-' if they are a value validator [https://www.pivotaltracker.com/story/show/127131179] [https://www.pivotaltracker.com/story/show/160742110] --- convert.go | 9 +++++++++ option.go | 34 ++++++++++++++++++++++++++++++++ parser.go | 4 ++-- parser_test.go | 53 ++++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 88 insertions(+), 12 deletions(-) diff --git a/convert.go b/convert.go index 984aac8..cda29b2 100644 --- a/convert.go +++ b/convert.go @@ -28,6 +28,15 @@ type Unmarshaler interface { UnmarshalFlag(value string) error } +// ValueValidator is the interface implemented by types that can validate a +// flag argument themselves. The provided value is directly passed from the +// command line. +type ValueValidator interface { + // IsValidValue returns an error if the provided string value is valid for + // the flag. + IsValidValue(value string) error +} + func getBase(options multiTag, base int) (int, error) { sbase := options.Get("base") diff --git a/option.go b/option.go index 8e306d9..5cebb54 100644 --- a/option.go +++ b/option.go @@ -389,6 +389,30 @@ func (option *Option) isUnmarshaler() Unmarshaler { return nil } +func (option *Option) isValueValidator() ValueValidator { + v := option.value + + for { + if !v.CanInterface() { + break + } + + i := v.Interface() + + if u, ok := i.(ValueValidator); ok { + return u + } + + if !v.CanAddr() { + break + } + + v = v.Addr() + } + + return nil +} + func (option *Option) isBool() bool { tp := option.value.Type() @@ -507,3 +531,13 @@ func (option *Option) shortAndLongName() string { return ret.String() } + +func (option *Option) isValidValue(arg string) error { + if validator := option.isValueValidator(); validator != nil { + return validator.IsValidValue(arg) + } + if argumentIsOption(arg) && !(option.isSignedNumber() && len(arg) > 1 && arg[0] == '-' && arg[1] >= '0' && arg[1] <= '9') { + return fmt.Errorf("expected argument for flag `%s', but got option `%s'", option, arg) + } + return nil +} diff --git a/parser.go b/parser.go index 54816a6..a5347b0 100644 --- a/parser.go +++ b/parser.go @@ -532,8 +532,8 @@ func (p *Parser) parseOption(s *parseState, name string, option *Option, canarg } else { arg = s.pop() - if argumentIsOption(arg) && !(option.isSignedNumber() && len(arg) > 1 && arg[0] == '-' && arg[1] >= '0' && arg[1] <= '9') { - return newErrorf(ErrExpectedArgument, "expected argument for flag `%s', but got option `%s'", option, arg) + if validationErr := option.isValidValue(arg); validationErr != nil { + return newErrorf(ErrExpectedArgument, validationErr.Error()) } else if p.Options&PassDoubleDash != 0 && arg == "--" { return newErrorf(ErrExpectedArgument, "expected argument for flag `%s', but got double dash `--'", option) } diff --git a/parser_test.go b/parser_test.go index f0c768d..bd2f464 100644 --- a/parser_test.go +++ b/parser_test.go @@ -1,6 +1,7 @@ package flags import ( + "errors" "fmt" "os" "reflect" @@ -363,6 +364,22 @@ func TestEnvDefaults(t *testing.T) { } } +type CustomFlag struct { + Value string +} + +func (c *CustomFlag) UnmarshalFlag(s string) error { + c.Value = s + return nil +} + +func (c *CustomFlag) IsValidValue(s string) error { + if !(s == "-1" || s == "-foo") { + return errors.New("invalid flag value") + } + return nil +} + func TestOptionAsArgument(t *testing.T) { var tests = []struct { args []string @@ -419,30 +436,46 @@ func TestOptionAsArgument(t *testing.T) { rest: []string{"-", "-"}, }, { - // Accept arguments which start with '-' if the next character is a digit, for number options only + // Accept arguments which start with '-' if the next character is a digit args: []string{"--int-slice", "-3"}, }, { - // Accept arguments which start with '-' if the next character is a digit, for number options only + // Accept arguments which start with '-' if the next character is a digit args: []string{"--int16", "-3"}, }, { - // Accept arguments which start with '-' if the next character is a digit, for number options only + // Accept arguments which start with '-' if the next character is a digit args: []string{"--float32", "-3.2"}, }, { - // Accept arguments which start with '-' if the next character is a digit, for number options only + // Accept arguments which start with '-' if the next character is a digit args: []string{"--float32ptr", "-3.2"}, }, + { + // Accept arguments for values that pass the IsValidValue fuction for value validators + args: []string{"--custom-flag", "-foo"}, + }, + { + // Accept arguments for values that pass the IsValidValue fuction for value validators + args: []string{"--custom-flag", "-1"}, + }, + { + // Rejects arguments for values that fail the IsValidValue fuction for value validators + args: []string{"--custom-flag", "-2"}, + expectError: true, + errType: ErrExpectedArgument, + errMsg: "invalid flag value", + }, } var opts struct { - StringSlice []string `long:"string-slice"` - IntSlice []int `long:"int-slice"` - Int16 int16 `long:"int16"` - Float32 float32 `long:"float32"` - Float32Ptr *float32 `long:"float32ptr"` - OtherOption bool `long:"other-option" short:"o"` + StringSlice []string `long:"string-slice"` + IntSlice []int `long:"int-slice"` + Int16 int16 `long:"int16"` + Float32 float32 `long:"float32"` + Float32Ptr *float32 `long:"float32ptr"` + OtherOption bool `long:"other-option" short:"o"` + Custom CustomFlag `long:"custom-flag" short:"c"` } for _, test := range tests { From fe4cb4b1478cb254c6c3a15b4320d04ac5bec4dd Mon Sep 17 00:00:00 2001 From: marinamoore Date: Mon, 29 Oct 2018 15:02:18 -0400 Subject: [PATCH 26/71] Added SOURCE_DATE_EPOCH support --- man.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/man.go b/man.go index a986c6e..a9044d9 100644 --- a/man.go +++ b/man.go @@ -3,7 +3,9 @@ package flags import ( "fmt" "io" + "os" "runtime" + "strconv" "strings" "time" ) @@ -175,6 +177,14 @@ func writeManPageCommand(wr io.Writer, name string, root *Command, command *Comm // writer. func (p *Parser) WriteManPage(wr io.Writer) { t := time.Now() + source_date_epoch := os.Getenv("SOURCE_DATE_EPOCH") + if source_date_epoch != "" { + sde, err := strconv.ParseInt(source_date_epoch, 10, 64) + if err != nil { + panic(fmt.Sprintf("Invalid SOURCE_DATE_EPOCH: %s", err)) + } + t = time.Unix(sde, 0) + } fmt.Fprintf(wr, ".TH %s 1 \"%s\"\n", manQuote(p.Name), t.Format("2 January 2006")) fmt.Fprintln(wr, ".SH NAME") From 4981010b653027ced86e0591f0ff935b72ce5408 Mon Sep 17 00:00:00 2001 From: marinamoore Date: Mon, 29 Oct 2018 15:21:21 -0400 Subject: [PATCH 27/71] formatting fixes --- man.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man.go b/man.go index a9044d9..97372b7 100644 --- a/man.go +++ b/man.go @@ -182,7 +182,7 @@ func (p *Parser) WriteManPage(wr io.Writer) { sde, err := strconv.ParseInt(source_date_epoch, 10, 64) if err != nil { panic(fmt.Sprintf("Invalid SOURCE_DATE_EPOCH: %s", err)) - } + } t = time.Unix(sde, 0) } From 594cb3b98df6a5a8c54f42e28f56c3d67e0f7ea6 Mon Sep 17 00:00:00 2001 From: Farbod Salimi Date: Mon, 17 Dec 2018 20:39:44 -0800 Subject: [PATCH 28/71] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f8d8eb7..f22650b 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ var opts struct { Name string `short:"n" long:"name" description:"A name" required:"true"` // Example of a flag restricted to a pre-defined set of strings - Name string `long:"animal" choice:"cat" choice:"dog"` + Animal string `long:"animal" choice:"cat" choice:"dog"` // Example of a value name File string `short:"f" long:"file" description:"A file" value-name:"FILE"` @@ -94,6 +94,7 @@ args := []string{ "-vv", "--offset=5", "-n", "Me", + "--animal", "dog", // anything other than "cat" or "dog" will raise an error "-p", "3", "-s", "hello", "-s", "world", @@ -118,6 +119,7 @@ if err != nil { fmt.Printf("Verbosity: %v\n", opts.Verbose) fmt.Printf("Offset: %d\n", opts.Offset) fmt.Printf("Name: %s\n", opts.Name) +fmt.Printf("Animal: %s\n", opts.Animal) 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]) From f0dfbc82957e3db331cb27732d270e275614b005 Mon Sep 17 00:00:00 2001 From: Boshi Lian Date: Fri, 4 Jan 2019 01:52:14 +0000 Subject: [PATCH 29/71] ignore unknow ini section if IgnoreUnknown is set --- ini.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ini.go b/ini.go index e714d3d..593bd27 100644 --- a/ini.go +++ b/ini.go @@ -505,7 +505,11 @@ func (i *IniParser) parse(ini *ini) error { groups := i.matchingGroups(name) if len(groups) == 0 { - return newErrorf(ErrUnknownGroup, "could not find option group `%s'", name) + if (p.Options & IgnoreUnknown) == None { + return newErrorf(ErrUnknownGroup, "could not find option group `%s'", name) + } + + continue } for _, inival := range section { From b1a42b8a49ad63f8cdd199b64f81b1e0e44d18e2 Mon Sep 17 00:00:00 2001 From: Peter Ebden Date: Sun, 24 Feb 2019 19:41:40 +0000 Subject: [PATCH 30/71] Add ability to programmatically add options to a group --- group.go | 7 +++++++ group_test.go | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/group.go b/group.go index ec909e5..544e538 100644 --- a/group.go +++ b/group.go @@ -73,6 +73,13 @@ func (g *Group) AddGroup(shortDescription string, longDescription string, data i return group, nil } +// AddOption adds a new option to this group. +func (g *Group) AddOption(option *Option, value reflect.Value) { + option.value = value + option.group = g + g.options = append(g.options, option) +} + // Groups returns the list of groups embedded in this group. func (g *Group) Groups() []*Group { return g.groups diff --git a/group_test.go b/group_test.go index 18cd6c1..5673f26 100644 --- a/group_test.go +++ b/group_test.go @@ -1,6 +1,7 @@ package flags import ( + "reflect" "testing" ) @@ -253,3 +254,19 @@ func TestFindOptionByShortFlagInSubGroup(t *testing.T) { t.Errorf("Expected 't', but got %v", opt.ShortName) } } + +func TestAddOptionNonOptional(t *testing.T) { + var opts struct { + Test bool + } + p := NewParser(&opts, Default) + p.AddOption(&Option{ + LongName: "test", + }, reflect.ValueOf(&opts.Test)) + _, err := p.ParseArgs([]string{"--test"}) + if err != nil { + t.Errorf("unexpected error: %s", err) + } else if !opts.Test { + t.Errorf("option not set") + } +} From 2442d95fb2f201050bd2bd4ab06ac2869dbd4b7c Mon Sep 17 00:00:00 2001 From: Peter Ebden Date: Sun, 24 Feb 2019 19:46:04 +0000 Subject: [PATCH 31/71] use an interface{} instead of reflect.Value --- group.go | 4 ++-- group_test.go | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/group.go b/group.go index 544e538..181caab 100644 --- a/group.go +++ b/group.go @@ -74,8 +74,8 @@ func (g *Group) AddGroup(shortDescription string, longDescription string, data i } // AddOption adds a new option to this group. -func (g *Group) AddOption(option *Option, value reflect.Value) { - option.value = value +func (g *Group) AddOption(option *Option, data interface{}) { + option.value = reflect.ValueOf(data) option.group = g g.options = append(g.options, option) } diff --git a/group_test.go b/group_test.go index 5673f26..409a7f2 100644 --- a/group_test.go +++ b/group_test.go @@ -1,7 +1,6 @@ package flags import ( - "reflect" "testing" ) @@ -262,7 +261,7 @@ func TestAddOptionNonOptional(t *testing.T) { p := NewParser(&opts, Default) p.AddOption(&Option{ LongName: "test", - }, reflect.ValueOf(&opts.Test)) + }, &opts.Test) _, err := p.ParseArgs([]string{"--test"}) if err != nil { t.Errorf("unexpected error: %s", err) From 97ea3ef321b95a1f09ffbd644a6e898ce28f8d04 Mon Sep 17 00:00:00 2001 From: Rupert Chen Date: Tue, 22 Jan 2019 13:19:34 -0500 Subject: [PATCH 32/71] Fix writing INI section names for subcommands Whereas a 'bar' subcommand under 'foo' should have the section name 'foo.bar', it was being written as 'bar.'. --- help_test.go | 31 +++++++++++++++++++++++++++++-- ini.go | 8 ++++---- ini_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 6 deletions(-) diff --git a/help_test.go b/help_test.go index 6d7798d..4e4ebd4 100644 --- a/help_test.go +++ b/help_test.go @@ -67,6 +67,13 @@ type helpOptions struct { ExtraVerbose []bool `long:"extra-verbose" description:"Use for extra verbosity"` } `command:"hidden-command" description:"A hidden command" hidden:"yes"` + ParentCommand struct { + Opt string `long:"opt" description:"This is a parent command option"` + SubCommand struct { + Opt string `long:"opt" description:"This is a sub command option"` + } `command:"sub" description:"A sub command"` + } `command:"parent" description:"A parent command"` + Args struct { Filename string `positional-arg-name:"filename" description:"A filename with a long description to trigger line wrapping"` Number int `positional-arg-name:"num" description:"A number"` @@ -100,7 +107,7 @@ func TestHelp(t *testing.T) { if runtime.GOOS == "windows" { expected = `Usage: - TestHelp [OPTIONS] [filename] [num] [hidden-in-help] + TestHelp [OPTIONS] [filename] [num] [hidden-in-help] Application Options: /v, /verbose Show verbose debug information @@ -145,10 +152,11 @@ Arguments: Available commands: bommand A command with only hidden options command A command (aliases: cm, cmd) + parent A command with a sub command ` } else { expected = `Usage: - TestHelp [OPTIONS] [filename] [num] [hidden-in-help] + TestHelp [OPTIONS] [filename] [num] [hidden-in-help] Application Options: -v, --verbose Show verbose debug information @@ -192,6 +200,7 @@ Arguments: Available commands: bommand A command with only hidden options command A command (aliases: cm, cmd) + parent A parent command ` } @@ -307,6 +316,24 @@ Longer \fBcommand\fP description .TP \fB\fB\-\-extra-verbose\fR\fP Use for extra verbosity +.SS parent +A parent command + +Longer \fBparent\fP description + +\fBUsage\fP: TestMan [OPTIONS] parent [parent-OPTIONS] +.TP +.TP +\fB\fB\-\-opt\fR\fP +This is a parent command option +.SS parent sub +A sub command + +\fBUsage\fP: parent [OPTIONS] sub [sub-OPTIONS] +.TP +.TP +\fB\fB\-\-opt\fR\fP +This is a sub command option `, tt.Format("2 January 2006"), envDefaultName) assertDiff(t, got, expected, "man page") diff --git a/ini.go b/ini.go index e714d3d..b48c286 100644 --- a/ini.go +++ b/ini.go @@ -325,19 +325,19 @@ func writeCommandIni(command *Command, namespace string, writer io.Writer, optio }) for _, c := range command.commands { - var nns string + var fqn string if c.Hidden { continue } if len(namespace) != 0 { - nns = c.Name + "." + nns + fqn = namespace + "." + c.Name } else { - nns = c.Name + fqn = c.Name } - writeCommandIni(c, nns, writer, options) + writeCommandIni(c, fqn, writer, options) } } diff --git a/ini_test.go b/ini_test.go index ad4852e..65fc3e9 100644 --- a/ini_test.go +++ b/ini_test.go @@ -104,6 +104,14 @@ Opt = ; Use for extra verbosity ; ExtraVerbose = +[parent] +; This is a parent command option +Opt = + +[parent.sub] +; This is a sub command option +Opt = + `, }, { @@ -167,6 +175,14 @@ EnvDefault2 = env-def ; Use for extra verbosity ; ExtraVerbose = +[parent] +; This is a parent command option +; Opt = + +[parent.sub] +; This is a sub command option +; Opt = + `, }, { @@ -228,6 +244,38 @@ EnvDefault2 = env-def ; Use for extra verbosity ; ExtraVerbose = +[parent] +; This is a parent command option +; Opt = + +[parent.sub] +; This is a sub command option +; Opt = + +`, + }, + { + []string{"-vv", "filename", "0", "3.14", "parent", "--opt=p", "sub", "--opt=s"}, + IniDefault, + `[Application Options] +; Show verbose debug information +verbose = true +verbose = true + +; Test env-default1 value +EnvDefault1 = env-def + +; Test env-default2 value +EnvDefault2 = env-def + +[parent] +; This is a parent command option +Opt = p + +[parent.sub] +; This is a sub command option +Opt = s + `, }, } From d71158e4391b59ee6b72475886d3d57ce8f5e301 Mon Sep 17 00:00:00 2001 From: Rupert Chen Date: Tue, 22 Jan 2019 17:20:01 -0500 Subject: [PATCH 33/71] Fix usage text of subcommands in man pages Previously, the usage text of a sub command on the man page would only include the direct parent command's name. A `foo` utility with `bar` command and `baz` subcommand would give `baz`s usage as ``` Usage: bar [OPTIONS] baz [baz-OPTIONS] ``` instead of ``` Usage: foo [OPTIONS] bar [bar-OPTIONS] baz [baz-OPTIONS] ``` --- help_test.go | 2 +- man.go | 21 +++++++++------------ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/help_test.go b/help_test.go index 4e4ebd4..ba75c7a 100644 --- a/help_test.go +++ b/help_test.go @@ -329,7 +329,7 @@ This is a parent command option .SS parent sub A sub command -\fBUsage\fP: parent [OPTIONS] sub [sub-OPTIONS] +\fBUsage\fP: TestMan [OPTIONS] parent [parent-OPTIONS] sub [sub-OPTIONS] .TP .TP \fB\fB\-\-opt\fR\fP diff --git a/man.go b/man.go index 97372b7..7e746af 100644 --- a/man.go +++ b/man.go @@ -107,7 +107,7 @@ func writeManPageOptions(wr io.Writer, grp *Group) { }) } -func writeManPageSubcommands(wr io.Writer, name string, root *Command) { +func writeManPageSubcommands(wr io.Writer, name string, usagePrefix string, root *Command) { commands := root.sortedVisibleCommands() for _, c := range commands { @@ -123,11 +123,11 @@ func writeManPageSubcommands(wr io.Writer, name string, root *Command) { nn = c.Name } - writeManPageCommand(wr, nn, root, c) + writeManPageCommand(wr, nn, usagePrefix, c) } } -func writeManPageCommand(wr io.Writer, name string, root *Command, command *Command) { +func writeManPageCommand(wr io.Writer, name string, usagePrefix string, command *Command) { fmt.Fprintf(wr, ".SS %s\n", name) fmt.Fprintln(wr, command.ShortDescription) @@ -147,6 +147,8 @@ func writeManPageCommand(wr io.Writer, name string, root *Command, command *Comm } } + var pre = usagePrefix + " " + command.Name + var usage string if us, ok := command.data.(Usage); ok { usage = us.Usage() @@ -154,15 +156,10 @@ func writeManPageCommand(wr io.Writer, name string, root *Command, command *Comm usage = fmt.Sprintf("[%s-OPTIONS]", command.Name) } - var pre string - if root.hasHelpOptions() { - pre = fmt.Sprintf("%s [OPTIONS] %s", root.Name, command.Name) - } else { - pre = fmt.Sprintf("%s %s", root.Name, command.Name) - } - + var nextPrefix = pre if len(usage) > 0 { fmt.Fprintf(wr, "\n\\fBUsage\\fP: %s %s\n.TP\n", manQuote(pre), manQuote(usage)) + nextPrefix = pre + " " + usage } if len(command.Aliases) > 0 { @@ -170,7 +167,7 @@ func writeManPageCommand(wr io.Writer, name string, root *Command, command *Comm } writeManPageOptions(wr, command.Group) - writeManPageSubcommands(wr, name, command) + writeManPageSubcommands(wr, name, nextPrefix, command) } // WriteManPage writes a basic man page in groff format to the specified @@ -210,6 +207,6 @@ func (p *Parser) WriteManPage(wr io.Writer) { if len(p.visibleCommands()) > 0 { fmt.Fprintln(wr, ".SH COMMANDS") - writeManPageSubcommands(wr, "", p.Command) + writeManPageSubcommands(wr, "", p.Name+" "+usage, p.Command) } } From c6f86cb83a382b6c6110ea3c42d299aed7975528 Mon Sep 17 00:00:00 2001 From: Tony Abbott Date: Tue, 21 May 2019 15:36:29 +0200 Subject: [PATCH 34/71] Use golang.org/x/sys/unix to determine terminal width to support aix/ppc64 --- check_crosscompile.sh | 2 ++ termsize.go | 25 ++++++------------------- termsize_nosysioctl.go | 2 +- tiocgwinsz_bsdish.go | 7 ------- tiocgwinsz_linux.go | 7 ------- tiocgwinsz_other.go | 7 ------- 6 files changed, 9 insertions(+), 41 deletions(-) delete mode 100644 tiocgwinsz_bsdish.go delete mode 100644 tiocgwinsz_linux.go delete mode 100644 tiocgwinsz_other.go diff --git a/check_crosscompile.sh b/check_crosscompile.sh index c494f61..f30e695 100755 --- a/check_crosscompile.sh +++ b/check_crosscompile.sh @@ -14,3 +14,5 @@ echo '# darwin' GOARCH=amd64 GOOS=darwin go build echo '# freebsd' GOARCH=amd64 GOOS=freebsd go build +echo '# aix ppc64' +GOARCH=ppc64 GOOS=aix go build diff --git a/termsize.go b/termsize.go index 29b1110..829e477 100644 --- a/termsize.go +++ b/termsize.go @@ -1,28 +1,15 @@ -// +build !windows,!plan9,!solaris,!appengine,!wasm +// +build !windows,!plan9,!appengine,!wasm package flags import ( - "syscall" - "unsafe" + "golang.org/x/sys/unix" ) -type winsize struct { - row, col uint16 - xpixel, ypixel uint16 -} - func getTerminalColumns() int { - ws := winsize{} - - if tIOCGWINSZ != 0 { - syscall.Syscall(syscall.SYS_IOCTL, - uintptr(0), - uintptr(tIOCGWINSZ), - uintptr(unsafe.Pointer(&ws))) - - return int(ws.col) + ws, err := unix.IoctlGetWinsize(0, unix.TIOCGWINSZ) + if err != nil { + return 80 } - - return 80 + return int(ws.Col) } diff --git a/termsize_nosysioctl.go b/termsize_nosysioctl.go index 12a5458..c1ff186 100644 --- a/termsize_nosysioctl.go +++ b/termsize_nosysioctl.go @@ -1,4 +1,4 @@ -// +build plan9 solaris appengine wasm +// +build plan9 appengine wasm package flags diff --git a/tiocgwinsz_bsdish.go b/tiocgwinsz_bsdish.go deleted file mode 100644 index fcc1186..0000000 --- a/tiocgwinsz_bsdish.go +++ /dev/null @@ -1,7 +0,0 @@ -// +build darwin freebsd netbsd openbsd - -package flags - -const ( - tIOCGWINSZ = 0x40087468 -) diff --git a/tiocgwinsz_linux.go b/tiocgwinsz_linux.go deleted file mode 100644 index e3975e2..0000000 --- a/tiocgwinsz_linux.go +++ /dev/null @@ -1,7 +0,0 @@ -// +build linux - -package flags - -const ( - tIOCGWINSZ = 0x5413 -) diff --git a/tiocgwinsz_other.go b/tiocgwinsz_other.go deleted file mode 100644 index 3082151..0000000 --- a/tiocgwinsz_other.go +++ /dev/null @@ -1,7 +0,0 @@ -// +build !darwin,!freebsd,!netbsd,!openbsd,!linux - -package flags - -const ( - tIOCGWINSZ = 0 -) From 249bd42be6a068ec774ea529b1d8f4dac7224a23 Mon Sep 17 00:00:00 2001 From: Tony Abbott Date: Tue, 21 May 2019 15:44:03 +0200 Subject: [PATCH 35/71] Cross compile solaris/amd64 --- check_crosscompile.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/check_crosscompile.sh b/check_crosscompile.sh index f30e695..5edc430 100755 --- a/check_crosscompile.sh +++ b/check_crosscompile.sh @@ -16,3 +16,5 @@ echo '# freebsd' GOARCH=amd64 GOOS=freebsd go build echo '# aix ppc64' GOARCH=ppc64 GOOS=aix go build +echo '# solaris amd64' +GOARCH=amd64 GOOS=solaris go build From a740d3cf5918041c5390d3e04176283819373f6e Mon Sep 17 00:00:00 2001 From: Maciek Borzecki Date: Mon, 17 Jun 2019 09:01:19 +0200 Subject: [PATCH 36/71] completion: do not complete hidden commands Declaring the command hidden it does not show up in help or man. It should also not appear in completion output. Signed-off-by: Maciek Borzecki --- completion.go | 2 +- completion_test.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/completion.go b/completion.go index 813a672..74bd5a3 100644 --- a/completion.go +++ b/completion.go @@ -130,7 +130,7 @@ func (c *completion) completeCommands(s *parseState, match string) []Completion n := make([]Completion, 0, len(s.command.commands)) for _, cmd := range s.command.commands { - if cmd.data != c && strings.HasPrefix(cmd.Name, match) { + if cmd.data != c && !cmd.Hidden && strings.HasPrefix(cmd.Name, match) { n = append(n, Completion{ Item: cmd.Name, Description: cmd.ShortDescription, diff --git a/completion_test.go b/completion_test.go index 4bd758d..aa3af90 100644 --- a/completion_test.go +++ b/completion_test.go @@ -68,6 +68,9 @@ var completionTestOptions struct { RenameCommand struct { Completed TestComplete `short:"c" long:"completed"` } `command:"rename" description:"rename an item"` + + HiddenCommand struct { + } `command:"hidden" description:"hidden command" hidden:"true"` } type completionTest struct { From 206428b03a5152306e043b5c0b7c7575c24afc61 Mon Sep 17 00:00:00 2001 From: Denis Titusov Date: Sun, 6 Oct 2019 14:19:11 +0300 Subject: [PATCH 37/71] Implement error interface --- error.go | 4 ++++ examples/main.go | 10 +++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/error.go b/error.go index 05528d8..73e07cf 100644 --- a/error.go +++ b/error.go @@ -97,6 +97,10 @@ func (e ErrorType) String() string { return "unrecognized error type" } +func (e ErrorType) Error() string { + return e.String() +} + // Error represents a parser error. The error returned from Parse is of this // type. The error contains both a Type and Message. type Error struct { diff --git a/examples/main.go b/examples/main.go index 632c331..9837df5 100644 --- a/examples/main.go +++ b/examples/main.go @@ -70,9 +70,13 @@ var parser = flags.NewParser(&options, flags.Default) func main() { if _, err := parser.Parse(); err != nil { - if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp { - os.Exit(0) - } else { + switch flagsErr := err.(type) { + case flags.ErrorType: + if flagsErr == flags.ErrHelp { + os.Exit(0) + } + os.Exit(1) + default: os.Exit(1) } } From dfeb63c261e555efba577194a25f7902132be864 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 24 Feb 2020 20:26:16 -0800 Subject: [PATCH 38/71] ini: fix IniParser.write() for zero values --- convert.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/convert.go b/convert.go index cda29b2..ef06d4b 100644 --- a/convert.go +++ b/convert.go @@ -53,7 +53,7 @@ func getBase(options multiTag, base int) (int, error) { func convertMarshal(val reflect.Value) (bool, string, error) { // Check first for the Marshaler interface - if val.Type().NumMethod() > 0 && val.CanInterface() { + if val.IsValid() && val.Type().NumMethod() > 0 && val.CanInterface() { if marshaler, ok := val.Interface().(Marshaler); ok { ret, err := marshaler.MarshalFlag() return true, ret, err @@ -68,6 +68,10 @@ func convertToString(val reflect.Value, options multiTag) (string, error) { return ret, err } + if !val.IsValid() { + return "", nil + } + tp := val.Type() // Support for time.Duration From cfe637d9c8815c1f3d07e7f809908bea72b730b7 Mon Sep 17 00:00:00 2001 From: Alex Suraci Date: Tue, 21 Apr 2020 10:27:06 -0400 Subject: [PATCH 39/71] return err for invalid env default value Signed-off-by: Alex Suraci --- option.go | 9 +++++++-- parser.go | 5 ++++- parser_test.go | 41 +++++++++++++++++++++++++++++------------ 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/option.go b/option.go index 5cebb54..bc41916 100644 --- a/option.go +++ b/option.go @@ -308,7 +308,7 @@ func (option *Option) empty() { } } -func (option *Option) clearDefault() { +func (option *Option) clearDefault() error { usedDefault := option.Default if envKey := option.EnvKeyWithNamespace(); envKey != "" { @@ -327,7 +327,10 @@ func (option *Option) clearDefault() { option.empty() for _, d := range usedDefault { - option.set(&d) + err := option.set(&d) + if err != nil { + return err + } option.isSetDefault = true } } else { @@ -344,6 +347,8 @@ func (option *Option) clearDefault() { } } } + + return nil } func (option *Option) valueIsDefault() bool { diff --git a/parser.go b/parser.go index a5347b0..69e4277 100644 --- a/parser.go +++ b/parser.go @@ -314,7 +314,10 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { return } - option.clearDefault() + err := option.clearDefault() + if err != nil { + s.err = err + } }) s.checkRequired(p) diff --git a/parser_test.go b/parser_test.go index bd2f464..410decb 100644 --- a/parser_test.go +++ b/parser_test.go @@ -260,10 +260,11 @@ type envDefaultOptions struct { func TestEnvDefaults(t *testing.T) { var tests = []struct { - msg string - args []string - expected envDefaultOptions - env map[string]string + msg string + args []string + expected envDefaultOptions + expectedErr string + env map[string]string }{ { msg: "no arguments, no env, expecting default values", @@ -298,6 +299,14 @@ func TestEnvDefaults(t *testing.T) { "NESTED_FOO": "a", }, }, + { + msg: "no arguments, malformed env defaults, expecting parse error", + args: []string{}, + expectedErr: `parsing "two": invalid syntax`, + env: map[string]string{ + "TEST_I": "two", + }, + }, { msg: "non-zero value arguments, expecting overwritten arguments", args: []string{"--i=3", "--t=3ms", "--m=c:3", "--s=3", "--nested.foo=\"p\""}, @@ -350,16 +359,24 @@ func TestEnvDefaults(t *testing.T) { os.Setenv(envKey, envValue) } _, err := ParseArgs(&opts, test.args) - if err != nil { - t.Fatalf("%s:\nUnexpected error: %v", test.msg, err) - } + if test.expectedErr != "" { + if err == nil { + t.Errorf("%s:\nExpected error containing substring %q", test.msg, test.expectedErr) + } else if !strings.Contains(err.Error(), test.expectedErr) { + t.Errorf("%s:\nExpected error %q to contain substring %q", test.msg, err, test.expectedErr) + } + } else { + if err != nil { + t.Fatalf("%s:\nUnexpected error: %v", test.msg, err) + } - if opts.Slice == nil { - opts.Slice = []int{} - } + if opts.Slice == nil { + opts.Slice = []int{} + } - if !reflect.DeepEqual(opts, test.expected) { - t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts) + if !reflect.DeepEqual(opts, test.expected) { + t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts) + } } } } From a34a5e386e856e51af3932ae4b939c15c9a797e6 Mon Sep 17 00:00:00 2001 From: Jamie Klassen Date: Tue, 28 Apr 2020 08:26:38 -0400 Subject: [PATCH 40/71] include flag name when erroring on env default Signed-off-by: Jamie Klassen --- parser.go | 6 ++++++ parser_test.go | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/parser.go b/parser.go index 69e4277..0c31b94 100644 --- a/parser.go +++ b/parser.go @@ -316,6 +316,12 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { err := option.clearDefault() 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()) + } s.err = err } }) diff --git a/parser_test.go b/parser_test.go index 410decb..5700bcd 100644 --- a/parser_test.go +++ b/parser_test.go @@ -358,7 +358,7 @@ func TestEnvDefaults(t *testing.T) { for envKey, envValue := range test.env { os.Setenv(envKey, envValue) } - _, err := ParseArgs(&opts, test.args) + _, err := NewParser(&opts, None).ParseArgs(test.args) if test.expectedErr != "" { if err == nil { t.Errorf("%s:\nExpected error containing substring %q", test.msg, test.expectedErr) From 0259d211c047e68138f1a5297646a2665211e2e0 Mon Sep 17 00:00:00 2001 From: Slava Volkov Date: Wed, 15 Jul 2020 23:18:27 +0300 Subject: [PATCH 41/71] positional args without allcmd.ArgsRequired help print dependent from arg.Required --- help.go | 6 +++++- help_test.go | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/help.go b/help.go index 068fce1..bbf764b 100644 --- a/help.go +++ b/help.go @@ -334,7 +334,11 @@ func (p *Parser) WriteHelp(writer io.Writer) { } if !allcmd.ArgsRequired { - fmt.Fprintf(wr, "[%s]", name) + if arg.Required > 0 { + fmt.Fprintf(wr, "%s", name) + } else { + fmt.Fprintf(wr, "[%s]", name) + } } else { fmt.Fprintf(wr, "%s", name) } diff --git a/help_test.go b/help_test.go index ba75c7a..27c3d0e 100644 --- a/help_test.go +++ b/help_test.go @@ -107,7 +107,7 @@ func TestHelp(t *testing.T) { if runtime.GOOS == "windows" { expected = `Usage: - TestHelp [OPTIONS] [filename] [num] [hidden-in-help] + TestHelp [OPTIONS] [filename] [num] hidden-in-help Application Options: /v, /verbose Show verbose debug information @@ -156,7 +156,7 @@ Available commands: ` } else { expected = `Usage: - TestHelp [OPTIONS] [filename] [num] [hidden-in-help] + TestHelp [OPTIONS] [filename] [num] hidden-in-help Application Options: -v, --verbose Show verbose debug information From ad09853afdfdb6d368bae3e3eeff89cdfc6a5a38 Mon Sep 17 00:00:00 2001 From: Maciek Borzecki Date: Tue, 5 Jan 2021 15:50:15 +0100 Subject: [PATCH 42/71] fix a panic when generating help while the subcommand and all option groups are hidden The panic is caused by calling strings.Repeat() with negative repeat count. This is triggered by incorrectly computed alignment info. Since the positional arguments are always shown (even when the subcommand is hidden), the arguments need to be accounted for when calculating alignment. The panic backtrace is the following: --- FAIL: TestHiddenCommandNoBuiltinHelp (0.00s) panic: strings: negative Repeat count [recovered] panic: strings: negative Repeat count goroutine 91 [running]: testing.tRunner.func1.1(0x57f160, 0x5f3f20) /home/maciek/code/go/go/src/testing/testing.go:1072 +0x30d testing.tRunner.func1(0xc000252a80) /home/maciek/code/go/go/src/testing/testing.go:1075 +0x41a panic(0x57f160, 0x5f3f20) /home/maciek/code/go/go/src/runtime/panic.go:969 +0x1b9 strings.Repeat(0x5bc3b0, 0x1, 0xfffffffffffffff1, 0x13, 0x0) /home/maciek/code/go/go/src/strings/strings.go:529 +0x5e5 github.com/jessevdk/go-flags.(*Parser).WriteHelp(0xc000387340, 0x5f6d40, 0xc0002772f0) /home/maciek/work/canonical/workspace/src/github.com/jessevdk/go-flags/help.go:449 +0x510 github.com/jessevdk/go-flags.TestHiddenCommandNoBuiltinHelp(0xc000252a80) /home/maciek/work/canonical/workspace/src/github.com/jessevdk/go-flags/help_test.go:469 +0x4d4 testing.tRunner(0xc000252a80, 0x5cd9a0) /home/maciek/code/go/go/src/testing/testing.go:1123 +0xef created by testing.(*T).Run /home/maciek/code/go/go/src/testing/testing.go:1168 +0x2b3 exit status 2 FAIL github.com/jessevdk/go-flags 0.016s Signed-off-by: Maciek Borzecki --- help.go | 8 ++--- help_test.go | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/help.go b/help.go index 068fce1..55948b0 100644 --- a/help.go +++ b/help.go @@ -72,15 +72,15 @@ func (p *Parser) getAlignmentInfo() alignmentInfo { var prevcmd *Command p.eachActiveGroup(func(c *Command, grp *Group) { - if !grp.showInHelp() { - return - } if c != prevcmd { for _, arg := range c.args { ret.updateLen(arg.Name, c != p.Command) } + prevcmd = c + } + if !grp.showInHelp() { + return } - for _, info := range grp.options { if !info.showInHelp() { continue diff --git a/help_test.go b/help_test.go index ba75c7a..92d13db 100644 --- a/help_test.go +++ b/help_test.go @@ -389,6 +389,91 @@ Help Options: } } +func TestHiddenCommandNoBuiltinHelp(t *testing.T) { + oldEnv := EnvSnapshot() + defer oldEnv.Restore() + os.Setenv("ENV_DEFAULT", "env-def") + + // no auto added help group + p := NewNamedParser("TestHelpCommand", 0) + // and no usage information either + p.Usage = "" + + // add custom help group which is not listed in --help output + var help struct { + ShowHelp func() error `short:"h" long:"help"` + } + help.ShowHelp = func() error { + return &Error{Type: ErrHelp} + } + hlpgrp, err := p.AddGroup("Help Options", "", &help) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + hlpgrp.Hidden = true + hlp := p.FindOptionByLongName("help") + hlp.Description = "Show this help message" + // make sure the --help option is hidden + hlp.Hidden = true + + // add a hidden command + var hiddenCmdOpts struct { + Foo bool `short:"f" long:"very-long-foo-option" description:"Very long foo description"` + Bar bool `short:"b" description:"Option bar"` + Positional struct { + PositionalFoo string `positional-arg-name:"" description:"positional foo"` + } `positional-args:"yes"` + } + cmdHidden, err := p.Command.AddCommand("hidden", "Hidden command description", "Long hidden command description", &hiddenCmdOpts) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + // make it hidden + cmdHidden.Hidden = true + if len(cmdHidden.Options()) != 2 { + t.Fatalf("unexpected options count") + } + // which help we ask for explicitly + _, err = p.ParseArgs([]string{"hidden", "--help"}) + + 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) + } + + var expected string + + if runtime.GOOS == "windows" { + expected = `Usage: + TestHelpCommand hidden [hidden-OPTIONS] [] + +Long hidden command description + +[hidden command arguments] + : positional foo +` + } else { + expected = `Usage: + TestHelpCommand hidden [hidden-OPTIONS] [] + +Long hidden command description + +[hidden command arguments] + : positional foo +` + } + h := &bytes.Buffer{} + p.WriteHelp(h) + + assertDiff(t, h.String(), expected, "help message") + } +} + func TestHelpDefaults(t *testing.T) { var expected string From 0fb31d3686e2c0afcd5877fcc616a1a09f5727de Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 21 Mar 2021 08:47:06 +0100 Subject: [PATCH 43/71] Use modules --- go.mod | 5 +++++ go.sum | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 go.mod create mode 100644 go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a626c5d --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/jessevdk/go-flags + +go 1.15 + +require golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7503251 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= From c0b9c5121db585c543871dcfb347235398dce6b9 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 21 Mar 2021 09:10:39 +0100 Subject: [PATCH 44/71] Cleanup from linting --- completion.go | 2 +- help_test.go | 8 ++++---- tag_test.go | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/completion.go b/completion.go index 74bd5a3..8ed61f1 100644 --- a/completion.go +++ b/completion.go @@ -82,7 +82,7 @@ func (c *completion) skipPositional(s *parseState, n int) { func (c *completion) completeOptionNames(s *parseState, prefix string, match string, short bool) []Completion { if short && len(match) != 0 { return []Completion{ - Completion{ + { Item: prefix + match, }, } diff --git a/help_test.go b/help_test.go index ba75c7a..8a4c2f8 100644 --- a/help_test.go +++ b/help_test.go @@ -590,10 +590,10 @@ func TestWroteHelp(t *testing.T) { isHelp bool } tests := map[string]testInfo{ - "No error": testInfo{value: nil, isHelp: false}, - "Plain error": testInfo{value: errors.New("an error"), isHelp: false}, - "ErrUnknown": testInfo{value: newError(ErrUnknown, "an error"), isHelp: false}, - "ErrHelp": testInfo{value: newError(ErrHelp, "an error"), isHelp: true}, + "No error": {value: nil, isHelp: false}, + "Plain error": {value: errors.New("an error"), isHelp: false}, + "ErrUnknown": {value: newError(ErrUnknown, "an error"), isHelp: false}, + "ErrHelp": {value: newError(ErrHelp, "an error"), isHelp: true}, } for name, test := range tests { diff --git a/tag_test.go b/tag_test.go index 9daa740..2c34323 100644 --- a/tag_test.go +++ b/tag_test.go @@ -6,7 +6,7 @@ import ( func TestTagMissingColon(t *testing.T) { var opts = struct { - Value bool `short` + TestValue bool `short` }{} assertParseFail(t, ErrTag, "expected `:' after key name, but got end of tag (in `short`)", &opts, "") @@ -14,7 +14,7 @@ func TestTagMissingColon(t *testing.T) { func TestTagMissingValue(t *testing.T) { var opts = struct { - Value bool `short:` + TestValue bool `short:` }{} assertParseFail(t, ErrTag, "expected `\"' to start tag value at end of tag (in `short:`)", &opts, "") @@ -22,7 +22,7 @@ func TestTagMissingValue(t *testing.T) { func TestTagMissingQuote(t *testing.T) { var opts = struct { - Value bool `short:"v` + TestValue bool `short:"v` }{} assertParseFail(t, ErrTag, "expected end of tag value `\"' at end of tag (in `short:\"v`)", &opts, "") @@ -30,7 +30,7 @@ func TestTagMissingQuote(t *testing.T) { func TestTagNewline(t *testing.T) { var opts = struct { - Value bool `long:"verbose" description:"verbose + TestValue bool `long:"verbose" description:"verbose something"` }{} From be6ab81d9e3fde476173ab961545bc1fd5452651 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 21 Mar 2021 09:13:22 +0100 Subject: [PATCH 45/71] Do not quote new lines in description for man pages Issue #349. --- man.go | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/man.go b/man.go index 7e746af..82572f9 100644 --- a/man.go +++ b/man.go @@ -10,30 +10,41 @@ import ( "time" ) +func manQuoteLines(s string) string { + lines := strings.Split(s, "\n") + parts := []string{} + + for _, line := range lines { + parts = append(parts, manQuote(line)) + } + + return strings.Join(parts, "\n") +} + func manQuote(s string) string { return strings.Replace(s, "\\", "\\\\", -1) } -func formatForMan(wr io.Writer, s string) { +func formatForMan(wr io.Writer, s string, quoter func(s string) string) { for { idx := strings.IndexRune(s, '`') if idx < 0 { - fmt.Fprintf(wr, "%s", manQuote(s)) + fmt.Fprintf(wr, "%s", quoter(s)) break } - fmt.Fprintf(wr, "%s", manQuote(s[:idx])) + fmt.Fprintf(wr, "%s", quoter(s[:idx])) s = s[idx+1:] idx = strings.IndexRune(s, '\'') if idx < 0 { - fmt.Fprintf(wr, "%s", manQuote(s)) + fmt.Fprintf(wr, "%s", quoter(s)) break } - fmt.Fprintf(wr, "\\fB%s\\fP", manQuote(s[:idx])) + fmt.Fprintf(wr, "\\fB%s\\fP", quoter(s[:idx])) s = s[idx+1:] } } @@ -50,7 +61,7 @@ func writeManPageOptions(wr io.Writer, grp *Group) { fmt.Fprintf(wr, ".SS %s\n", group.ShortDescription) if group.LongDescription != "" { - formatForMan(wr, group.LongDescription) + formatForMan(wr, group.LongDescription, manQuoteLines) fmt.Fprintln(wr, "") } } @@ -100,7 +111,7 @@ func writeManPageOptions(wr io.Writer, grp *Group) { fmt.Fprintln(wr, "\\fP") if len(opt.Description) != 0 { - formatForMan(wr, opt.Description) + formatForMan(wr, opt.Description, manQuoteLines) fmt.Fprintln(wr, "") } } @@ -139,10 +150,10 @@ func writeManPageCommand(wr io.Writer, name string, usagePrefix string, command if strings.HasPrefix(command.LongDescription, cmdstart) { fmt.Fprintf(wr, "The \\fI%s\\fP command", manQuote(command.Name)) - formatForMan(wr, command.LongDescription[len(cmdstart):]) + formatForMan(wr, command.LongDescription[len(cmdstart):], manQuoteLines) fmt.Fprintln(wr, "") } else { - formatForMan(wr, command.LongDescription) + formatForMan(wr, command.LongDescription, manQuoteLines) fmt.Fprintln(wr, "") } } @@ -185,7 +196,7 @@ func (p *Parser) WriteManPage(wr io.Writer) { 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", manQuote(p.Name), manQuote(p.ShortDescription)) + fmt.Fprintf(wr, "%s \\- %s\n", manQuote(p.Name), manQuoteLines(p.ShortDescription)) fmt.Fprintln(wr, ".SH SYNOPSIS") usage := p.Usage @@ -197,7 +208,7 @@ func (p *Parser) WriteManPage(wr io.Writer) { fmt.Fprintf(wr, "\\fB%s\\fP %s\n", manQuote(p.Name), manQuote(usage)) fmt.Fprintln(wr, ".SH DESCRIPTION") - formatForMan(wr, p.LongDescription) + formatForMan(wr, p.LongDescription, manQuoteLines) fmt.Fprintln(wr, "") fmt.Fprintln(wr, ".SH OPTIONS") From 59f46c22112c7be066ef452ce9395d4117f76959 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 21 Mar 2021 09:14:15 +0100 Subject: [PATCH 46/71] Add test Issue #349. --- help_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/help_test.go b/help_test.go index 8a4c2f8..0beb1d3 100644 --- a/help_test.go +++ b/help_test.go @@ -216,7 +216,7 @@ func TestMan(t *testing.T) { var opts helpOptions p := NewNamedParser("TestMan", HelpFlag) p.ShortDescription = "Test manpage generation" - p.LongDescription = "This is a somewhat `longer' description of what this does" + p.LongDescription = "This is a somewhat `longer' description of what this does.\nWith multiple lines." p.AddGroup("Application Options", "The application options", &opts) for _, cmd := range p.Commands() { @@ -244,7 +244,8 @@ TestMan \- Test manpage generation .SH SYNOPSIS \fBTestMan\fP [OPTIONS] .SH DESCRIPTION -This is a somewhat \fBlonger\fP description of what this does +This is a somewhat \fBlonger\fP description of what this does. +With multiple lines. .SH OPTIONS .SS Application Options The application options From 6a6df1be4f0285203eafe8e39deca331e34926ff Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 21 Mar 2021 09:17:24 +0100 Subject: [PATCH 47/71] Bump travis version --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 06dbe72..2fc5e5f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,13 +5,10 @@ os: - osx go: - - 1.x - - 1.9.x - - 1.10.x + - 1.16.x install: # go-flags - - go get -d -v ./... - go build -v ./... # linting From daf243b65b7c59ed93898387245020253a27fbed Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 21 Mar 2021 10:36:07 +0100 Subject: [PATCH 48/71] Do not show type for call errors Fixes #337. --- parser.go | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/parser.go b/parser.go index 0c31b94..f2e4237 100644 --- a/parser.go +++ b/parser.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "path" + "reflect" "sort" "strings" "unicode/utf8" @@ -317,10 +318,7 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { err := option.clearDefault() 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()) + err = p.marshalError(option, err) } s.err = err } @@ -571,16 +569,37 @@ func (p *Parser) parseOption(s *parseState, name string, option *Option, canarg 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()) + err = p.marshalError(option, err) } } return err } +func (p *Parser) marshalError(option *Option, err error) *Error { + s := "invalid argument for flag `%s'" + + expected := p.expectedType(option) + + if expected != "" { + s = s + " (expected " + expected + ")" + } + + return newErrorf(ErrMarshal, s+": %s", + option, + err.Error()) +} + +func (p *Parser) expectedType(option *Option) string { + valueType := option.value.Type() + + if valueType.Kind() == reflect.Func { + return "" + } + + return valueType.String() +} + 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 77d7cb9b851deca9030398b4ec809b7f15ef575f Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 21 Mar 2021 11:12:43 +0100 Subject: [PATCH 49/71] Do not prevent ini defaults overriding builtin defaults Issue #326. --- ini.go | 18 ++++++++++++++---- ini_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ option.go | 19 +++++++++++++++++-- parser.go | 4 ---- 4 files changed, 76 insertions(+), 10 deletions(-) diff --git a/ini.go b/ini.go index 5debe74..2223d7b 100644 --- a/ini.go +++ b/ini.go @@ -541,9 +541,8 @@ func (i *IniParser) parse(ini *ini) error { continue } - // ini value is ignored if override is set and - // value was previously set from non default - if i.ParseAsDefaults && !opt.isSetDefault { + // ini value is ignored if parsed as default but defaults are prevented + if i.ParseAsDefaults && opt.preventDefault { continue } @@ -576,7 +575,15 @@ func (i *IniParser) parse(ini *ini) error { } } - if err := opt.set(pval); err != nil { + var err error + + if i.ParseAsDefaults { + err = opt.setDefault(pval) + } else { + err = opt.set(pval) + } + + if err != nil { return &IniError{ Message: err.Error(), File: ini.File, @@ -584,6 +591,9 @@ func (i *IniParser) parse(ini *ini) error { } } + // Defaults from ini files take precendence over defaults from parser + opt.preventDefault = true + // either all INI values are quoted or only values who need quoting if _, ok := quotesLookup[opt]; !inival.Quoted || !ok { quotesLookup[opt] = inival.Quoted diff --git a/ini_test.go b/ini_test.go index 65fc3e9..72bc386 100644 --- a/ini_test.go +++ b/ini_test.go @@ -903,6 +903,51 @@ func TestIniOverrides(t *testing.T) { assertString(t, opts.ValueWithDefaultOverrideCli, "cli-value") } +func TestIniOverridesFromConfigFlag(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 { + Config func(filename string) `long:"config"` + ValueWithDefault string `long:"value-with-default" default:"value"` + ValueWithDefaultOverrideCli string `long:"value-with-default-override-cli" default:"value"` + } + + p := NewParser(&opts, Default) + + opt := p.FindOptionByLongName("config") + opt.Default = []string{file.Name()} + + opts.Config = func(filename string) { + parser := NewIniParser(p) + parser.ParseAsDefaults = true + parser.ParseFile(filename) + } + + _, 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 TestIniRequired(t *testing.T) { var opts struct { Required string `short:"r" required:"yes" description:"required"` diff --git a/option.go b/option.go index bc41916..cc441f8 100644 --- a/option.go +++ b/option.go @@ -280,6 +280,17 @@ func (option *Option) set(value *string) error { return convert("", option.value, option.tag) } +func (option *Option) setDefault(value *string) error { + if err := option.set(value); err != nil { + return err + } + + option.isSetDefault = true + option.preventDefault = false + + return nil +} + func (option *Option) showInHelp() bool { return !option.Hidden && (option.ShortName != 0 || len(option.LongName) != 0) } @@ -309,6 +320,10 @@ func (option *Option) empty() { } func (option *Option) clearDefault() error { + if option.preventDefault { + return nil + } + usedDefault := option.Default if envKey := option.EnvKeyWithNamespace(); envKey != "" { @@ -327,11 +342,11 @@ func (option *Option) clearDefault() error { option.empty() for _, d := range usedDefault { - err := option.set(&d) + err := option.setDefault(&d) + if err != nil { return err } - option.isSetDefault = true } } else { tp := option.value.Type() diff --git a/parser.go b/parser.go index f2e4237..1ebce81 100644 --- a/parser.go +++ b/parser.go @@ -311,10 +311,6 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { if s.err == nil { p.eachOption(func(c *Command, g *Group, option *Option) { - if option.preventDefault { - return - } - err := option.clearDefault() if err != nil { if _, ok := err.(*Error); !ok { From 1878de27329cba29066dc088d84b3ce743885f82 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 21 Mar 2021 11:31:49 +0100 Subject: [PATCH 50/71] Clear reference values (maps and slices) when first set Fixes #323. --- ini.go | 4 ++++ ini_test.go | 24 ++++++++++++++++++++++++ option.go | 16 +++++++++++----- parser.go | 13 +------------ 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/ini.go b/ini.go index 2223d7b..60b36c7 100644 --- a/ini.go +++ b/ini.go @@ -499,6 +499,10 @@ func (i *IniParser) matchingGroups(name string) []*Group { func (i *IniParser) parse(ini *ini) error { p := i.parser + p.eachOption(func(cmd *Command, group *Group, option *Option) { + option.clearReferenceBeforeSet = true + }) + var quotesLookup = make(map[*Option]bool) for name, section := range ini.Sections { diff --git a/ini_test.go b/ini_test.go index 72bc386..72c890c 100644 --- a/ini_test.go +++ b/ini_test.go @@ -971,6 +971,30 @@ func TestIniRequired(t *testing.T) { assertString(t, opts.Required, "cli-value") } +func TestIniRequiredSlice_ShouldNotNeedToBeSpecifiedOnCli(t *testing.T) { + type options struct { + Items []string `long:"item" required:"true"` + } + var opts options + ini := ` +[Application Options] +item=abc` + args := []string{} + + parser := NewParser(&opts, Default) + inip := NewIniParser(parser) + + inip.Parse(strings.NewReader(ini)) + + _, err := parser.ParseArgs(args) + + if err != nil { + t.Fatalf("Unexpected failure: %v", err) + } + + assertString(t, opts.Items[0], "abc") +} + func TestWriteFile(t *testing.T) { file, err := ioutil.TempFile("", "") if err != nil { diff --git a/option.go b/option.go index cc441f8..f6d6941 100644 --- a/option.go +++ b/option.go @@ -80,10 +80,11 @@ type Option struct { // Determines if the option will be always quoted in the INI output iniQuote bool - tag multiTag - isSet bool - isSetDefault bool - preventDefault bool + tag multiTag + isSet bool + isSetDefault bool + preventDefault bool + clearReferenceBeforeSet bool defaultLiteral string } @@ -241,12 +242,13 @@ func (option *Option) IsSetDefault() bool { func (option *Option) set(value *string) error { kind := option.value.Type().Kind() - if (kind == reflect.Map || kind == reflect.Slice) && !option.isSet { + if (kind == reflect.Map || kind == reflect.Slice) && option.clearReferenceBeforeSet { option.empty() } option.isSet = true option.preventDefault = true + option.clearReferenceBeforeSet = false if len(option.Choices) != 0 { found := false @@ -281,6 +283,10 @@ func (option *Option) set(value *string) error { } func (option *Option) setDefault(value *string) error { + if option.preventDefault { + return nil + } + if err := option.set(value); err != nil { return err } diff --git a/parser.go b/parser.go index 1ebce81..3fc3f7b 100644 --- a/parser.go +++ b/parser.go @@ -208,8 +208,7 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { } p.eachOption(func(c *Command, g *Group, option *Option) { - option.isSet = false - option.isSetDefault = false + option.clearReferenceBeforeSet = true option.updateDefaultLiteral() }) @@ -713,13 +712,3 @@ func (p *Parser) printError(err error) error { 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 b29baf28fc4bf2276a3a7fc4f5e79316743288c4 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 21 Mar 2021 11:49:04 +0100 Subject: [PATCH 51/71] Make option.set public Originally authored by Tim ABell. Closes #301. --- ini.go | 2 +- option.go | 4 ++-- parser.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ini.go b/ini.go index 60b36c7..6e3194e 100644 --- a/ini.go +++ b/ini.go @@ -584,7 +584,7 @@ func (i *IniParser) parse(ini *ini) error { if i.ParseAsDefaults { err = opt.setDefault(pval) } else { - err = opt.set(pval) + err = opt.Set(pval) } if err != nil { diff --git a/option.go b/option.go index f6d6941..257996a 100644 --- a/option.go +++ b/option.go @@ -239,7 +239,7 @@ func (option *Option) IsSetDefault() bool { // 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 { +func (option *Option) Set(value *string) error { kind := option.value.Type().Kind() if (kind == reflect.Map || kind == reflect.Slice) && option.clearReferenceBeforeSet { @@ -287,7 +287,7 @@ func (option *Option) setDefault(value *string) error { return nil } - if err := option.set(value); err != nil { + if err := option.Set(value); err != nil { return err } diff --git a/parser.go b/parser.go index 3fc3f7b..738f568 100644 --- a/parser.go +++ b/parser.go @@ -525,7 +525,7 @@ func (p *Parser) parseOption(s *parseState, name string, option *Option, canarg return newErrorf(ErrNoArgumentForBool, "bool flag `%s' cannot have an argument", option) } - err = option.set(nil) + err = option.Set(nil) } else if argument != nil || (canarg && !s.eof()) { var arg string @@ -546,13 +546,13 @@ func (p *Parser) parseOption(s *parseState, name string, option *Option, canarg } if err == nil { - err = option.set(&arg) + err = option.Set(&arg) } } else if option.OptionalArgument { option.empty() for _, v := range option.OptionalValue { - err = option.set(&v) + err = option.Set(&v) if err != nil { break From 8884f753895366fd343029de9069cbae0fcb11cb Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sun, 21 Mar 2021 11:53:54 +0100 Subject: [PATCH 52/71] Run travis on ppc --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 2fc5e5f..30dbe17 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,10 @@ os: - linux - osx +arch: + - amd64 + - ppc64le + go: - 1.16.x From c253d68d7bc61e54445f498f66d23107f3c2fb1e Mon Sep 17 00:00:00 2001 From: Alex Couture-Beil Date: Mon, 19 Jul 2021 17:04:13 -0700 Subject: [PATCH 53/71] Option to allow passing value to bool flag When AllowBoolValues Option is given, allow passing a true/false value to a boolean. This is useful when a user is calling a program from a shell script and wishes to programatically set the flag by passing along an existing variable from bash. Signed-off-by: Alex Couture-Beil --- parser.go | 9 +++-- parser_test.go | 102 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 97 insertions(+), 14 deletions(-) diff --git a/parser.go b/parser.go index 738f568..18bd2e1 100644 --- a/parser.go +++ b/parser.go @@ -113,6 +113,10 @@ const ( // POSIX processing. PassAfterNonOption + // AllowBoolValues allows a user to assign true/false to a boolean value + // rather than raising an error stating it cannot have an argument. + AllowBoolValues + // Default is a convenient default set of options which should cover // most of the uses of the flags package. Default = HelpFlag | PrintErrors | PassDoubleDash @@ -521,11 +525,10 @@ func (p *parseState) estimateCommand() error { func (p *Parser) parseOption(s *parseState, name string, option *Option, canarg bool, argument *string) (err error) { if !option.canArgument() { - if argument != nil { + if argument != nil && (p.Options&AllowBoolValues) == None { return newErrorf(ErrNoArgumentForBool, "bool flag `%s' cannot have an argument", option) } - - err = option.Set(nil) + err = option.Set(argument) } else if argument != nil || (canarg && !s.eof()) { var arg string diff --git a/parser_test.go b/parser_test.go index 5700bcd..9a951a0 100644 --- a/parser_test.go +++ b/parser_test.go @@ -37,9 +37,10 @@ type defaultOptions struct { func TestDefaults(t *testing.T) { var tests = []struct { - msg string - args []string - expected defaultOptions + msg string + args []string + expected defaultOptions + expectedErr string }{ { msg: "no arguments, expecting default values", @@ -91,6 +92,11 @@ func TestDefaults(t *testing.T) { SliceDefault: []int{3}, }, }, + { + msg: "non-zero value arguments, expecting overwritten arguments", + args: []string{"-3=true"}, + expectedErr: "bool flag `-3' cannot have an argument", + }, { msg: "zero value arguments, expecting overwritten arguments", args: []string{"--i=0", "--id=0", "--f=0", "--fd=0", "--str", "", "--strd=\"\"", "--t=0ms", "--td=0s", "--m=:0", "--md=:0", "--s=0", "--sd=0"}, @@ -120,16 +126,24 @@ func TestDefaults(t *testing.T) { var opts defaultOptions _, err := ParseArgs(&opts, test.args) - if err != nil { - t.Fatalf("%s:\nUnexpected error: %v", test.msg, err) - } + if test.expectedErr != "" { + if err == nil { + t.Errorf("%s:\nExpected error containing substring %q", test.msg, test.expectedErr) + } else if !strings.Contains(err.Error(), test.expectedErr) { + t.Errorf("%s:\nExpected error %q to contain substring %q", test.msg, err, test.expectedErr) + } + } else { + if err != nil { + t.Fatalf("%s:\nUnexpected error: %v", test.msg, err) + } - if opts.Slice == nil { - opts.Slice = []int{} - } + if opts.Slice == nil { + opts.Slice = []int{} + } - if !reflect.DeepEqual(opts, test.expected) { - t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts) + if !reflect.DeepEqual(opts, test.expected) { + t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts) + } } } } @@ -680,3 +694,69 @@ func TestCommandHandler(t *testing.T) { assertStringArray(t, executedArgs, []string{"arg1", "arg2"}) } + +func TestAllowBoolValues(t *testing.T) { + var tests = []struct { + msg string + args []string + expectedErr string + expected bool + expectedNonOptArgs []string + }{ + { + msg: "no value", + args: []string{"-v"}, + expected: true, + }, + { + msg: "true value", + args: []string{"-v=true"}, + expected: true, + }, + { + msg: "false value", + args: []string{"-v=false"}, + expected: false, + }, + { + msg: "bad value", + args: []string{"-v=badvalue"}, + expectedErr: `parsing "badvalue": invalid syntax`, + }, + { + // this test is to ensure flag values can only be specified as --flag=value and not "--flag value". + // if "--flag value" was supported it's not clear if value should be a non-optional argument + // or the value for the flag. + msg: "validate flags can only be set with a value immediately following an assignment operator (=)", + args: []string{"-v", "false"}, + expected: true, + expectedNonOptArgs: []string{"false"}, + }, + } + + for _, test := range tests { + var opts = struct { + Value bool `short:"v"` + }{} + parser := NewParser(&opts, AllowBoolValues) + nonOptArgs, err := parser.ParseArgs(test.args) + + if test.expectedErr == "" { + if err != nil { + t.Fatalf("%s:\nUnexpected parse error: %s", test.msg, err) + } + if opts.Value != test.expected { + t.Errorf("%s:\nExpected %v; got %v", test.msg, test.expected, opts.Value) + } + if len(test.expectedNonOptArgs) != len(nonOptArgs) && !reflect.DeepEqual(test.expectedNonOptArgs, nonOptArgs) { + t.Errorf("%s:\nUnexpected non-argument options\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.expectedNonOptArgs, nonOptArgs) + } + } else { + if err == nil { + t.Errorf("%s:\nExpected error containing substring %q", test.msg, test.expectedErr) + } else if !strings.Contains(err.Error(), test.expectedErr) { + t.Errorf("%s:\nExpected error %q to contain substring %q", test.msg, err, test.expectedErr) + } + } + } +} From 1980cc32fe37659719be43ce7b6d4525b2cc6da7 Mon Sep 17 00:00:00 2001 From: Cemre Mengu Date: Tue, 7 Sep 2021 13:00:12 +0300 Subject: [PATCH 54/71] docs: add ENV variable usage example --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index f22650b..ed6123c 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,9 @@ var opts struct { // Example of a map IntMap map[string]int `long:"intmap" description:"A map from string to int"` + + // Example of env variable + Thresholds []int `long:"thresholds" default:"1" default:"2" env:"THRESHOLD_VALUES" env-delim:","` } // Callback which will invoke callto: to call a number. From cbaa49749df064f23d012a4874251a29e2c8c3ae Mon Sep 17 00:00:00 2001 From: Anthony Fok Date: Thu, 2 Dec 2021 05:41:43 -0700 Subject: [PATCH 55/71] Add support for SOURCE_DATE_EPOCH for TestMan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This stems from of Marina Moore’s (@mnm678) excellent work on adding SOURCE_DATE_EPOCH for reproducible builds in PR #285, and has been extended by Jelmer Vernooij (@jelmer) such that TestMan in help_test.go would not fail when SOURCE_DATE_EPOCH is set to a date different from today. Reference: * https://salsa.debian.org/go-team/packages/golang-go-flags/-/merge_requests/1 * https://salsa.debian.org/go-team/packages/golang-go-flags/-/commit/3f11c5378d8d3778fbd6b249d6dd26e4785fd7ac --- help_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/help_test.go b/help_test.go index c713bba..dee8533 100644 --- a/help_test.go +++ b/help_test.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "runtime" + "strconv" "strings" "testing" "time" @@ -229,6 +230,14 @@ func TestMan(t *testing.T) { got := buf.String() tt := time.Now() + source_date_epoch := os.Getenv("SOURCE_DATE_EPOCH") + if source_date_epoch != "" { + sde, err := strconv.ParseInt(source_date_epoch, 10, 64) + if err != nil { + panic(fmt.Sprintf("Invalid SOURCE_DATE_EPOCH: %s", err)) + } + tt = time.Unix(sde, 0) + } var envDefaultName string From 282c5367c7f2a23feb20b16aec9b333d02e8e44b Mon Sep 17 00:00:00 2001 From: Leonardo Rocha Date: Wed, 23 Nov 2022 13:17:59 -0300 Subject: [PATCH 56/71] Update to run go-flags on AIX Without flag !aix, the library show the following error when you build to a cross environment aix/ppc64: github.com/jessevdk/go-flags@v1.4.0/termsize.go:19:27: undefined: syscall.SYS_IOCTL --- termsize.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/termsize.go b/termsize.go index 829e477..c23417c 100644 --- a/termsize.go +++ b/termsize.go @@ -1,4 +1,4 @@ -// +build !windows,!plan9,!appengine,!wasm +// +build !windows,!plan9,!appengine,!wasm,!aix package flags From 415ea36774cf3c3f86ae434bc0664bbd6dba5e2d Mon Sep 17 00:00:00 2001 From: Leonardo Rocha Date: Wed, 23 Nov 2022 13:19:10 -0300 Subject: [PATCH 57/71] Update termsize_nosysioctl.go Without flag aix, the library show the following error when you build to a cross environment aix/ppc64: github.com/jessevdk/go-flags@v1.4.0/help.go:65:20: undefined: getTerminalColumns --- termsize_nosysioctl.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/termsize_nosysioctl.go b/termsize_nosysioctl.go index c1ff186..e809607 100644 --- a/termsize_nosysioctl.go +++ b/termsize_nosysioctl.go @@ -1,4 +1,4 @@ -// +build plan9 appengine wasm +// +build plan9 appengine wasm aix package flags From eaefba95e5fb52f8295c2ce85bbc8bb2b7c86517 Mon Sep 17 00:00:00 2001 From: Ana Chua Date: Fri, 2 Dec 2022 09:48:17 +0800 Subject: [PATCH 58/71] Add option for key value delimiter --- convert.go | 7 ++++++- convert_test.go | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/convert.go b/convert.go index ef06d4b..2367592 100644 --- a/convert.go +++ b/convert.go @@ -271,7 +271,12 @@ func convert(val string, retval reflect.Value, options multiTag) error { retval.Set(reflect.Append(retval, elemval)) case reflect.Map: - parts := strings.SplitN(val, ":", 2) + keyValueDelimiter := options.Get("key-value-delimiter") + if keyValueDelimiter == "" { + keyValueDelimiter = ":" + } + + parts := strings.SplitN(val, keyValueDelimiter, 2) key := parts[0] var value string diff --git a/convert_test.go b/convert_test.go index ef131dc..21982ae 100644 --- a/convert_test.go +++ b/convert_test.go @@ -157,3 +157,22 @@ func TestConvertToStringInvalidUintBase(t *testing.T) { assertError(t, err, ErrMarshal, "strconv.ParseInt: parsing \"no\": invalid syntax") } + +func TestConvertToMapWithDelimiter(t *testing.T) { + var opts = struct { + StringStringMap map[string]string `long:"string-string-map" key-value-delimiter:"="` + }{} + + p := NewNamedParser("test", Default) + grp, _ := p.AddGroup("test group", "", &opts) + o := grp.Options()[0] + + err := convert("key=value", o.value, o.tag) + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + assertString(t, opts.StringStringMap["key"], "value") +} From 6c1df9db710abd5a5c55a8ab5c27abab1fffb78d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Virtus?= Date: Wed, 18 Jan 2023 15:14:13 +0100 Subject: [PATCH 59/71] Support per-command PassAfterNonOption Currently, PassAfterNonOption is a Parser flag. This means that it applies to all commands. Sometimes it's desirable have this behavior on only a subset of commands. For instance, for restart subcommand, we would like the default behavior of parsing both -v and --wait flags: mycmd restart -v foo bar --wait But for exec subcommand, we want only the -v flag to be parsed. The -l flag should be treated as positional argument. mycmd exec -v ls -l / Introduce PassAfterNonOption boolean field in Command structure that has the same meaning as the flag in Parser but applies only to the currently active command. The field can be set by adding pass-after-non-option tag to data structure declaration. This only allows to selectively enable the behavior in commands when PassAfterNonOption Parser flag is unset. When the flag is set, the behavior is enabled for all commands and cannot be disabled by false value of PassAfterNonOption field. This is to keep the code simple and backwards compatible (since PassAfterNonOption field defaults to false). --- command.go | 11 +++++ command_test.go | 115 ++++++++++++++++++++++++++++++++++++++++++++++++ parser.go | 2 +- 3 files changed, 127 insertions(+), 1 deletion(-) diff --git a/command.go b/command.go index 879465d..ac4f1e3 100644 --- a/command.go +++ b/command.go @@ -30,6 +30,12 @@ type Command struct { // Whether positional arguments are required ArgsRequired bool + // Whether to pass all arguments after the first non option as remaining + // command line arguments. This is equivalent to strict POSIX processing. + // This is command-local version of PassAfterNonOption Parser flag. It + // cannot be turned off when PassAfterNonOption Parser flag is set. + PassAfterNonOption bool + commands []*Command hasBuiltinHelpGroup bool args []*Arg @@ -244,6 +250,7 @@ func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler { longDescription := mtag.Get("long-description") subcommandsOptional := mtag.Get("subcommands-optional") aliases := mtag.GetMany("alias") + passAfterNonOption := mtag.Get("pass-after-non-option") subc, err := c.AddCommand(subcommand, shortDescription, longDescription, ptrval.Interface()) @@ -261,6 +268,10 @@ func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler { subc.Aliases = aliases } + if len(passAfterNonOption) > 0 { + subc.PassAfterNonOption = true + } + return true, nil } diff --git a/command_test.go b/command_test.go index 80d56be..e2019b4 100644 --- a/command_test.go +++ b/command_test.go @@ -648,3 +648,118 @@ func TestCommandPassAfterNonOptionWithPositional(t *testing.T) { assertStringArray(t, opts.Bar.args, []string{}) assertStringArray(t, opts.Bar.Positional.Args, []string{"baz", "-v", "-g"}) } + +type cmdLocalPassAfterNonOptionMix struct { + FlagA bool `short:"a"` + Cmd1 struct { + FlagB bool `short:"b"` + Positional struct { + Args []string + } `positional-args:"yes"` + } `command:"cmd1" pass-after-non-option:"yes"` + Cmd2 struct { + FlagB bool `short:"b"` + Positional struct { + Args []string + } `positional-args:"yes"` + } `command:"cmd2"` +} + +func TestCommandLocalPassAfterNonOptionMixCmd1(t *testing.T) { + var opts cmdLocalPassAfterNonOptionMix + + assertParseSuccess(t, &opts, "cmd1", "-b", "arg1", "-a", "arg2", "-x") + + if opts.FlagA { + t.Errorf("Expected FlagA to be false") + } + + if !opts.Cmd1.FlagB { + t.Errorf("Expected Cmd1.FlagB to be true") + } + + assertStringArray(t, opts.Cmd1.Positional.Args, []string{"arg1", "-a", "arg2", "-x"}) +} + +func TestCommandLocalPassAfterNonOptionMixCmd2(t *testing.T) { + var opts cmdLocalPassAfterNonOptionMix + + assertParseSuccess(t, &opts, "cmd2", "-b", "arg1", "-a", "arg2") + + if !opts.FlagA { + t.Errorf("Expected FlagA to be true") + } + + if !opts.Cmd2.FlagB { + t.Errorf("Expected Cmd2.FlagB to be true") + } + + assertStringArray(t, opts.Cmd2.Positional.Args, []string{"arg1", "arg2"}) +} + +func TestCommandLocalPassAfterNonOptionMixCmd2UnkownFlag(t *testing.T) { + var opts cmdLocalPassAfterNonOptionMix + + assertParseFail(t, ErrUnknownFlag, "unknown flag `x'", &opts, "cmd2", "-b", "arg1", "-a", "arg2", "-x") +} + +type cmdLocalPassAfterNonOptionNest struct { + FlagA bool `short:"a"` + Cmd1 struct { + FlagB bool `short:"b"` + Cmd2 struct { + FlagC bool `short:"c"` + Cmd3 struct { + FlagD bool `short:"d"` + } `command:"cmd3"` + } `command:"cmd2" subcommands-optional:"yes" pass-after-non-option:"yes"` + } `command:"cmd1"` +} + +func TestCommandLocalPassAfterNonOptionNest1(t *testing.T) { + var opts cmdLocalPassAfterNonOptionNest + + ret := assertParseSuccess(t, &opts, "cmd1", "cmd2", "-a", "x", "-b", "cmd3", "-c", "-d") + + if !opts.FlagA { + t.Errorf("Expected FlagA to be true") + } + + if opts.Cmd1.FlagB { + t.Errorf("Expected Cmd1.FlagB to be false") + } + + if opts.Cmd1.Cmd2.FlagC { + t.Errorf("Expected Cmd1.Cmd2.FlagC to be false") + } + + if opts.Cmd1.Cmd2.Cmd3.FlagD { + t.Errorf("Expected Cmd1.Cmd2.Cmd3.FlagD to be false") + } + + assertStringArray(t, ret, []string{"x", "-b", "cmd3", "-c", "-d"}) +} + +func TestCommandLocalPassAfterNonOptionNest2(t *testing.T) { + var opts cmdLocalPassAfterNonOptionNest + + ret := assertParseSuccess(t, &opts, "cmd1", "cmd2", "cmd3", "-a", "x", "-b", "-c", "-d") + + if !opts.FlagA { + t.Errorf("Expected FlagA to be true") + } + + if !opts.Cmd1.FlagB { + t.Errorf("Expected Cmd1.FlagB to be true") + } + + if !opts.Cmd1.Cmd2.FlagC { + t.Errorf("Expected Cmd1.Cmd2.FlagC to be true") + } + + if !opts.Cmd1.Cmd2.Cmd3.FlagD { + t.Errorf("Expected Cmd1.Cmd2.Cmd3.FlagD to be true") + } + + assertStringArray(t, ret, []string{"x"}) +} diff --git a/parser.go b/parser.go index 738f568..83bbb50 100644 --- a/parser.go +++ b/parser.go @@ -252,7 +252,7 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { } if !argumentIsOption(arg) { - if (p.Options&PassAfterNonOption) != None && s.lookup.commands[arg] == nil { + if ((p.Options&PassAfterNonOption) != None || s.command.PassAfterNonOption) && s.lookup.commands[arg] == nil { // If PassAfterNonOption is set then all remaining arguments // are considered positional if err = s.addArgs(s.arg); err != nil { From 9c12e344787cc5ba72f2894efca6dc631f57a3f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Feb 2023 07:46:22 +0000 Subject: [PATCH 60/71] Bump golang.org/x/sys from 0.0.0-20210320140829-1e4c9ba3b0c4 to 0.1.0 Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.0.0-20210320140829-1e4c9ba3b0c4 to 0.1.0. - [Release notes](https://github.com/golang/sys/releases) - [Commits](https://github.com/golang/sys/commits/v0.1.0) --- updated-dependencies: - dependency-name: golang.org/x/sys dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a626c5d..942941d 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module github.com/jessevdk/go-flags go 1.15 -require golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 +require golang.org/x/sys v0.1.0 diff --git a/go.sum b/go.sum index 7503251..b69ea85 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 9090f1aa2d91c842aa56fd36023df2f85a32deb4 Mon Sep 17 00:00:00 2001 From: "Pavel @grbit Griaznov" Date: Tue, 16 Apr 2024 14:09:55 +0200 Subject: [PATCH 61/71] undersocre support for numbers (set default base for strconv.Parse to 0) --- convert.go | 4 ++-- parser_test.go | 44 ++++++++++++++++++++++++++------------------ 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/convert.go b/convert.go index ef06d4b..82c0912 100644 --- a/convert.go +++ b/convert.go @@ -224,7 +224,7 @@ func convert(val string, retval reflect.Value, options multiTag) error { retval.SetBool(b) } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - base, err := getBase(options, 10) + base, err := getBase(options, 0) if err != nil { return err @@ -238,7 +238,7 @@ func convert(val string, retval reflect.Value, options multiTag) error { retval.SetInt(parsed) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - base, err := getBase(options, 10) + base, err := getBase(options, 0) if err != nil { return err diff --git a/parser_test.go b/parser_test.go index 5700bcd..29921b1 100644 --- a/parser_test.go +++ b/parser_test.go @@ -13,11 +13,13 @@ import ( ) type defaultOptions struct { - Int int `long:"i"` - IntDefault int `long:"id" default:"1"` + Int int `long:"i"` + IntDefault int `long:"id" default:"1"` + IntUnderscore int `long:"idu" default:"1_0"` - Float64 float64 `long:"f"` - Float64Default float64 `long:"fd" default:"-3.14"` + Float64 float64 `long:"f"` + Float64Default float64 `long:"fd" default:"-3.14"` + Float64Underscore float64 `long:"fdu" default:"-3_3.14"` NumericFlag bool `short:"3"` @@ -45,11 +47,13 @@ func TestDefaults(t *testing.T) { msg: "no arguments, expecting default values", args: []string{}, expected: defaultOptions{ - Int: 0, - IntDefault: 1, + Int: 0, + IntDefault: 1, + IntUnderscore: 10, - Float64: 0.0, - Float64Default: -3.14, + Float64: 0.0, + Float64Default: -3.14, + Float64Underscore: -33.14, NumericFlag: false, @@ -68,13 +72,15 @@ 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", "-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", "--idu=3_3", "--f=-2.71", "--fd=2.71", "--fdu=2_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, + Int: 3, + IntDefault: 3, + IntUnderscore: 33, - Float64: -2.71, - Float64Default: 2.71, + Float64: -2.71, + Float64Default: 2.71, + Float64Underscore: 22.71, NumericFlag: true, @@ -93,13 +99,15 @@ func TestDefaults(t *testing.T) { }, { msg: "zero value arguments, expecting overwritten arguments", - args: []string{"--i=0", "--id=0", "--f=0", "--fd=0", "--str", "", "--strd=\"\"", "--t=0ms", "--td=0s", "--m=:0", "--md=:0", "--s=0", "--sd=0"}, + args: []string{"--i=0", "--id=0", "--idu=0", "--f=0", "--fd=0", "--fdu=0", "--str", "", "--strd=\"\"", "--t=0ms", "--td=0s", "--m=:0", "--md=:0", "--s=0", "--sd=0"}, expected: defaultOptions{ - Int: 0, - IntDefault: 0, + Int: 0, + IntDefault: 0, + IntUnderscore: 0, - Float64: 0, - Float64Default: 0, + Float64: 0, + Float64Default: 0, + Float64Underscore: 0, String: "", StringDefault: "", From b2440162c07623d198fe6da5bada2d7c9b671349 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sat, 15 Jun 2024 11:37:49 +0200 Subject: [PATCH 62/71] Create go.yml --- .github/workflows/go.yml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/go.yml diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..1f51407 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,39 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Go + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + + - name: Build + run: go build -v ./... + + - name: Install dependencies + run: go get -v golang.org/x/lint/golint + + - name: Test + run: go test -v ./... + + - name: Format + run: exit $(gofmt -l . | wc -l) + + - name: Linting + run: | + go tool vet -all=true -v=true . || true + $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/golint ./... From c3489c6ff700f222b401eeb41ae2e87ed0221d76 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sat, 15 Jun 2024 11:30:34 +0200 Subject: [PATCH 63/71] UPgrade to x/sys v0.21.0 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 942941d..79f2a04 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module github.com/jessevdk/go-flags go 1.15 -require golang.org/x/sys v0.1.0 +require golang.org/x/sys v0.21.0 diff --git a/go.sum b/go.sum index b69ea85..ac7fb31 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= From 593b49bd12c9c3f9b8a320f65849234e337f30f0 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sat, 15 Jun 2024 11:32:21 +0200 Subject: [PATCH 64/71] Bump go version to 1.20 --- .travis.yml | 2 +- go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 30dbe17..ac15c41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ arch: - ppc64le go: - - 1.16.x + - 1.20.x install: # go-flags diff --git a/go.mod b/go.mod index 79f2a04..934cfbe 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module github.com/jessevdk/go-flags -go 1.15 +go 1.20 require golang.org/x/sys v0.21.0 From ef356de66d8005301eb4a1f25e329dcedff5a1f8 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sat, 15 Jun 2024 11:44:47 +0200 Subject: [PATCH 65/71] Fix workflow --- .github/workflows/go.yml | 36 ++++++++++++++------------------- .travis.yml | 43 ---------------------------------------- 2 files changed, 15 insertions(+), 64 deletions(-) delete mode 100644 .travis.yml diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1f51407..0ff3692 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -5,35 +5,29 @@ name: Go on: push: - branches: [ "master" ] + branches: ["master"] pull_request: - branches: [ "master" ] + branches: ["master"] jobs: - build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.20' + - uses: actions/checkout@v4 - - name: Build - run: go build -v ./... + - name: setup + uses: actions/setup-go@v4 + with: + go-version: "1.20" - - name: Install dependencies - run: go get -v golang.org/x/lint/golint + - name: build + run: go build -v ./... - - name: Test - run: go test -v ./... + - name: test + run: go test -v ./... - - name: Format - run: exit $(gofmt -l . | wc -l) + - name: gofmt + run: exit $(gofmt -l . | wc -l) - - name: Linting - run: | - go tool vet -all=true -v=true . || true - $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/golint ./... + - name: vet + run: go vet -all=true -v=true . diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ac15c41..0000000 --- a/.travis.yml +++ /dev/null @@ -1,43 +0,0 @@ -language: go - -os: - - linux - - osx - -arch: - - amd64 - - ppc64le - -go: - - 1.20.x - -install: - # go-flags - - go build -v ./... - - # linting - - go get -v golang.org/x/lint/golint - - # code coverage - - 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 - -script: - # go-flags - - $(exit $(gofmt -l . | wc -l)) - - go test -v ./... - - # linting - - go tool vet -all=true -v=true . || true - - $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/golint ./... - - # code coverage - - $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/ginkgo -r -cover - - $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/gover - - if [ "$TRAVIS_SECURE_ENV_VARS" = "true" ]; then $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/goveralls -coverprofile=gover.coverprofile -service=travis-ci -repotoken $COVERALLS_TOKEN; fi - -env: - # coveralls.io - secure: "RCYbiB4P0RjQRIoUx/vG/AjP3mmYCbzOmr86DCww1Z88yNcy3hYr3Cq8rpPtYU5v0g7wTpu4adaKIcqRE9xknYGbqj3YWZiCoBP1/n4Z+9sHW3Dsd9D/GRGeHUus0laJUGARjWoCTvoEtOgTdGQDoX7mH+pUUY0FBltNYUdOiiU=" From 30073cd056cadeb8671c6b83918e656067b2683c Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sat, 15 Jun 2024 11:45:38 +0200 Subject: [PATCH 66/71] fmt --- flags.go | 274 ++++++++++++++++++++--------------------- ini.go | 12 +- optstyle_other.go | 1 + optstyle_windows.go | 1 + termsize.go | 1 + termsize_nosysioctl.go | 1 + termsize_windows.go | 1 + 7 files changed, 145 insertions(+), 146 deletions(-) diff --git a/flags.go b/flags.go index ac2157d..a6acf1b 100644 --- a/flags.go +++ b/flags.go @@ -8,46 +8,45 @@ The flags package is similar in functionality to the go built-in flag package but provides more options and uses reflection to provide a convenient and succinct way of specifying command line options. - -Supported features +# Supported features The following features are supported in go-flags: - Options with short names (-v) - Options with long names (--verbose) - Options with and without arguments (bool v.s. other type) - Options with optional arguments and default values - Option default values from ENVIRONMENT_VARIABLES, including slice and map values - Multiple option groups each containing a set of options - Generate and print well-formatted help message - Passing remaining command line arguments after -- (optional) - Ignoring unknown command line options (optional) - Supports -I/usr/include -I=/usr/include -I /usr/include option argument specification - Supports multiple short options -aux - Supports all primitive go types (string, int{8..64}, uint{8..64}, float) - Supports same option multiple times (can store in slice or last option counts) - Supports maps - Supports function callbacks - Supports namespaces for (nested) option groups + Options with short names (-v) + Options with long names (--verbose) + Options with and without arguments (bool v.s. other type) + Options with optional arguments and default values + Option default values from ENVIRONMENT_VARIABLES, including slice and map values + Multiple option groups each containing a set of options + Generate and print well-formatted help message + Passing remaining command line arguments after -- (optional) + Ignoring unknown command line options (optional) + Supports -I/usr/include -I=/usr/include -I /usr/include option argument specification + Supports multiple short options -aux + Supports all primitive go types (string, int{8..64}, uint{8..64}, float) + Supports same option multiple times (can store in slice or last option counts) + Supports maps + Supports function callbacks + Supports namespaces for (nested) option groups Additional features specific to Windows: - Options with short names (/v) - Options with long names (/verbose) - Windows-style options with arguments use a colon as the delimiter - Modify generated help message with Windows-style / options - Windows style options can be disabled at build time using the "forceposix" - build tag + Options with short names (/v) + Options with long names (/verbose) + Windows-style options with arguments use a colon as the delimiter + Modify generated help message with Windows-style / options + Windows style options can be disabled at build time using the "forceposix" + build tag -Basic usage +# Basic usage 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"` - } + 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 @@ -60,9 +59,9 @@ whenever the option is encountered, a value is appended to the slice. Map options from string to primitive type are also supported. On the command line, you specify the value for such an option as key:value. For example - type Options struct { - AuthorInfo string[string] `short:"a"` - } + type Options struct { + AuthorInfo string[string] `short:"a"` + } Then, the AuthorInfo map can be filled with something like -a name:Jesse -a "surname:van den Kieboom". @@ -71,96 +70,94 @@ Finally, for full control over the conversion between command line argument values and options, user defined types can choose to implement the Marshaler and Unmarshaler interfaces. - -Available field tags +# Available field tags The following is a list of tags for struct fields supported by go-flags: - short: the short name of the option (single character) - long: the long name of the option - required: if non empty, makes the option required to appear on the command - line. If a required option is not present, the parser will - return ErrRequired (optional) - description: the description of the option (optional) - long-description: the long description of the option. Currently only - displayed in generated man pages (optional) - no-flag: if non-empty, this field is ignored as an option (optional) - - optional: if non-empty, makes the argument of the option 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) - default: the default value of an option. This tag can be specified - multiple times in the case of slices or maps (optional) - default-mask: when specified, this value will be displayed in the help - instead of the actual default value. This is useful - mostly for hiding otherwise sensitive information from - showing up in the help. If default-mask takes the special - value "-", then no default value will be shown at all - (optional) - env: the default value of the option is overridden from the - specified environment variable, if one has been defined. - (optional) - 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) - (optional) - choice: limits the values for an option to a set of values. - Repeat this tag once for each allowable value. - e.g. `long:"animal" choice:"cat" choice:"dog"` - hidden: if non-empty, the option is not visible in the help or man page. - - base: a base (radix) used to convert strings to integer values, the - default base is 10 (i.e. decimal) (optional) - - ini-name: the explicit ini option name (optional) - no-ini: if non-empty this field is ignored as an ini option - (optional) - - group: when specified on a struct field, makes the struct - field a separate group with the given name (optional) - namespace: when specified on a group struct field, the namespace - gets prepended to every option's long name and - subgroup's namespace of this group, separated by - the parser's namespace delimiter (optional) - env-namespace: when specified on a group struct field, the env-namespace - gets prepended to every option's env key and - subgroup's env-namespace of this group, separated by - the parser's env-namespace delimiter (optional) - command: when specified on a struct field, makes the struct - field a (sub)command with the given name (optional) - subcommands-optional: when specified on a command struct field, makes - any subcommands of that command optional (optional) - alias: when specified on a command struct field, adds the - specified name as an alias for the command. Can be - be specified multiple times to add more than one - alias (optional) - positional-args: when specified on a field with a struct type, - uses the fields of that struct to parse remaining - positional command line arguments into (in order - of the fields). If a field has a slice type, - 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. 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) + short: the short name of the option (single character) + long: the long name of the option + required: if non empty, makes the option required to appear on the command + line. If a required option is not present, the parser will + return ErrRequired (optional) + description: the description of the option (optional) + long-description: the long description of the option. Currently only + displayed in generated man pages (optional) + no-flag: if non-empty, this field is ignored as an option (optional) + + optional: if non-empty, makes the argument of the option 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) + default: the default value of an option. This tag can be specified + multiple times in the case of slices or maps (optional) + default-mask: when specified, this value will be displayed in the help + instead of the actual default value. This is useful + mostly for hiding otherwise sensitive information from + showing up in the help. If default-mask takes the special + value "-", then no default value will be shown at all + (optional) + env: the default value of the option is overridden from the + specified environment variable, if one has been defined. + (optional) + 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) + (optional) + choice: limits the values for an option to a set of values. + Repeat this tag once for each allowable value. + e.g. `long:"animal" choice:"cat" choice:"dog"` + hidden: if non-empty, the option is not visible in the help or man page. + + base: a base (radix) used to convert strings to integer values, the + default base is 10 (i.e. decimal) (optional) + + ini-name: the explicit ini option name (optional) + no-ini: if non-empty this field is ignored as an ini option + (optional) + + group: when specified on a struct field, makes the struct + field a separate group with the given name (optional) + namespace: when specified on a group struct field, the namespace + gets prepended to every option's long name and + subgroup's namespace of this group, separated by + the parser's namespace delimiter (optional) + env-namespace: when specified on a group struct field, the env-namespace + gets prepended to every option's env key and + subgroup's env-namespace of this group, separated by + the parser's env-namespace delimiter (optional) + command: when specified on a struct field, makes the struct + field a (sub)command with the given name (optional) + subcommands-optional: when specified on a command struct field, makes + any subcommands of that command optional (optional) + alias: when specified on a command struct field, adds the + specified name as an alias for the command. Can be + be specified multiple times to add more than one + alias (optional) + positional-args: when specified on a field with a struct type, + uses the fields of that struct to parse remaining + positional command line arguments into (in order + of the fields). If a field has a slice type, + 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. 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) Either the `short:` tag or the `long:` must be specified to make the field eligible as an option. - -Option groups +# Option groups Option groups are a simple way to semantically separate your options. All options in a particular group are shown together in the help under the name @@ -169,14 +166,12 @@ precisely and emphasize the options affiliation to their group. There are currently three ways to specify option groups. - 1. Use NewNamedParser specifying the various option groups. - 2. Use AddGroup to add a group to an existing parser. - 3. Add a struct field to the top-level options annotated with the - group:"group-name" tag. + 1. Use NewNamedParser specifying the various option groups. + 2. Use AddGroup to add a group to an existing parser. + 3. Add a struct field to the top-level options annotated with the + group:"group-name" tag. - - -Commands +# Commands The flags package also has basic support for commands. Commands are often used in monolithic applications that support various commands or actions. @@ -186,9 +181,9 @@ application. There are currently two ways to specify a command. - 1. Use AddCommand on an existing parser. - 2. Add a struct field to your options struct annotated with the - command:"command-name" tag. + 1. Use AddCommand on an existing parser. + 2. Add a struct field to your options struct annotated with the + command:"command-name" tag. The most common, idiomatic way to implement commands is to define a global parser instance and implement each command in a separate file. These @@ -204,15 +199,14 @@ 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: - ./app -v add - ./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 +# Completion go-flags has builtin support to provide bash completion of flags, commands and argument values. To use completion, the binary which uses go-flags @@ -226,7 +220,7 @@ by replacing the argument parsing routine with the completion routine which outputs completions for the passed arguments. The basic invocation to complete a set of arguments is therefore: - GO_FLAGS_COMPLETION=1 ./completion-example arg1 arg2 arg3 + GO_FLAGS_COMPLETION=1 ./completion-example arg1 arg2 arg3 where `completion-example` is the binary, `arg1` and `arg2` are the current arguments, and `arg3` (the last argument) is the argument @@ -237,20 +231,20 @@ are more than 1 completion items. To use this with bash completion, a simple file can be written which calls the binary which supports go-flags completion: - _completion_example() { - # All arguments except the first one - args=("${COMP_WORDS[@]:1:$COMP_CWORD}") + _completion_example() { + # All arguments except the first one + args=("${COMP_WORDS[@]:1:$COMP_CWORD}") - # Only split on newlines - local IFS=$'\n' + # Only split on newlines + local IFS=$'\n' - # Call completion (note that the first element of COMP_WORDS is - # the executable itself) - COMPREPLY=($(GO_FLAGS_COMPLETION=1 ${COMP_WORDS[0]} "${args[@]}")) - return 0 - } + # Call completion (note that the first element of COMP_WORDS is + # the executable itself) + COMPREPLY=($(GO_FLAGS_COMPLETION=1 ${COMP_WORDS[0]} "${args[@]}")) + return 0 + } - complete -F _completion_example completion-example + complete -F _completion_example completion-example Completion requires the parser option PassDoubleDash and is therefore enforced if the environment variable GO_FLAGS_COMPLETION is set. diff --git a/ini.go b/ini.go index 6e3194e..565595e 100644 --- a/ini.go +++ b/ini.go @@ -113,18 +113,18 @@ func (i *IniParser) ParseFile(filename string) error { // // The format of the ini file is as follows: // -// [Option group name] -// option = value +// [Option group name] +// option = value // // Each section in the ini file represents an option group or command in the // flags parser. The default flags parser option group (i.e. when using // flags.Parse) is named 'Application Options'. The ini option name is matched // in the following order: // -// 1. Compared to the ini-name tag on the option struct field (if present) -// 2. Compared to the struct field name -// 3. Compared to the option long name (if present) -// 4. Compared to the option short name (if present) +// 1. Compared to the ini-name tag on the option struct field (if present) +// 2. Compared to the struct field name +// 3. Compared to the option long name (if present) +// 4. Compared to the option short name (if present) // // Sections for nested groups and commands can be addressed using a dot `.' // namespacing notation (i.e [subcommand.Options]). Group section names are diff --git a/optstyle_other.go b/optstyle_other.go index 56dfdae..f84b697 100644 --- a/optstyle_other.go +++ b/optstyle_other.go @@ -1,3 +1,4 @@ +//go:build !windows || forceposix // +build !windows forceposix package flags diff --git a/optstyle_windows.go b/optstyle_windows.go index f3f28ae..e802904 100644 --- a/optstyle_windows.go +++ b/optstyle_windows.go @@ -1,3 +1,4 @@ +//go:build !forceposix // +build !forceposix package flags diff --git a/termsize.go b/termsize.go index c23417c..7bcf66f 100644 --- a/termsize.go +++ b/termsize.go @@ -1,3 +1,4 @@ +//go:build !windows && !plan9 && !appengine && !wasm && !aix // +build !windows,!plan9,!appengine,!wasm,!aix package flags diff --git a/termsize_nosysioctl.go b/termsize_nosysioctl.go index e809607..d839220 100644 --- a/termsize_nosysioctl.go +++ b/termsize_nosysioctl.go @@ -1,3 +1,4 @@ +//go:build plan9 || appengine || wasm || aix // +build plan9 appengine wasm aix package flags diff --git a/termsize_windows.go b/termsize_windows.go index 5c0fa6b..189a1b3 100644 --- a/termsize_windows.go +++ b/termsize_windows.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package flags From 4eda719c013c9de01dff62fcf4c1543316e0a200 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sat, 15 Jun 2024 11:49:12 +0200 Subject: [PATCH 67/71] Minor cleanup unused parameter --- completion.go | 6 +++--- optstyle_other.go | 2 +- parser.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/completion.go b/completion.go index 8ed61f1..dc6a447 100644 --- a/completion.go +++ b/completion.go @@ -188,8 +188,8 @@ func (c *completion) complete(args []string) []Completion { } if argumentIsOption(arg) { - prefix, optname, islong := stripOptionPrefix(arg) - optname, _, argument := splitOption(prefix, optname, islong) + _, optname, islong := stripOptionPrefix(arg) + optname, _, argument := splitOption(optname, islong) if argument == nil { var o *Option @@ -250,7 +250,7 @@ func (c *completion) complete(args []string) []Completion { } else if argumentStartsOption(lastarg) { // Complete the option prefix, optname, islong := stripOptionPrefix(lastarg) - optname, split, argument := splitOption(prefix, optname, islong) + optname, split, argument := splitOption(optname, islong) if argument == nil && !islong { rname, n := utf8.DecodeRuneInString(optname) diff --git a/optstyle_other.go b/optstyle_other.go index f84b697..2053db9 100644 --- a/optstyle_other.go +++ b/optstyle_other.go @@ -43,7 +43,7 @@ func stripOptionPrefix(optname string) (prefix string, name string, islong bool) // splitOption attempts to split the passed option into a name and an argument. // When there is no argument specified, nil will be returned for it. -func splitOption(prefix string, option string, islong bool) (string, string, *string) { +func splitOption(option string, islong bool) (string, string, *string) { pos := strings.Index(option, "=") if (islong && pos >= 0) || (!islong && pos == 1) { diff --git a/parser.go b/parser.go index 939dd7b..f3fb650 100644 --- a/parser.go +++ b/parser.go @@ -279,8 +279,8 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { continue } - prefix, optname, islong := stripOptionPrefix(arg) - optname, _, argument := splitOption(prefix, optname, islong) + _, optname, islong := stripOptionPrefix(arg) + optname, _, argument := splitOption(optname, islong) if islong { err = p.parseLong(s, optname, argument) From 01b08739b31385d8aff2009f0594260079b584e7 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sat, 15 Jun 2024 11:49:19 +0200 Subject: [PATCH 68/71] Remove test that does not pass vet --- tag_test.go | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 tag_test.go diff --git a/tag_test.go b/tag_test.go deleted file mode 100644 index 2c34323..0000000 --- a/tag_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package flags - -import ( - "testing" -) - -func TestTagMissingColon(t *testing.T) { - var opts = struct { - TestValue bool `short` - }{} - - assertParseFail(t, ErrTag, "expected `:' after key name, but got end of tag (in `short`)", &opts, "") -} - -func TestTagMissingValue(t *testing.T) { - var opts = struct { - TestValue bool `short:` - }{} - - assertParseFail(t, ErrTag, "expected `\"' to start tag value at end of tag (in `short:`)", &opts, "") -} - -func TestTagMissingQuote(t *testing.T) { - var opts = struct { - TestValue bool `short:"v` - }{} - - assertParseFail(t, ErrTag, "expected end of tag value `\"' at end of tag (in `short:\"v`)", &opts, "") -} - -func TestTagNewline(t *testing.T) { - var opts = struct { - TestValue bool `long:"verbose" description:"verbose -something"` - }{} - - assertParseFail(t, ErrTag, "unexpected newline in tag value `description' (in `long:\"verbose\" description:\"verbose\nsomething\"`)", &opts, "") -} From 7e06247c94519a9e84b4f23563430b4ace811dc5 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sat, 15 Jun 2024 11:53:33 +0200 Subject: [PATCH 69/71] Remove unused status --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ed6123c..759eeb0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ go-flags: a go library for parsing command line arguments ========================================================= -[![GoDoc](https://godoc.org/github.com/jessevdk/go-flags?status.png)](https://godoc.org/github.com/jessevdk/go-flags) [![Build Status](https://travis-ci.org/jessevdk/go-flags.svg?branch=master)](https://travis-ci.org/jessevdk/go-flags) [![Coverage Status](https://img.shields.io/coveralls/jessevdk/go-flags.svg)](https://coveralls.io/r/jessevdk/go-flags?branch=master) +[![GoDoc](https://godoc.org/github.com/jessevdk/go-flags?status.png)](https://godoc.org/github.com/jessevdk/go-flags) This library provides similar functionality to the builtin flag library of go, but provides much more functionality and nicer formatting. From the @@ -78,7 +78,7 @@ var opts struct { // Example of a map IntMap map[string]int `long:"intmap" description:"A map from string to int"` - + // Example of env variable Thresholds []int `long:"thresholds" default:"1" default:"2" env:"THRESHOLD_VALUES" env-delim:","` } From c573fc0a2fbbc56a40370c47d314a8162cb1aaf1 Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sat, 15 Jun 2024 11:55:01 +0200 Subject: [PATCH 70/71] Update for main branch rename --- .github/workflows/go.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 0ff3692..27e6e7d 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -5,9 +5,9 @@ name: Go on: push: - branches: ["master"] + branches: ["main"] pull_request: - branches: ["master"] + branches: ["main"] jobs: build: From c02e333e441eb1187c25e6d689d769d499ec2a0b Mon Sep 17 00:00:00 2001 From: Jesse van den Kieboom Date: Sat, 15 Jun 2024 14:24:44 +0200 Subject: [PATCH 71/71] Revert "Minor cleanup unused parameter" This reverts commit 4eda719c013c9de01dff62fcf4c1543316e0a200. --- completion.go | 6 +++--- optstyle_other.go | 2 +- parser.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/completion.go b/completion.go index dc6a447..8ed61f1 100644 --- a/completion.go +++ b/completion.go @@ -188,8 +188,8 @@ func (c *completion) complete(args []string) []Completion { } if argumentIsOption(arg) { - _, optname, islong := stripOptionPrefix(arg) - optname, _, argument := splitOption(optname, islong) + prefix, optname, islong := stripOptionPrefix(arg) + optname, _, argument := splitOption(prefix, optname, islong) if argument == nil { var o *Option @@ -250,7 +250,7 @@ func (c *completion) complete(args []string) []Completion { } else if argumentStartsOption(lastarg) { // Complete the option prefix, optname, islong := stripOptionPrefix(lastarg) - optname, split, argument := splitOption(optname, islong) + optname, split, argument := splitOption(prefix, optname, islong) if argument == nil && !islong { rname, n := utf8.DecodeRuneInString(optname) diff --git a/optstyle_other.go b/optstyle_other.go index 2053db9..f84b697 100644 --- a/optstyle_other.go +++ b/optstyle_other.go @@ -43,7 +43,7 @@ func stripOptionPrefix(optname string) (prefix string, name string, islong bool) // splitOption attempts to split the passed option into a name and an argument. // When there is no argument specified, nil will be returned for it. -func splitOption(option string, islong bool) (string, string, *string) { +func splitOption(prefix string, option string, islong bool) (string, string, *string) { pos := strings.Index(option, "=") if (islong && pos >= 0) || (!islong && pos == 1) { diff --git a/parser.go b/parser.go index f3fb650..939dd7b 100644 --- a/parser.go +++ b/parser.go @@ -279,8 +279,8 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { continue } - _, optname, islong := stripOptionPrefix(arg) - optname, _, argument := splitOption(optname, islong) + prefix, optname, islong := stripOptionPrefix(arg) + optname, _, argument := splitOption(prefix, optname, islong) if islong { err = p.parseLong(s, optname, argument)