diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..27e6e7d --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,33 @@ +# 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: ["main"] + pull_request: + branches: ["main"] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: setup + uses: actions/setup-go@v4 + with: + go-version: "1.20" + + - name: build + run: go build -v ./... + + - name: test + run: go test -v ./... + + - name: gofmt + run: exit $(gofmt -l . | wc -l) + + - name: vet + run: go vet -all=true -v=true . diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0f0728d..0000000 --- a/.travis.yml +++ /dev/null @@ -1,44 +0,0 @@ -language: go - -os: - - linux - - osx - -go: - - 1.x - - 1.7.x - - 1.8.x - - 1.9.x - - 1.10.x - -install: - # go-flags - - go get -d -v ./... - - go build -v ./... - - # linting - - go get github.com/golang/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=" diff --git a/README.md b/README.md index 3b02394..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 @@ -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 + 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"` @@ -75,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. @@ -91,6 +97,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", @@ -115,6 +122,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]) 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 } diff --git a/check_crosscompile.sh b/check_crosscompile.sh index c494f61..5edc430 100755 --- a/check_crosscompile.sh +++ b/check_crosscompile.sh @@ -14,3 +14,7 @@ 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 +echo '# solaris amd64' +GOARCH=amd64 GOOS=solaris go build diff --git a/command.go b/command.go index 486bacb..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 } @@ -438,7 +449,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 +458,7 @@ func (c *Command) hasCliOptions() bool { } for _, opt := range g.options { - if opt.canCli() { + if opt.showInHelp() { ret = true } } diff --git a/command_test.go b/command_test.go index dc04b66..e2019b4 100644 --- a/command_test.go +++ b/command_test.go @@ -580,3 +580,186 @@ 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"}) +} + +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/completion.go b/completion.go index 7a7a08b..8ed61f1 100644 --- a/completion.go +++ b/completion.go @@ -2,6 +2,7 @@ package flags import ( "fmt" + "os" "path/filepath" "reflect" "sort" @@ -62,6 +63,11 @@ func completionsWithoutDescriptions(items []string) []Completion { // prefix. 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[0] = ret[0] + "/" + } + } return completionsWithoutDescriptions(ret) } @@ -76,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, }, } @@ -124,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 26f70e4..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 { @@ -84,6 +87,13 @@ func init() { completionTestFilename := []string{filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion_test.go")} + completionTestSubdir := []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 +237,20 @@ 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") + "/"}, + completionTestSubdir, + false, + }, + { // Custom completed []string{"rename", "-c", "hello un"}, diff --git a/convert.go b/convert.go index 984aac8..b27f698 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") @@ -44,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 @@ -59,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 @@ -211,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 @@ -225,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 @@ -258,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") +} 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) } } diff --git a/flags.go b/flags.go index 889762d..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,91 +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. - This tag can be specified multiple times (optional) - 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) - 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 @@ -164,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. @@ -181,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 @@ -199,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 @@ -221,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 @@ -232,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/go.mod b/go.mod new file mode 100644 index 0000000..934cfbe --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/jessevdk/go-flags + +go 1.20 + +require golang.org/x/sys v0.21.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ac7fb31 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/group.go b/group.go index 9e057ab..181caab 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 @@ -70,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, data interface{}) { + option.value = reflect.ValueOf(data) + 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 @@ -165,6 +175,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) @@ -358,6 +380,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/group_test.go b/group_test.go index 18cd6c1..409a7f2 100644 --- a/group_test.go +++ b/group_test.go @@ -253,3 +253,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", + }, &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") + } +} diff --git a/help.go b/help.go index d380305..8fd3244 100644 --- a/help.go +++ b/help.go @@ -76,10 +76,13 @@ func (p *Parser) getAlignmentInfo() alignmentInfo { 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.canCli() { + if !info.showInHelp() { continue } @@ -225,12 +228,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) } @@ -305,7 +308,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) } @@ -331,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) } @@ -393,7 +400,7 @@ func (p *Parser) WriteHelp(writer io.Writer) { } for _, info := range grp.options { - if !info.canCli() || info.Hidden { + if !info.showInHelp() { continue } @@ -489,3 +496,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..dee8533 100644 --- a/help_test.go +++ b/help_test.go @@ -3,9 +3,11 @@ package flags import ( "bufio" "bytes" + "errors" "fmt" "os" "runtime" + "strconv" "strings" "testing" "time" @@ -26,6 +28,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 { @@ -35,8 +39,13 @@ 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 { + 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"` @@ -47,6 +56,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"` @@ -55,6 +68,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"` @@ -88,7 +108,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 +151,13 @@ Arguments: num: A number 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 @@ -177,7 +199,9 @@ Arguments: num: A number Available commands: + bommand A command with only hidden options command A command (aliases: cm, cmd) + parent A parent command ` } @@ -193,10 +217,12 @@ 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) - 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) @@ -204,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 @@ -219,7 +253,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 @@ -274,6 +309,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 @@ -287,6 +326,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: TestMan [OPTIONS] parent [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") @@ -342,6 +399,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 @@ -536,3 +678,25 @@ func TestHelpDefaultMask(t *testing.T) { } } } + +func TestWroteHelp(t *testing.T) { + type testInfo struct { + value error + isHelp bool + } + tests := map[string]testInfo{ + "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 { + t.Run(name, func(t *testing.T) { + res := WroteHelp(test.value) + if test.isHelp != res { + t.Errorf("Expected %t, got %t", test.isHelp, res) + } + }) + } +} diff --git a/ini.go b/ini.go index e714d3d..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 @@ -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) } } @@ -499,13 +499,21 @@ 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 { 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 { @@ -537,9 +545,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 } @@ -572,7 +579,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, @@ -580,6 +595,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 ad4852e..72c890c 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 + `, }, } @@ -855,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"` @@ -878,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/man.go b/man.go index 0cb114e..82572f9 100644 --- a/man.go +++ b/man.go @@ -3,42 +3,55 @@ package flags import ( "fmt" "io" + "os" "runtime" + "strconv" "strings" "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:] } } func writeManPageOptions(wr io.Writer, grp *Group) { grp.eachGroup(func(group *Group) { - if group.Hidden || len(group.options) == 0 { + if !group.showInHelp() { return } @@ -48,13 +61,13 @@ 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, "") } } for _, opt := range group.options { - if !opt.canCli() || opt.Hidden { + if !opt.showInHelp() { continue } @@ -83,11 +96,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())) } } @@ -98,14 +111,14 @@ 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, "") } } }) } -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 { @@ -121,11 +134,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) @@ -137,30 +150,27 @@ func writeManPageCommand(wr io.Writer, name string, root *Command, command *Comm 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, "") } } + var pre = usagePrefix + " " + command.Name + 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() { - 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 { @@ -168,17 +178,25 @@ 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 // 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") - 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 @@ -190,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") @@ -200,6 +218,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) } } diff --git a/option.go b/option.go index 5f85250..257996a 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 } @@ -139,6 +140,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 { @@ -187,15 +239,16 @@ 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.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 @@ -229,8 +282,23 @@ func (option *Option) set(value *string) error { return convert("", option.value, option.tag) } -func (option *Option) canCli() bool { - return option.ShortName != 0 || len(option.LongName) != 0 +func (option *Option) setDefault(value *string) error { + if option.preventDefault { + return nil + } + + 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) } func (option *Option) canArgument() bool { @@ -257,14 +325,17 @@ func (option *Option) empty() { } } -func (option *Option) clearDefault() { +func (option *Option) clearDefault() error { + if option.preventDefault { + return nil + } + usedDefault := option.Default - if envKey := option.EnvDefaultKey; envKey != "" { + if envKey := option.EnvKeyWithNamespace(); envKey != "" { if value, ok := os.LookupEnv(envKey); ok { if option.EnvDefaultDelim != "" { - usedDefault = strings.Split(value, - option.EnvDefaultDelim) + usedDefault = strings.Split(value, option.EnvDefaultDelim) } else { usedDefault = []string{value} } @@ -277,8 +348,11 @@ func (option *Option) clearDefault() { option.empty() for _, d := range usedDefault { - option.set(&d) - option.isSetDefault = true + err := option.setDefault(&d) + + if err != nil { + return err + } } } else { tp := option.value.Type() @@ -294,6 +368,8 @@ func (option *Option) clearDefault() { } } } + + return nil } func (option *Option) valueIsDefault() bool { @@ -339,6 +415,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() @@ -457,3 +557,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/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/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/parser.go b/parser.go index 0a7922a..939dd7b 100644 --- a/parser.go +++ b/parser.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "path" + "reflect" "sort" "strings" "unicode/utf8" @@ -29,6 +30,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 @@ -109,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 @@ -170,9 +178,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 @@ -203,8 +212,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() }) @@ -237,6 +245,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 +256,20 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { } if !argumentIsOption(arg) { + 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 { + break + } + + 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 { @@ -256,8 +279,6 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { continue } - var err error - prefix, optname, islong := stripOptionPrefix(arg) optname, _, argument := splitOption(prefix, optname, islong) @@ -293,11 +314,13 @@ 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 { + err = p.marshalError(option, err) + } + s.err = err } - - option.clearDefault() }) s.checkRequired(p) @@ -502,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 @@ -515,8 +537,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) } @@ -527,13 +549,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 @@ -545,16 +567,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 @@ -649,23 +692,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 { @@ -688,13 +715,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) -} diff --git a/parser_test.go b/parser_test.go index 374f21c..df37044 100644 --- a/parser_test.go +++ b/parser_test.go @@ -1,6 +1,7 @@ package flags import ( + "errors" "fmt" "os" "reflect" @@ -12,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"` @@ -36,19 +39,22 @@ 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", 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, @@ -67,13 +73,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, @@ -90,15 +98,22 @@ 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"}, + 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: "", @@ -119,16 +134,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) + } } } } @@ -245,19 +268,25 @@ 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) { 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", @@ -267,6 +296,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 +309,64 @@ 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: "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"}, + 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", }, }, } @@ -328,21 +380,45 @@ func TestEnvDefaults(t *testing.T) { for envKey, envValue := range test.env { os.Setenv(envKey, envValue) } - _, err := ParseArgs(&opts, test.args) - if err != nil { - t.Fatalf("%s:\nUnexpected error: %v", test.msg, err) - } + _, 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) + } 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) + } } } } +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 @@ -399,30 +475,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 { @@ -610,3 +702,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) + } + } + } +} diff --git a/tag_test.go b/tag_test.go deleted file mode 100644 index 9daa740..0000000 --- a/tag_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package flags - -import ( - "testing" -) - -func TestTagMissingColon(t *testing.T) { - var opts = struct { - Value 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 { - Value 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 { - Value 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 { - Value bool `long:"verbose" description:"verbose -something"` - }{} - - assertParseFail(t, ErrTag, "unexpected newline in tag value `description' (in `long:\"verbose\" description:\"verbose\nsomething\"`)", &opts, "") -} diff --git a/termsize.go b/termsize.go index 1ca6a85..7bcf66f 100644 --- a/termsize.go +++ b/termsize.go @@ -1,28 +1,16 @@ -// +build !windows,!plan9,!solaris,!appengine +//go:build !windows && !plan9 && !appengine && !wasm && !aix +// +build !windows,!plan9,!appengine,!wasm,!aix 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 3d5385b..d839220 100644 --- a/termsize_nosysioctl.go +++ b/termsize_nosysioctl.go @@ -1,4 +1,5 @@ -// +build windows plan9 solaris appengine +//go:build plan9 || appengine || wasm || aix +// +build plan9 appengine wasm aix package flags diff --git a/termsize_windows.go b/termsize_windows.go new file mode 100644 index 0000000..189a1b3 --- /dev/null +++ b/termsize_windows.go @@ -0,0 +1,86 @@ +//go:build windows +// +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 +} 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 -)