git-subtree-dir: vendor/github.com/alexflint/go-arg git-subtree-mainline:f34def0639git-subtree-split:7c77c70f85
| @ -0,0 +1,24 @@ | |||
| # Compiled Object files, Static and Dynamic libs (Shared Objects) | |||
| *.o | |||
| *.a | |||
| *.so | |||
| # Folders | |||
| _obj | |||
| _test | |||
| # Architecture specific extensions/prefixes | |||
| *.[568vq] | |||
| [568vq].out | |||
| *.cgo1.go | |||
| *.cgo2.c | |||
| _cgo_defun.c | |||
| _cgo_gotypes.go | |||
| _cgo_export.* | |||
| _testmain.go | |||
| *.exe | |||
| *.test | |||
| *.prof | |||
| @ -0,0 +1,9 @@ | |||
| language: go | |||
| go: | |||
| - tip | |||
| before_install: | |||
| - go get github.com/axw/gocov/gocov | |||
| - go get github.com/mattn/goveralls | |||
| - if ! go get github.com/golang/tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi | |||
| script: | |||
| - $HOME/gopath/bin/goveralls -service=travis-ci | |||
| @ -0,0 +1,24 @@ | |||
| Copyright (c) 2015, Alex Flint | |||
| All rights reserved. | |||
| Redistribution and use in source and binary forms, with or without | |||
| modification, are permitted provided that the following conditions are met: | |||
| * Redistributions of source code must retain the above copyright notice, this | |||
| list of conditions and the following disclaimer. | |||
| * Redistributions in binary form must reproduce the above copyright notice, | |||
| this list of conditions and the following disclaimer in the documentation | |||
| and/or other materials provided with the distribution. | |||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |||
| AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||
| IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |||
| DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |||
| FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |||
| DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |||
| SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |||
| CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |||
| OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |||
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |||
| @ -0,0 +1,278 @@ | |||
| [](https://godoc.org/github.com/alexflint/go-arg) | |||
| [](https://travis-ci.org/alexflint/go-arg) | |||
| [](https://coveralls.io/github/alexflint/go-arg?branch=master) | |||
| [](https://goreportcard.com/badge/github.com/alexflint/go-arg) | |||
| ## Structured argument parsing for Go | |||
| ```shell | |||
| go get github.com/alexflint/go-arg | |||
| ``` | |||
| Declare the command line arguments your program accepts by defining a struct. | |||
| ```go | |||
| var args struct { | |||
| Foo string | |||
| Bar bool | |||
| } | |||
| arg.MustParse(&args) | |||
| fmt.Println(args.Foo, args.Bar) | |||
| ``` | |||
| ```shell | |||
| $ ./example --foo=hello --bar | |||
| hello true | |||
| ``` | |||
| ### Required arguments | |||
| ```go | |||
| var args struct { | |||
| ID int `arg:"required"` | |||
| Timeout time.Duration | |||
| } | |||
| arg.MustParse(&args) | |||
| ``` | |||
| ```shell | |||
| $ ./example | |||
| usage: example --id ID [--timeout TIMEOUT] | |||
| error: --id is required | |||
| ``` | |||
| ### Positional arguments | |||
| ```go | |||
| var args struct { | |||
| Input string `arg:"positional"` | |||
| Output []string `arg:"positional"` | |||
| } | |||
| arg.MustParse(&args) | |||
| fmt.Println("Input:", args.Input) | |||
| fmt.Println("Output:", args.Output) | |||
| ``` | |||
| ``` | |||
| $ ./example src.txt x.out y.out z.out | |||
| Input: src.txt | |||
| Output: [x.out y.out z.out] | |||
| ``` | |||
| ### Environment variables | |||
| ```go | |||
| var args struct { | |||
| Workers int `arg:"env"` | |||
| } | |||
| arg.MustParse(&args) | |||
| fmt.Println("Workers:", args.Workers) | |||
| ``` | |||
| ``` | |||
| $ WORKERS=4 ./example | |||
| Workers: 4 | |||
| ``` | |||
| ``` | |||
| $ WORKERS=4 ./example --workers=6 | |||
| Workers: 6 | |||
| ``` | |||
| You can also override the name of the environment variable: | |||
| ```go | |||
| var args struct { | |||
| Workers int `arg:"env:NUM_WORKERS"` | |||
| } | |||
| arg.MustParse(&args) | |||
| fmt.Println("Workers:", args.Workers) | |||
| ``` | |||
| ``` | |||
| $ NUM_WORKERS=4 ./example | |||
| Workers: 4 | |||
| ``` | |||
| ### Usage strings | |||
| ```go | |||
| var args struct { | |||
| Input string `arg:"positional"` | |||
| Output []string `arg:"positional"` | |||
| Verbose bool `arg:"-v,help:verbosity level"` | |||
| Dataset string `arg:"help:dataset to use"` | |||
| Optimize int `arg:"-O,help:optimization level"` | |||
| } | |||
| arg.MustParse(&args) | |||
| ``` | |||
| ```shell | |||
| $ ./example -h | |||
| usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]] | |||
| positional arguments: | |||
| input | |||
| output | |||
| options: | |||
| --verbose, -v verbosity level | |||
| --dataset DATASET dataset to use | |||
| --optimize OPTIMIZE, -O OPTIMIZE | |||
| optimization level | |||
| --help, -h print this help message | |||
| ``` | |||
| ### Default values | |||
| ```go | |||
| var args struct { | |||
| Foo string | |||
| Bar bool | |||
| } | |||
| args.Foo = "default value" | |||
| arg.MustParse(&args) | |||
| ``` | |||
| ### Arguments with multiple values | |||
| ```go | |||
| var args struct { | |||
| Database string | |||
| IDs []int64 | |||
| } | |||
| arg.MustParse(&args) | |||
| fmt.Printf("Fetching the following IDs from %s: %q", args.Database, args.IDs) | |||
| ``` | |||
| ```shell | |||
| ./example -database foo -ids 1 2 3 | |||
| Fetching the following IDs from foo: [1 2 3] | |||
| ``` | |||
| ### Custom validation | |||
| ```go | |||
| var args struct { | |||
| Foo string | |||
| Bar string | |||
| } | |||
| p := arg.MustParse(&args) | |||
| if args.Foo == "" && args.Bar == "" { | |||
| p.Fail("you must provide one of --foo and --bar") | |||
| } | |||
| ``` | |||
| ```shell | |||
| ./example | |||
| usage: samples [--foo FOO] [--bar BAR] | |||
| error: you must provide one of --foo and --bar | |||
| ``` | |||
| ### Version strings | |||
| ```go | |||
| type args struct { | |||
| ... | |||
| } | |||
| func (args) Version() string { | |||
| return "someprogram 4.3.0" | |||
| } | |||
| func main() { | |||
| var args args | |||
| arg.MustParse(&args) | |||
| } | |||
| ``` | |||
| ```shell | |||
| $ ./example --version | |||
| someprogram 4.3.0 | |||
| ``` | |||
| ### Embedded structs | |||
| The fields of embedded structs are treated just like regular fields: | |||
| ```go | |||
| type DatabaseOptions struct { | |||
| Host string | |||
| Username string | |||
| Password string | |||
| } | |||
| type LogOptions struct { | |||
| LogFile string | |||
| Verbose bool | |||
| } | |||
| func main() { | |||
| var args struct { | |||
| DatabaseOptions | |||
| LogOptions | |||
| } | |||
| arg.MustParse(&args) | |||
| } | |||
| ``` | |||
| As usual, any field tagged with `arg:"-"` is ignored. | |||
| ### Custom parsing | |||
| You can implement your own argument parser by implementing `encoding.TextUnmarshaler`: | |||
| ```go | |||
| package main | |||
| import ( | |||
| "fmt" | |||
| "strings" | |||
| "github.com/alexflint/go-arg" | |||
| ) | |||
| // Accepts command line arguments of the form "head.tail" | |||
| type NameDotName struct { | |||
| Head, Tail string | |||
| } | |||
| func (n *NameDotName) UnmarshalText(b []byte) error { | |||
| s := string(b) | |||
| pos := strings.Index(s, ".") | |||
| if pos == -1 { | |||
| return fmt.Errorf("missing period in %s", s) | |||
| } | |||
| n.Head = s[:pos] | |||
| n.Tail = s[pos+1:] | |||
| return nil | |||
| } | |||
| func main() { | |||
| var args struct { | |||
| Name *NameDotName | |||
| } | |||
| arg.MustParse(&args) | |||
| fmt.Printf("%#v\n", args.Name) | |||
| } | |||
| ``` | |||
| ```shell | |||
| $ ./example --name=foo.bar | |||
| &main.NameDotName{Head:"foo", Tail:"bar"} | |||
| $ ./example --name=oops | |||
| usage: example [--name NAME] | |||
| error: error processing --name: missing period in "oops" | |||
| ``` | |||
| ### Documentation | |||
| https://godoc.org/github.com/alexflint/go-arg | |||
| ### Rationale | |||
| There are many command line argument parsing libraries for Go, including one in the standard library, so why build another? | |||
| The shortcomings of the `flag` library that ships in the standard library are well known. Positional arguments must preceed options, so `./prog x --foo=1` does what you expect but `./prog --foo=1 x` does not. Arguments cannot have both long (`--foo`) and short (`-f`) forms. | |||
| Many third-party argument parsing libraries are geared for writing sophisticated command line interfaces. The excellent `codegangsta/cli` is perfect for working with multiple sub-commands and nested flags, but is probably overkill for a simple script with a handful of flags. | |||
| The main idea behind `go-arg` is that Go already has an excellent way to describe data structures using Go structs, so there is no need to develop more levels of abstraction on top of this. Instead of one API to specify which arguments your program accepts, and then another API to get the values of those arguments, why not replace both with a single struct? | |||
| @ -0,0 +1,36 @@ | |||
| // Package arg parses command line arguments using the fields from a struct. | |||
| // | |||
| // For example, | |||
| // | |||
| // var args struct { | |||
| // Iter int | |||
| // Debug bool | |||
| // } | |||
| // arg.MustParse(&args) | |||
| // | |||
| // defines two command line arguments, which can be set using any of | |||
| // | |||
| // ./example --iter=1 --debug // debug is a boolean flag so its value is set to true | |||
| // ./example -iter 1 // debug defaults to its zero value (false) | |||
| // ./example --debug=true // iter defaults to its zero value (zero) | |||
| // | |||
| // The fastest way to see how to use go-arg is to read the examples below. | |||
| // | |||
| // Fields can be bool, string, any float type, or any signed or unsigned integer type. | |||
| // They can also be slices of any of the above, or slices of pointers to any of the above. | |||
| // | |||
| // Tags can be specified using the `arg` package name: | |||
| // | |||
| // var args struct { | |||
| // Input string `arg:"positional"` | |||
| // Log string `arg:"positional,required"` | |||
| // Debug bool `arg:"-d,help:turn on debug mode"` | |||
| // RealMode bool `arg:"--real" | |||
| // Wr io.Writer `arg:"-"` | |||
| // } | |||
| // | |||
| // The valid tag strings are `positional`, `required`, and `help`. Further, any tag string | |||
| // that starts with a single hyphen is the short form for an argument (e.g. `./example -d`), | |||
| // and any tag string that starts with two hyphens is the long form for the argument | |||
| // (instead of the field name). Fields can be excluded from processing with `arg:"-"`. | |||
| package arg | |||
| @ -0,0 +1,87 @@ | |||
| package arg | |||
| import ( | |||
| "fmt" | |||
| "os" | |||
| ) | |||
| // This example demonstrates basic usage | |||
| func Example() { | |||
| // These are the args you would pass in on the command line | |||
| os.Args = []string{"./example", "--foo=hello", "--bar"} | |||
| var args struct { | |||
| Foo string | |||
| Bar bool | |||
| } | |||
| MustParse(&args) | |||
| fmt.Println(args.Foo, args.Bar) | |||
| } | |||
| // This example demonstrates arguments that have default values | |||
| func Example_defaultValues() { | |||
| // These are the args you would pass in on the command line | |||
| os.Args = []string{"--help"} | |||
| var args struct { | |||
| Foo string | |||
| Bar bool | |||
| } | |||
| args.Foo = "default value" | |||
| MustParse(&args) | |||
| fmt.Println(args.Foo, args.Bar) | |||
| } | |||
| // This example demonstrates arguments that are required | |||
| func Example_requiredArguments() { | |||
| // These are the args you would pass in on the command line | |||
| os.Args = []string{"--foo=1", "--bar"} | |||
| var args struct { | |||
| Foo string `arg:"required"` | |||
| Bar bool | |||
| } | |||
| MustParse(&args) | |||
| } | |||
| // This example demonstrates positional arguments | |||
| func Example_positionalArguments() { | |||
| // These are the args you would pass in on the command line | |||
| os.Args = []string{"./example", "in", "out1", "out2", "out3"} | |||
| var args struct { | |||
| Input string `arg:"positional"` | |||
| Output []string `arg:"positional"` | |||
| } | |||
| MustParse(&args) | |||
| fmt.Println("Input:", args.Input) | |||
| fmt.Println("Output:", args.Output) | |||
| } | |||
| // This example demonstrates arguments that have multiple values | |||
| func Example_multipleValues() { | |||
| // The args you would pass in on the command line | |||
| os.Args = []string{"--help"} | |||
| var args struct { | |||
| Database string | |||
| IDs []int64 | |||
| } | |||
| MustParse(&args) | |||
| fmt.Printf("Fetching the following IDs from %s: %q", args.Database, args.IDs) | |||
| } | |||
| // This example shows the usage string generated by go-arg | |||
| func Example_usageString() { | |||
| // These are the args you would pass in on the command line | |||
| os.Args = []string{"--help"} | |||
| var args struct { | |||
| Input string `arg:"positional"` | |||
| Output []string `arg:"positional"` | |||
| Verbose bool `arg:"-v,help:verbosity level"` | |||
| Dataset string `arg:"help:dataset to use"` | |||
| Optimize int `arg:"-O,help:optimization level"` | |||
| } | |||
| MustParse(&args) | |||
| } | |||
| @ -0,0 +1,424 @@ | |||
| package arg | |||
| import ( | |||
| "errors" | |||
| "fmt" | |||
| "os" | |||
| "path/filepath" | |||
| "reflect" | |||
| "strings" | |||
| ) | |||
| // spec represents a command line option | |||
| type spec struct { | |||
| dest reflect.Value | |||
| long string | |||
| short string | |||
| multiple bool | |||
| required bool | |||
| positional bool | |||
| help string | |||
| env string | |||
| wasPresent bool | |||
| boolean bool | |||
| } | |||
| // ErrHelp indicates that -h or --help were provided | |||
| var ErrHelp = errors.New("help requested by user") | |||
| // ErrVersion indicates that --version was provided | |||
| var ErrVersion = errors.New("version requested by user") | |||
| // MustParse processes command line arguments and exits upon failure | |||
| func MustParse(dest ...interface{}) *Parser { | |||
| p, err := NewParser(Config{}, dest...) | |||
| if err != nil { | |||
| fmt.Println(err) | |||
| os.Exit(-1) | |||
| } | |||
| err = p.Parse(os.Args[1:]) | |||
| if err == ErrHelp { | |||
| p.WriteHelp(os.Stdout) | |||
| os.Exit(0) | |||
| } | |||
| if err == ErrVersion { | |||
| fmt.Println(p.version) | |||
| os.Exit(0) | |||
| } | |||
| if err != nil { | |||
| p.Fail(err.Error()) | |||
| } | |||
| return p | |||
| } | |||
| // Parse processes command line arguments and stores them in dest | |||
| func Parse(dest ...interface{}) error { | |||
| p, err := NewParser(Config{}, dest...) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| return p.Parse(os.Args[1:]) | |||
| } | |||
| // Config represents configuration options for an argument parser | |||
| type Config struct { | |||
| Program string // Program is the name of the program used in the help text | |||
| } | |||
| // Parser represents a set of command line options with destination values | |||
| type Parser struct { | |||
| spec []*spec | |||
| config Config | |||
| version string | |||
| } | |||
| // Versioned is the interface that the destination struct should implement to | |||
| // make a version string appear at the top of the help message. | |||
| type Versioned interface { | |||
| // Version returns the version string that will be printed on a line by itself | |||
| // at the top of the help message. | |||
| Version() string | |||
| } | |||
| // walkFields calls a function for each field of a struct, recursively expanding struct fields. | |||
| func walkFields(v reflect.Value, visit func(field reflect.StructField, val reflect.Value, owner reflect.Type) bool) { | |||
| t := v.Type() | |||
| for i := 0; i < t.NumField(); i++ { | |||
| field := t.Field(i) | |||
| val := v.Field(i) | |||
| expand := visit(field, val, t) | |||
| if expand && field.Type.Kind() == reflect.Struct { | |||
| walkFields(val, visit) | |||
| } | |||
| } | |||
| } | |||
| // NewParser constructs a parser from a list of destination structs | |||
| func NewParser(config Config, dests ...interface{}) (*Parser, error) { | |||
| p := Parser{ | |||
| config: config, | |||
| } | |||
| for _, dest := range dests { | |||
| if dest, ok := dest.(Versioned); ok { | |||
| p.version = dest.Version() | |||
| } | |||
| v := reflect.ValueOf(dest) | |||
| if v.Kind() != reflect.Ptr { | |||
| panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", v.Type())) | |||
| } | |||
| v = v.Elem() | |||
| if v.Kind() != reflect.Struct { | |||
| panic(fmt.Sprintf("%T is not a struct pointer", dest)) | |||
| } | |||
| var errs []string | |||
| walkFields(v, func(field reflect.StructField, val reflect.Value, t reflect.Type) bool { | |||
| // Check for the ignore switch in the tag | |||
| tag := field.Tag.Get("arg") | |||
| if tag == "-" { | |||
| return false | |||
| } | |||
| // If this is an embedded struct then recurse into its fields | |||
| if field.Anonymous && field.Type.Kind() == reflect.Struct { | |||
| return true | |||
| } | |||
| spec := spec{ | |||
| long: strings.ToLower(field.Name), | |||
| dest: val, | |||
| } | |||
| // Check whether this field is supported. It's good to do this here rather than | |||
| // wait until setScalar because it means that a program with invalid argument | |||
| // fields will always fail regardless of whether the arguments it received | |||
| // exercised those fields. | |||
| var parseable bool | |||
| parseable, spec.boolean, spec.multiple = canParse(field.Type) | |||
| if !parseable { | |||
| errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported", | |||
| t.Name(), field.Name, field.Type.String())) | |||
| return false | |||
| } | |||
| // Look at the tag | |||
| if tag != "" { | |||
| for _, key := range strings.Split(tag, ",") { | |||
| var value string | |||
| if pos := strings.Index(key, ":"); pos != -1 { | |||
| value = key[pos+1:] | |||
| key = key[:pos] | |||
| } | |||
| switch { | |||
| case strings.HasPrefix(key, "--"): | |||
| spec.long = key[2:] | |||
| case strings.HasPrefix(key, "-"): | |||
| if len(key) != 2 { | |||
| errs = append(errs, fmt.Sprintf("%s.%s: short arguments must be one character only", | |||
| t.Name(), field.Name)) | |||
| return false | |||
| } | |||
| spec.short = key[1:] | |||
| case key == "required": | |||
| spec.required = true | |||
| case key == "positional": | |||
| spec.positional = true | |||
| case key == "help": | |||
| spec.help = value | |||
| case key == "env": | |||
| // Use override name if provided | |||
| if value != "" { | |||
| spec.env = value | |||
| } else { | |||
| spec.env = strings.ToUpper(field.Name) | |||
| } | |||
| default: | |||
| errs = append(errs, fmt.Sprintf("unrecognized tag '%s' on field %s", key, tag)) | |||
| return false | |||
| } | |||
| } | |||
| } | |||
| p.spec = append(p.spec, &spec) | |||
| // if this was an embedded field then we already returned true up above | |||
| return false | |||
| }) | |||
| if len(errs) > 0 { | |||
| return nil, errors.New(strings.Join(errs, "\n")) | |||
| } | |||
| } | |||
| if p.config.Program == "" { | |||
| p.config.Program = "program" | |||
| if len(os.Args) > 0 { | |||
| p.config.Program = filepath.Base(os.Args[0]) | |||
| } | |||
| } | |||
| return &p, nil | |||
| } | |||
| // Parse processes the given command line option, storing the results in the field | |||
| // of the structs from which NewParser was constructed | |||
| func (p *Parser) Parse(args []string) error { | |||
| // If -h or --help were specified then print usage | |||
| for _, arg := range args { | |||
| if arg == "-h" || arg == "--help" { | |||
| return ErrHelp | |||
| } | |||
| if arg == "--version" { | |||
| return ErrVersion | |||
| } | |||
| if arg == "--" { | |||
| break | |||
| } | |||
| } | |||
| // Process all command line arguments | |||
| err := process(p.spec, args) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| // Validate | |||
| return validate(p.spec) | |||
| } | |||
| // process goes through arguments one-by-one, parses them, and assigns the result to | |||
| // the underlying struct field | |||
| func process(specs []*spec, args []string) error { | |||
| // construct a map from --option to spec | |||
| optionMap := make(map[string]*spec) | |||
| for _, spec := range specs { | |||
| if spec.positional { | |||
| continue | |||
| } | |||
| if spec.long != "" { | |||
| optionMap[spec.long] = spec | |||
| } | |||
| if spec.short != "" { | |||
| optionMap[spec.short] = spec | |||
| } | |||
| if spec.env != "" { | |||
| if value, found := os.LookupEnv(spec.env); found { | |||
| err := setScalar(spec.dest, value) | |||
| if err != nil { | |||
| return fmt.Errorf("error processing environment variable %s: %v", spec.env, err) | |||
| } | |||
| spec.wasPresent = true | |||
| } | |||
| } | |||
| } | |||
| // process each string from the command line | |||
| var allpositional bool | |||
| var positionals []string | |||
| // must use explicit for loop, not range, because we manipulate i inside the loop | |||
| for i := 0; i < len(args); i++ { | |||
| arg := args[i] | |||
| if arg == "--" { | |||
| allpositional = true | |||
| continue | |||
| } | |||
| if !strings.HasPrefix(arg, "-") || allpositional { | |||
| positionals = append(positionals, arg) | |||
| continue | |||
| } | |||
| // check for an equals sign, as in "--foo=bar" | |||
| var value string | |||
| opt := strings.TrimLeft(arg, "-") | |||
| if pos := strings.Index(opt, "="); pos != -1 { | |||
| value = opt[pos+1:] | |||
| opt = opt[:pos] | |||
| } | |||
| // lookup the spec for this option | |||
| spec, ok := optionMap[opt] | |||
| if !ok { | |||
| return fmt.Errorf("unknown argument %s", arg) | |||
| } | |||
| spec.wasPresent = true | |||
| // deal with the case of multiple values | |||
| if spec.multiple { | |||
| var values []string | |||
| if value == "" { | |||
| for i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { | |||
| values = append(values, args[i+1]) | |||
| i++ | |||
| } | |||
| } else { | |||
| values = append(values, value) | |||
| } | |||
| err := setSlice(spec.dest, values) | |||
| if err != nil { | |||
| return fmt.Errorf("error processing %s: %v", arg, err) | |||
| } | |||
| continue | |||
| } | |||
| // if it's a flag and it has no value then set the value to true | |||
| // use boolean because this takes account of TextUnmarshaler | |||
| if spec.boolean && value == "" { | |||
| value = "true" | |||
| } | |||
| // if we have something like "--foo" then the value is the next argument | |||
| if value == "" { | |||
| if i+1 == len(args) || strings.HasPrefix(args[i+1], "-") { | |||
| return fmt.Errorf("missing value for %s", arg) | |||
| } | |||
| value = args[i+1] | |||
| i++ | |||
| } | |||
| err := setScalar(spec.dest, value) | |||
| if err != nil { | |||
| return fmt.Errorf("error processing %s: %v", arg, err) | |||
| } | |||
| } | |||
| // process positionals | |||
| for _, spec := range specs { | |||
| if spec.positional { | |||
| if spec.multiple { | |||
| err := setSlice(spec.dest, positionals) | |||
| if err != nil { | |||
| return fmt.Errorf("error processing %s: %v", spec.long, err) | |||
| } | |||
| positionals = nil | |||
| } else if len(positionals) > 0 { | |||
| err := setScalar(spec.dest, positionals[0]) | |||
| if err != nil { | |||
| return fmt.Errorf("error processing %s: %v", spec.long, err) | |||
| } | |||
| positionals = positionals[1:] | |||
| } else if spec.required { | |||
| return fmt.Errorf("%s is required", spec.long) | |||
| } | |||
| } | |||
| } | |||
| if len(positionals) > 0 { | |||
| return fmt.Errorf("too many positional arguments at '%s'", positionals[0]) | |||
| } | |||
| return nil | |||
| } | |||
| // validate an argument spec after arguments have been parse | |||
| func validate(spec []*spec) error { | |||
| for _, arg := range spec { | |||
| if !arg.positional && arg.required && !arg.wasPresent { | |||
| return fmt.Errorf("--%s is required", arg.long) | |||
| } | |||
| } | |||
| return nil | |||
| } | |||
| // parse a value as the appropriate type and store it in the struct | |||
| func setSlice(dest reflect.Value, values []string) error { | |||
| if !dest.CanSet() { | |||
| return fmt.Errorf("field is not writable") | |||
| } | |||
| var ptr bool | |||
| elem := dest.Type().Elem() | |||
| if elem.Kind() == reflect.Ptr { | |||
| ptr = true | |||
| elem = elem.Elem() | |||
| } | |||
| // Truncate the dest slice in case default values exist | |||
| if !dest.IsNil() { | |||
| dest.SetLen(0) | |||
| } | |||
| for _, s := range values { | |||
| v := reflect.New(elem) | |||
| if err := setScalar(v.Elem(), s); err != nil { | |||
| return err | |||
| } | |||
| if !ptr { | |||
| v = v.Elem() | |||
| } | |||
| dest.Set(reflect.Append(dest, v)) | |||
| } | |||
| return nil | |||
| } | |||
| // canParse returns true if the type can be parsed from a string | |||
| func canParse(t reflect.Type) (parseable, boolean, multiple bool) { | |||
| parseable, boolean = isScalar(t) | |||
| if parseable { | |||
| return | |||
| } | |||
| // Look inside pointer types | |||
| if t.Kind() == reflect.Ptr { | |||
| t = t.Elem() | |||
| } | |||
| // Look inside slice types | |||
| if t.Kind() == reflect.Slice { | |||
| multiple = true | |||
| t = t.Elem() | |||
| } | |||
| parseable, boolean = isScalar(t) | |||
| if parseable { | |||
| return | |||
| } | |||
| // Look inside pointer types (again, in case of []*Type) | |||
| if t.Kind() == reflect.Ptr { | |||
| t = t.Elem() | |||
| } | |||
| parseable, boolean = isScalar(t) | |||
| if parseable { | |||
| return | |||
| } | |||
| return false, false, false | |||
| } | |||
| @ -0,0 +1,656 @@ | |||
| package arg | |||
| import ( | |||
| "net" | |||
| "net/mail" | |||
| "os" | |||
| "strings" | |||
| "testing" | |||
| "time" | |||
| "github.com/stretchr/testify/assert" | |||
| "github.com/stretchr/testify/require" | |||
| ) | |||
| func setenv(t *testing.T, name, val string) { | |||
| if err := os.Setenv(name, val); err != nil { | |||
| t.Error(err) | |||
| } | |||
| } | |||
| func parse(cmdline string, dest interface{}) error { | |||
| p, err := NewParser(Config{}, dest) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| var parts []string | |||
| if len(cmdline) > 0 { | |||
| parts = strings.Split(cmdline, " ") | |||
| } | |||
| return p.Parse(parts) | |||
| } | |||
| func TestString(t *testing.T) { | |||
| var args struct { | |||
| Foo string | |||
| } | |||
| err := parse("--foo bar", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, "bar", args.Foo) | |||
| } | |||
| func TestInt(t *testing.T) { | |||
| var args struct { | |||
| Foo int | |||
| } | |||
| err := parse("--foo 7", &args) | |||
| require.NoError(t, err) | |||
| assert.EqualValues(t, 7, args.Foo) | |||
| } | |||
| func TestUint(t *testing.T) { | |||
| var args struct { | |||
| Foo uint | |||
| } | |||
| err := parse("--foo 7", &args) | |||
| require.NoError(t, err) | |||
| assert.EqualValues(t, 7, args.Foo) | |||
| } | |||
| func TestFloat(t *testing.T) { | |||
| var args struct { | |||
| Foo float32 | |||
| } | |||
| err := parse("--foo 3.4", &args) | |||
| require.NoError(t, err) | |||
| assert.EqualValues(t, 3.4, args.Foo) | |||
| } | |||
| func TestDuration(t *testing.T) { | |||
| var args struct { | |||
| Foo time.Duration | |||
| } | |||
| err := parse("--foo 3ms", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, 3*time.Millisecond, args.Foo) | |||
| } | |||
| func TestInvalidDuration(t *testing.T) { | |||
| var args struct { | |||
| Foo time.Duration | |||
| } | |||
| err := parse("--foo xxx", &args) | |||
| require.Error(t, err) | |||
| } | |||
| func TestIntPtr(t *testing.T) { | |||
| var args struct { | |||
| Foo *int | |||
| } | |||
| err := parse("--foo 123", &args) | |||
| require.NoError(t, err) | |||
| require.NotNil(t, args.Foo) | |||
| assert.Equal(t, 123, *args.Foo) | |||
| } | |||
| func TestIntPtrNotPresent(t *testing.T) { | |||
| var args struct { | |||
| Foo *int | |||
| } | |||
| err := parse("", &args) | |||
| require.NoError(t, err) | |||
| assert.Nil(t, args.Foo) | |||
| } | |||
| func TestMixed(t *testing.T) { | |||
| var args struct { | |||
| Foo string `arg:"-f"` | |||
| Bar int | |||
| Baz uint `arg:"positional"` | |||
| Ham bool | |||
| Spam float32 | |||
| } | |||
| args.Bar = 3 | |||
| err := parse("123 -spam=1.2 -ham -f xyz", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, "xyz", args.Foo) | |||
| assert.Equal(t, 3, args.Bar) | |||
| assert.Equal(t, uint(123), args.Baz) | |||
| assert.Equal(t, true, args.Ham) | |||
| assert.EqualValues(t, 1.2, args.Spam) | |||
| } | |||
| func TestRequired(t *testing.T) { | |||
| var args struct { | |||
| Foo string `arg:"required"` | |||
| } | |||
| err := parse("", &args) | |||
| require.Error(t, err, "--foo is required") | |||
| } | |||
| func TestShortFlag(t *testing.T) { | |||
| var args struct { | |||
| Foo string `arg:"-f"` | |||
| } | |||
| err := parse("-f xyz", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, "xyz", args.Foo) | |||
| err = parse("-foo xyz", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, "xyz", args.Foo) | |||
| err = parse("--foo xyz", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, "xyz", args.Foo) | |||
| } | |||
| func TestInvalidShortFlag(t *testing.T) { | |||
| var args struct { | |||
| Foo string `arg:"-foo"` | |||
| } | |||
| err := parse("", &args) | |||
| assert.Error(t, err) | |||
| } | |||
| func TestLongFlag(t *testing.T) { | |||
| var args struct { | |||
| Foo string `arg:"--abc"` | |||
| } | |||
| err := parse("-abc xyz", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, "xyz", args.Foo) | |||
| err = parse("--abc xyz", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, "xyz", args.Foo) | |||
| } | |||
| func TestCaseSensitive(t *testing.T) { | |||
| var args struct { | |||
| Lower bool `arg:"-v"` | |||
| Upper bool `arg:"-V"` | |||
| } | |||
| err := parse("-v", &args) | |||
| require.NoError(t, err) | |||
| assert.True(t, args.Lower) | |||
| assert.False(t, args.Upper) | |||
| } | |||
| func TestCaseSensitive2(t *testing.T) { | |||
| var args struct { | |||
| Lower bool `arg:"-v"` | |||
| Upper bool `arg:"-V"` | |||
| } | |||
| err := parse("-V", &args) | |||
| require.NoError(t, err) | |||
| assert.False(t, args.Lower) | |||
| assert.True(t, args.Upper) | |||
| } | |||
| func TestPositional(t *testing.T) { | |||
| var args struct { | |||
| Input string `arg:"positional"` | |||
| Output string `arg:"positional"` | |||
| } | |||
| err := parse("foo", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, "foo", args.Input) | |||
| assert.Equal(t, "", args.Output) | |||
| } | |||
| func TestPositionalPointer(t *testing.T) { | |||
| var args struct { | |||
| Input string `arg:"positional"` | |||
| Output []*string `arg:"positional"` | |||
| } | |||
| err := parse("foo bar baz", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, "foo", args.Input) | |||
| bar := "bar" | |||
| baz := "baz" | |||
| assert.Equal(t, []*string{&bar, &baz}, args.Output) | |||
| } | |||
| func TestRequiredPositional(t *testing.T) { | |||
| var args struct { | |||
| Input string `arg:"positional"` | |||
| Output string `arg:"positional,required"` | |||
| } | |||
| err := parse("foo", &args) | |||
| assert.Error(t, err) | |||
| } | |||
| func TestTooManyPositional(t *testing.T) { | |||
| var args struct { | |||
| Input string `arg:"positional"` | |||
| Output string `arg:"positional"` | |||
| } | |||
| err := parse("foo bar baz", &args) | |||
| assert.Error(t, err) | |||
| } | |||
| func TestMultiple(t *testing.T) { | |||
| var args struct { | |||
| Foo []int | |||
| Bar []string | |||
| } | |||
| err := parse("--foo 1 2 3 --bar x y z", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, []int{1, 2, 3}, args.Foo) | |||
| assert.Equal(t, []string{"x", "y", "z"}, args.Bar) | |||
| } | |||
| func TestMultipleWithEq(t *testing.T) { | |||
| var args struct { | |||
| Foo []int | |||
| Bar []string | |||
| } | |||
| err := parse("--foo 1 2 3 --bar=x", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, []int{1, 2, 3}, args.Foo) | |||
| assert.Equal(t, []string{"x"}, args.Bar) | |||
| } | |||
| func TestMultipleWithDefault(t *testing.T) { | |||
| var args struct { | |||
| Foo []int | |||
| Bar []string | |||
| } | |||
| args.Foo = []int{42} | |||
| args.Bar = []string{"foo"} | |||
| err := parse("--foo 1 2 3 --bar x y z", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, []int{1, 2, 3}, args.Foo) | |||
| assert.Equal(t, []string{"x", "y", "z"}, args.Bar) | |||
| } | |||
| func TestExemptField(t *testing.T) { | |||
| var args struct { | |||
| Foo string | |||
| Bar interface{} `arg:"-"` | |||
| } | |||
| err := parse("--foo xyz", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, "xyz", args.Foo) | |||
| } | |||
| func TestUnknownField(t *testing.T) { | |||
| var args struct { | |||
| Foo string | |||
| } | |||
| err := parse("--bar xyz", &args) | |||
| assert.Error(t, err) | |||
| } | |||
| func TestMissingRequired(t *testing.T) { | |||
| var args struct { | |||
| Foo string `arg:"required"` | |||
| X []string `arg:"positional"` | |||
| } | |||
| err := parse("x", &args) | |||
| assert.Error(t, err) | |||
| } | |||
| func TestMissingValue(t *testing.T) { | |||
| var args struct { | |||
| Foo string | |||
| } | |||
| err := parse("--foo", &args) | |||
| assert.Error(t, err) | |||
| } | |||
| func TestInvalidInt(t *testing.T) { | |||
| var args struct { | |||
| Foo int | |||
| } | |||
| err := parse("--foo=xyz", &args) | |||
| assert.Error(t, err) | |||
| } | |||
| func TestInvalidUint(t *testing.T) { | |||
| var args struct { | |||
| Foo uint | |||
| } | |||
| err := parse("--foo=xyz", &args) | |||
| assert.Error(t, err) | |||
| } | |||
| func TestInvalidFloat(t *testing.T) { | |||
| var args struct { | |||
| Foo float64 | |||
| } | |||
| err := parse("--foo xyz", &args) | |||
| require.Error(t, err) | |||
| } | |||
| func TestInvalidBool(t *testing.T) { | |||
| var args struct { | |||
| Foo bool | |||
| } | |||
| err := parse("--foo=xyz", &args) | |||
| require.Error(t, err) | |||
| } | |||
| func TestInvalidIntSlice(t *testing.T) { | |||
| var args struct { | |||
| Foo []int | |||
| } | |||
| err := parse("--foo 1 2 xyz", &args) | |||
| require.Error(t, err) | |||
| } | |||
| func TestInvalidPositional(t *testing.T) { | |||
| var args struct { | |||
| Foo int `arg:"positional"` | |||
| } | |||
| err := parse("xyz", &args) | |||
| require.Error(t, err) | |||
| } | |||
| func TestInvalidPositionalSlice(t *testing.T) { | |||
| var args struct { | |||
| Foo []int `arg:"positional"` | |||
| } | |||
| err := parse("1 2 xyz", &args) | |||
| require.Error(t, err) | |||
| } | |||
| func TestNoMoreOptions(t *testing.T) { | |||
| var args struct { | |||
| Foo string | |||
| Bar []string `arg:"positional"` | |||
| } | |||
| err := parse("abc -- --foo xyz", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, "", args.Foo) | |||
| assert.Equal(t, []string{"abc", "--foo", "xyz"}, args.Bar) | |||
| } | |||
| func TestHelpFlag(t *testing.T) { | |||
| var args struct { | |||
| Foo string | |||
| Bar interface{} `arg:"-"` | |||
| } | |||
| err := parse("--help", &args) | |||
| assert.Equal(t, ErrHelp, err) | |||
| } | |||
| func TestPanicOnNonPointer(t *testing.T) { | |||
| var args struct{} | |||
| assert.Panics(t, func() { | |||
| parse("", args) | |||
| }) | |||
| } | |||
| func TestPanicOnNonStruct(t *testing.T) { | |||
| var args string | |||
| assert.Panics(t, func() { | |||
| parse("", &args) | |||
| }) | |||
| } | |||
| func TestUnsupportedType(t *testing.T) { | |||
| var args struct { | |||
| Foo interface{} | |||
| } | |||
| err := parse("--foo", &args) | |||
| assert.Error(t, err) | |||
| } | |||
| func TestUnsupportedSliceElement(t *testing.T) { | |||
| var args struct { | |||
| Foo []interface{} | |||
| } | |||
| err := parse("--foo 3", &args) | |||
| assert.Error(t, err) | |||
| } | |||
| func TestUnsupportedSliceElementMissingValue(t *testing.T) { | |||
| var args struct { | |||
| Foo []interface{} | |||
| } | |||
| err := parse("--foo", &args) | |||
| assert.Error(t, err) | |||
| } | |||
| func TestUnknownTag(t *testing.T) { | |||
| var args struct { | |||
| Foo string `arg:"this_is_not_valid"` | |||
| } | |||
| err := parse("--foo xyz", &args) | |||
| assert.Error(t, err) | |||
| } | |||
| func TestParse(t *testing.T) { | |||
| var args struct { | |||
| Foo string | |||
| } | |||
| os.Args = []string{"example", "--foo", "bar"} | |||
| err := Parse(&args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, "bar", args.Foo) | |||
| } | |||
| func TestParseError(t *testing.T) { | |||
| var args struct { | |||
| Foo string `arg:"this_is_not_valid"` | |||
| } | |||
| os.Args = []string{"example", "--bar"} | |||
| err := Parse(&args) | |||
| assert.Error(t, err) | |||
| } | |||
| func TestMustParse(t *testing.T) { | |||
| var args struct { | |||
| Foo string | |||
| } | |||
| os.Args = []string{"example", "--foo", "bar"} | |||
| parser := MustParse(&args) | |||
| assert.Equal(t, "bar", args.Foo) | |||
| assert.NotNil(t, parser) | |||
| } | |||
| func TestEnvironmentVariable(t *testing.T) { | |||
| var args struct { | |||
| Foo string `arg:"env"` | |||
| } | |||
| setenv(t, "FOO", "bar") | |||
| os.Args = []string{"example"} | |||
| MustParse(&args) | |||
| assert.Equal(t, "bar", args.Foo) | |||
| } | |||
| func TestEnvironmentVariableOverrideName(t *testing.T) { | |||
| var args struct { | |||
| Foo string `arg:"env:BAZ"` | |||
| } | |||
| setenv(t, "BAZ", "bar") | |||
| os.Args = []string{"example"} | |||
| MustParse(&args) | |||
| assert.Equal(t, "bar", args.Foo) | |||
| } | |||
| func TestEnvironmentVariableOverrideArgument(t *testing.T) { | |||
| var args struct { | |||
| Foo string `arg:"env"` | |||
| } | |||
| setenv(t, "FOO", "bar") | |||
| os.Args = []string{"example", "--foo", "baz"} | |||
| MustParse(&args) | |||
| assert.Equal(t, "baz", args.Foo) | |||
| } | |||
| func TestEnvironmentVariableError(t *testing.T) { | |||
| var args struct { | |||
| Foo int `arg:"env"` | |||
| } | |||
| setenv(t, "FOO", "bar") | |||
| os.Args = []string{"example"} | |||
| err := Parse(&args) | |||
| assert.Error(t, err) | |||
| } | |||
| func TestEnvironmentVariableRequired(t *testing.T) { | |||
| var args struct { | |||
| Foo string `arg:"env,required"` | |||
| } | |||
| setenv(t, "FOO", "bar") | |||
| os.Args = []string{"example"} | |||
| MustParse(&args) | |||
| assert.Equal(t, "bar", args.Foo) | |||
| } | |||
| type textUnmarshaler struct { | |||
| val int | |||
| } | |||
| func (f *textUnmarshaler) UnmarshalText(b []byte) error { | |||
| f.val = len(b) | |||
| return nil | |||
| } | |||
| func TestTextUnmarshaler(t *testing.T) { | |||
| // fields that implement TextUnmarshaler should be parsed using that interface | |||
| var args struct { | |||
| Foo *textUnmarshaler | |||
| } | |||
| err := parse("--foo abc", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, 3, args.Foo.val) | |||
| } | |||
| type boolUnmarshaler bool | |||
| func (p *boolUnmarshaler) UnmarshalText(b []byte) error { | |||
| *p = len(b)%2 == 0 | |||
| return nil | |||
| } | |||
| func TestBoolUnmarhsaler(t *testing.T) { | |||
| // test that a bool type that implements TextUnmarshaler is | |||
| // handled as a TextUnmarshaler not as a bool | |||
| var args struct { | |||
| Foo *boolUnmarshaler | |||
| } | |||
| err := parse("--foo ab", &args) | |||
| require.NoError(t, err) | |||
| assert.EqualValues(t, true, *args.Foo) | |||
| } | |||
| type sliceUnmarshaler []int | |||
| func (p *sliceUnmarshaler) UnmarshalText(b []byte) error { | |||
| *p = sliceUnmarshaler{len(b)} | |||
| return nil | |||
| } | |||
| func TestSliceUnmarhsaler(t *testing.T) { | |||
| // test that a slice type that implements TextUnmarshaler is | |||
| // handled as a TextUnmarshaler not as a slice | |||
| var args struct { | |||
| Foo *sliceUnmarshaler | |||
| Bar string `arg:"positional"` | |||
| } | |||
| err := parse("--foo abcde xyz", &args) | |||
| require.NoError(t, err) | |||
| require.Len(t, *args.Foo, 1) | |||
| assert.EqualValues(t, 5, (*args.Foo)[0]) | |||
| assert.Equal(t, "xyz", args.Bar) | |||
| } | |||
| func TestIP(t *testing.T) { | |||
| var args struct { | |||
| Host net.IP | |||
| } | |||
| err := parse("--host 192.168.0.1", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, "192.168.0.1", args.Host.String()) | |||
| } | |||
| func TestPtrToIP(t *testing.T) { | |||
| var args struct { | |||
| Host *net.IP | |||
| } | |||
| err := parse("--host 192.168.0.1", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, "192.168.0.1", args.Host.String()) | |||
| } | |||
| func TestIPSlice(t *testing.T) { | |||
| var args struct { | |||
| Host []net.IP | |||
| } | |||
| err := parse("--host 192.168.0.1 127.0.0.1", &args) | |||
| require.NoError(t, err) | |||
| require.Len(t, args.Host, 2) | |||
| assert.Equal(t, "192.168.0.1", args.Host[0].String()) | |||
| assert.Equal(t, "127.0.0.1", args.Host[1].String()) | |||
| } | |||
| func TestInvalidIPAddress(t *testing.T) { | |||
| var args struct { | |||
| Host net.IP | |||
| } | |||
| err := parse("--host xxx", &args) | |||
| assert.Error(t, err) | |||
| } | |||
| func TestMAC(t *testing.T) { | |||
| var args struct { | |||
| Host net.HardwareAddr | |||
| } | |||
| err := parse("--host 0123.4567.89ab", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, "01:23:45:67:89:ab", args.Host.String()) | |||
| } | |||
| func TestInvalidMac(t *testing.T) { | |||
| var args struct { | |||
| Host net.HardwareAddr | |||
| } | |||
| err := parse("--host xxx", &args) | |||
| assert.Error(t, err) | |||
| } | |||
| func TestMailAddr(t *testing.T) { | |||
| var args struct { | |||
| Recipient mail.Address | |||
| } | |||
| err := parse("--recipient foo@example.com", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, "<foo@example.com>", args.Recipient.String()) | |||
| } | |||
| func TestInvalidMailAddr(t *testing.T) { | |||
| var args struct { | |||
| Recipient mail.Address | |||
| } | |||
| err := parse("--recipient xxx", &args) | |||
| assert.Error(t, err) | |||
| } | |||
| type A struct { | |||
| X string | |||
| } | |||
| type B struct { | |||
| Y int | |||
| } | |||
| func TestEmbedded(t *testing.T) { | |||
| var args struct { | |||
| A | |||
| B | |||
| Z bool | |||
| } | |||
| err := parse("--x=hello --y=321 --z", &args) | |||
| require.NoError(t, err) | |||
| assert.Equal(t, "hello", args.X) | |||
| assert.Equal(t, 321, args.Y) | |||
| assert.Equal(t, true, args.Z) | |||
| } | |||
| @ -0,0 +1,145 @@ | |||
| package arg | |||
| import ( | |||
| "encoding" | |||
| "fmt" | |||
| "net" | |||
| "net/mail" | |||
| "reflect" | |||
| "strconv" | |||
| "time" | |||
| ) | |||
| // The reflected form of some special types | |||
| var ( | |||
| textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem() | |||
| durationType = reflect.TypeOf(time.Duration(0)) | |||
| mailAddressType = reflect.TypeOf(mail.Address{}) | |||
| ipType = reflect.TypeOf(net.IP{}) | |||
| macType = reflect.TypeOf(net.HardwareAddr{}) | |||
| ) | |||
| // isScalar returns true if the type can be parsed from a single string | |||
| func isScalar(t reflect.Type) (scalar, boolean bool) { | |||
| // If it implements encoding.TextUnmarshaler then use that | |||
| if t.Implements(textUnmarshalerType) { | |||
| // scalar=YES, boolean=NO | |||
| return true, false | |||
| } | |||
| // If we have a pointer then dereference it | |||
| if t.Kind() == reflect.Ptr { | |||
| t = t.Elem() | |||
| } | |||
| // Check for other special types | |||
| switch t { | |||
| case durationType, mailAddressType, ipType, macType: | |||
| // scalar=YES, boolean=NO | |||
| return true, false | |||
| } | |||
| // Fall back to checking the kind | |||
| switch t.Kind() { | |||
| case reflect.Bool: | |||
| // scalar=YES, boolean=YES | |||
| return true, true | |||
| case reflect.String, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, | |||
| reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, | |||
| reflect.Float32, reflect.Float64: | |||
| // scalar=YES, boolean=NO | |||
| return true, false | |||
| } | |||
| // scalar=NO, boolean=NO | |||
| return false, false | |||
| } | |||
| // set a value from a string | |||
| func setScalar(v reflect.Value, s string) error { | |||
| if !v.CanSet() { | |||
| return fmt.Errorf("field is not exported") | |||
| } | |||
| // If we have a nil pointer then allocate a new object | |||
| if v.Kind() == reflect.Ptr && v.IsNil() { | |||
| v.Set(reflect.New(v.Type().Elem())) | |||
| } | |||
| // Get the object as an interface | |||
| scalar := v.Interface() | |||
| // If it implements encoding.TextUnmarshaler then use that | |||
| if scalar, ok := scalar.(encoding.TextUnmarshaler); ok { | |||
| return scalar.UnmarshalText([]byte(s)) | |||
| } | |||
| // If we have a pointer then dereference it | |||
| if v.Kind() == reflect.Ptr { | |||
| v = v.Elem() | |||
| } | |||
| // Switch on concrete type | |||
| switch scalar.(type) { | |||
| case time.Duration: | |||
| duration, err := time.ParseDuration(s) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| v.Set(reflect.ValueOf(duration)) | |||
| return nil | |||
| case mail.Address: | |||
| addr, err := mail.ParseAddress(s) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| v.Set(reflect.ValueOf(*addr)) | |||
| return nil | |||
| case net.IP: | |||
| ip := net.ParseIP(s) | |||
| if ip == nil { | |||
| return fmt.Errorf(`invalid IP address: "%s"`, s) | |||
| } | |||
| v.Set(reflect.ValueOf(ip)) | |||
| return nil | |||
| case net.HardwareAddr: | |||
| ip, err := net.ParseMAC(s) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| v.Set(reflect.ValueOf(ip)) | |||
| return nil | |||
| } | |||
| // Switch on kind so that we can handle derived types | |||
| switch v.Kind() { | |||
| case reflect.String: | |||
| v.SetString(s) | |||
| case reflect.Bool: | |||
| x, err := strconv.ParseBool(s) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| v.SetBool(x) | |||
| case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | |||
| x, err := strconv.ParseInt(s, 10, v.Type().Bits()) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| v.SetInt(x) | |||
| case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: | |||
| x, err := strconv.ParseUint(s, 10, v.Type().Bits()) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| v.SetUint(x) | |||
| case reflect.Float32, reflect.Float64: | |||
| x, err := strconv.ParseFloat(s, v.Type().Bits()) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| v.SetFloat(x) | |||
| default: | |||
| return fmt.Errorf("cannot parse argument into %s", v.Type().String()) | |||
| } | |||
| return nil | |||
| } | |||
| @ -0,0 +1,139 @@ | |||
| package arg | |||
| import ( | |||
| "fmt" | |||
| "io" | |||
| "os" | |||
| "reflect" | |||
| "strings" | |||
| ) | |||
| // the width of the left column | |||
| const colWidth = 25 | |||
| // Fail prints usage information to stderr and exits with non-zero status | |||
| func (p *Parser) Fail(msg string) { | |||
| p.WriteUsage(os.Stderr) | |||
| fmt.Fprintln(os.Stderr, "error:", msg) | |||
| os.Exit(-1) | |||
| } | |||
| // WriteUsage writes usage information to the given writer | |||
| func (p *Parser) WriteUsage(w io.Writer) { | |||
| var positionals, options []*spec | |||
| for _, spec := range p.spec { | |||
| if spec.positional { | |||
| positionals = append(positionals, spec) | |||
| } else { | |||
| options = append(options, spec) | |||
| } | |||
| } | |||
| if p.version != "" { | |||
| fmt.Fprintln(w, p.version) | |||
| } | |||
| fmt.Fprintf(w, "usage: %s", p.config.Program) | |||
| // write the option component of the usage message | |||
| for _, spec := range options { | |||
| // prefix with a space | |||
| fmt.Fprint(w, " ") | |||
| if !spec.required { | |||
| fmt.Fprint(w, "[") | |||
| } | |||
| fmt.Fprint(w, synopsis(spec, "--"+spec.long)) | |||
| if !spec.required { | |||
| fmt.Fprint(w, "]") | |||
| } | |||
| } | |||
| // write the positional component of the usage message | |||
| for _, spec := range positionals { | |||
| // prefix with a space | |||
| fmt.Fprint(w, " ") | |||
| up := strings.ToUpper(spec.long) | |||
| if spec.multiple { | |||
| fmt.Fprintf(w, "[%s [%s ...]]", up, up) | |||
| } else { | |||
| fmt.Fprint(w, up) | |||
| } | |||
| } | |||
| fmt.Fprint(w, "\n") | |||
| } | |||
| // WriteHelp writes the usage string followed by the full help string for each option | |||
| func (p *Parser) WriteHelp(w io.Writer) { | |||
| var positionals, options []*spec | |||
| for _, spec := range p.spec { | |||
| if spec.positional { | |||
| positionals = append(positionals, spec) | |||
| } else { | |||
| options = append(options, spec) | |||
| } | |||
| } | |||
| p.WriteUsage(w) | |||
| // write the list of positionals | |||
| if len(positionals) > 0 { | |||
| fmt.Fprint(w, "\npositional arguments:\n") | |||
| for _, spec := range positionals { | |||
| left := " " + spec.long | |||
| fmt.Fprint(w, left) | |||
| if spec.help != "" { | |||
| if len(left)+2 < colWidth { | |||
| fmt.Fprint(w, strings.Repeat(" ", colWidth-len(left))) | |||
| } else { | |||
| fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth)) | |||
| } | |||
| fmt.Fprint(w, spec.help) | |||
| } | |||
| fmt.Fprint(w, "\n") | |||
| } | |||
| } | |||
| // write the list of options | |||
| fmt.Fprint(w, "\noptions:\n") | |||
| for _, spec := range options { | |||
| printOption(w, spec) | |||
| } | |||
| // write the list of built in options | |||
| printOption(w, &spec{boolean: true, long: "help", short: "h", help: "display this help and exit"}) | |||
| if p.version != "" { | |||
| printOption(w, &spec{boolean: true, long: "version", help: "display version and exit"}) | |||
| } | |||
| } | |||
| func printOption(w io.Writer, spec *spec) { | |||
| left := " " + synopsis(spec, "--"+spec.long) | |||
| if spec.short != "" { | |||
| left += ", " + synopsis(spec, "-"+spec.short) | |||
| } | |||
| fmt.Fprint(w, left) | |||
| if spec.help != "" { | |||
| if len(left)+2 < colWidth { | |||
| fmt.Fprint(w, strings.Repeat(" ", colWidth-len(left))) | |||
| } else { | |||
| fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth)) | |||
| } | |||
| fmt.Fprint(w, spec.help) | |||
| } | |||
| // If spec.dest is not the zero value then a default value has been added. | |||
| v := spec.dest | |||
| if v.IsValid() { | |||
| z := reflect.Zero(v.Type()) | |||
| if (v.Type().Comparable() && z.Type().Comparable() && v.Interface() != z.Interface()) || v.Kind() == reflect.Slice && !v.IsNil() { | |||
| fmt.Fprintf(w, " [default: %v]", v) | |||
| } | |||
| } | |||
| fmt.Fprint(w, "\n") | |||
| } | |||
| func synopsis(spec *spec, form string) string { | |||
| if spec.boolean { | |||
| return form | |||
| } | |||
| return form + " " + strings.ToUpper(spec.long) | |||
| } | |||
| @ -0,0 +1,131 @@ | |||
| package arg | |||
| import ( | |||
| "bytes" | |||
| "os" | |||
| "testing" | |||
| "github.com/stretchr/testify/assert" | |||
| "github.com/stretchr/testify/require" | |||
| ) | |||
| func TestWriteUsage(t *testing.T) { | |||
| expectedUsage := "usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] INPUT [OUTPUT [OUTPUT ...]]\n" | |||
| expectedHelp := `usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] INPUT [OUTPUT [OUTPUT ...]] | |||
| positional arguments: | |||
| input | |||
| output list of outputs | |||
| options: | |||
| --name NAME name to use [default: Foo Bar] | |||
| --value VALUE secret value [default: 42] | |||
| --verbose, -v verbosity level | |||
| --dataset DATASET dataset to use | |||
| --optimize OPTIMIZE, -O OPTIMIZE | |||
| optimization level | |||
| --ids IDS Ids | |||
| --values VALUES Values [default: [3.14 42 256]] | |||
| --workers WORKERS, -w WORKERS | |||
| number of workers to start | |||
| --help, -h display this help and exit | |||
| ` | |||
| var args struct { | |||
| Input string `arg:"positional"` | |||
| Output []string `arg:"positional,help:list of outputs"` | |||
| Name string `arg:"help:name to use"` | |||
| Value int `arg:"help:secret value"` | |||
| Verbose bool `arg:"-v,help:verbosity level"` | |||
| Dataset string `arg:"help:dataset to use"` | |||
| Optimize int `arg:"-O,help:optimization level"` | |||
| Ids []int64 `arg:"help:Ids"` | |||
| Values []float64 `arg:"help:Values"` | |||
| Workers int `arg:"-w,env:WORKERS,help:number of workers to start"` | |||
| } | |||
| args.Name = "Foo Bar" | |||
| args.Value = 42 | |||
| args.Values = []float64{3.14, 42, 256} | |||
| p, err := NewParser(Config{}, &args) | |||
| require.NoError(t, err) | |||
| os.Args[0] = "example" | |||
| var usage bytes.Buffer | |||
| p.WriteUsage(&usage) | |||
| assert.Equal(t, expectedUsage, usage.String()) | |||
| var help bytes.Buffer | |||
| p.WriteHelp(&help) | |||
| assert.Equal(t, expectedHelp, help.String()) | |||
| } | |||
| func TestUsageLongPositionalWithHelp(t *testing.T) { | |||
| expectedHelp := `usage: example VERYLONGPOSITIONALWITHHELP | |||
| positional arguments: | |||
| verylongpositionalwithhelp | |||
| this positional argument is very long | |||
| options: | |||
| --help, -h display this help and exit | |||
| ` | |||
| var args struct { | |||
| VeryLongPositionalWithHelp string `arg:"positional,help:this positional argument is very long"` | |||
| } | |||
| p, err := NewParser(Config{}, &args) | |||
| require.NoError(t, err) | |||
| os.Args[0] = "example" | |||
| var help bytes.Buffer | |||
| p.WriteHelp(&help) | |||
| assert.Equal(t, expectedHelp, help.String()) | |||
| } | |||
| func TestUsageWithProgramName(t *testing.T) { | |||
| expectedHelp := `usage: myprogram | |||
| options: | |||
| --help, -h display this help and exit | |||
| ` | |||
| config := Config{ | |||
| Program: "myprogram", | |||
| } | |||
| p, err := NewParser(config, &struct{}{}) | |||
| require.NoError(t, err) | |||
| os.Args[0] = "example" | |||
| var help bytes.Buffer | |||
| p.WriteHelp(&help) | |||
| assert.Equal(t, expectedHelp, help.String()) | |||
| } | |||
| type versioned struct{} | |||
| // Version returns the version for this program | |||
| func (versioned) Version() string { | |||
| return "example 3.2.1" | |||
| } | |||
| func TestUsageWithVersion(t *testing.T) { | |||
| expectedHelp := `example 3.2.1 | |||
| usage: example | |||
| options: | |||
| --help, -h display this help and exit | |||
| --version display version and exit | |||
| ` | |||
| os.Args[0] = "example" | |||
| p, err := NewParser(Config{}, &versioned{}) | |||
| require.NoError(t, err) | |||
| var help bytes.Buffer | |||
| p.WriteHelp(&help) | |||
| actual := help.String() | |||
| t.Logf("Expected:\n%s", expectedHelp) | |||
| t.Logf("Actual:\n%s", actual) | |||
| if expectedHelp != actual { | |||
| t.Fail() | |||
| } | |||
| } | |||