commit ce7ff1d7bf7b90d8d62cda1cbbd0f1cb63b2bce5 Author: brettlangdon Date: Sat Jan 23 08:03:08 2016 -0500 Squashed 'vendor/github.com/alexflint/go-arg/' content from commit e560d07 git-subtree-dir: vendor/github.com/alexflint/go-arg git-subtree-split: e560d079baf3881884b897c4cc5850675aad7c15 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..daf913b --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d4e2c01 --- /dev/null +++ b/.travis.yml @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a50c494 --- /dev/null +++ b/LICENSE @@ -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. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d1d12f --- /dev/null +++ b/README.md @@ -0,0 +1,182 @@ +[![GoDoc](https://godoc.org/github.com/alexflint/go-arg?status.svg)](https://godoc.org/github.com/alexflint/go-arg) +[![Build Status](https://travis-ci.org/alexflint/go-arg.svg?branch=master)](https://travis-ci.org/alexflint/go-arg) +[![Coverage Status](https://coveralls.io/repos/alexflint/go-arg/badge.svg?branch=master&service=github)](https://coveralls.io/github/alexflint/go-arg?branch=master) + +## Structured argument parsing for Go + +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 { + Foo string `arg:"required"` + Bar bool +} +arg.MustParse(&args) +``` + +```shell +$ ./example +usage: example --foo FOO [--bar] +error: --foo 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 +``` + +### Installation + +```shell +go get github.com/alexflint/go-arg +``` + +### 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? diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..500ec83 --- /dev/null +++ b/doc.go @@ -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 diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..bdba6ac --- /dev/null +++ b/example_test.go @@ -0,0 +1,87 @@ +package arg + +import ( + "fmt" + "os" +) + +// This example demonstrates basic usage +func Example_Basic() { + // 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) +} diff --git a/parse.go b/parse.go new file mode 100644 index 0000000..ce3892f --- /dev/null +++ b/parse.go @@ -0,0 +1,370 @@ +package arg + +import ( + "errors" + "fmt" + "os" + "reflect" + "strconv" + "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 + isBool bool +} + +// ErrHelp indicates that -h or --help were provided +var ErrHelp = errors.New("help requested by user") + +// MustParse processes command line arguments and exits upon failure +func MustParse(dest ...interface{}) *Parser { + p, err := NewParser(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 != 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(dest...) + if err != nil { + return err + } + return p.Parse(os.Args[1:]) +} + +// Parser represents a set of command line options with destination values +type Parser struct { + spec []*spec +} + +// NewParser constructs a parser from a list of destination structs +func NewParser(dests ...interface{}) (*Parser, error) { + var specs []*spec + for _, dest := range dests { + 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)) + } + + t := v.Type() + for i := 0; i < t.NumField(); i++ { + // Check for the ignore switch in the tag + field := t.Field(i) + tag := field.Tag.Get("arg") + if tag == "-" { + continue + } + + spec := spec{ + long: strings.ToLower(field.Name), + dest: v.Field(i), + } + + // Get the scalar type for this field + scalarType := field.Type + if scalarType.Kind() == reflect.Slice { + spec.multiple = true + scalarType = scalarType.Elem() + if scalarType.Kind() == reflect.Ptr { + scalarType = scalarType.Elem() + } + } + + // Check for unsupported types + switch scalarType.Kind() { + case reflect.Array, reflect.Chan, reflect.Func, reflect.Interface, + reflect.Map, reflect.Ptr, reflect.Struct, + reflect.Complex64, reflect.Complex128: + return nil, fmt.Errorf("%s.%s: %s fields are not supported", t.Name(), field.Name, scalarType.Kind()) + } + + // Specify that it is a bool for usage + if scalarType.Kind() == reflect.Bool { + spec.isBool = true + } + + // 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 { + return nil, fmt.Errorf("%s.%s: short arguments must be one character only", t.Name(), field.Name) + } + 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: + return nil, fmt.Errorf("unrecognized tag '%s' on field %s", key, tag) + } + } + } + specs = append(specs, &spec) + } + } + return &Parser{spec: specs}, 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 == "--" { + 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 + if spec.dest.Kind() == reflect.Bool && 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 apropriate 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() + } + + 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 +} + +// set a value from a string +func setScalar(v reflect.Value, s string) error { + if !v.CanSet() { + return fmt.Errorf("field is not exported") + } + + switch v.Kind() { + case reflect.String: + v.Set(reflect.ValueOf(s)) + case reflect.Bool: + x, err := strconv.ParseBool(s) + if err != nil { + return err + } + v.Set(reflect.ValueOf(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.Set(reflect.ValueOf(x).Convert(v.Type())) + 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.Set(reflect.ValueOf(x).Convert(v.Type())) + case reflect.Float32, reflect.Float64: + x, err := strconv.ParseFloat(s, v.Type().Bits()) + if err != nil { + return err + } + v.Set(reflect.ValueOf(x).Convert(v.Type())) + default: + return fmt.Errorf("not a scalar type: %s", v.Kind()) + } + return nil +} diff --git a/parse_test.go b/parse_test.go new file mode 100644 index 0000000..f3e7350 --- /dev/null +++ b/parse_test.go @@ -0,0 +1,409 @@ +package arg + +import ( + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func parse(cmdline string, dest interface{}) error { + p, err := NewParser(dest) + if err != nil { + return err + } + return p.Parse(strings.Split(cmdline, " ")) +} + +func TestStringSingle(t *testing.T) { + var args struct { + Foo string + } + err := parse("--foo bar", &args) + require.NoError(t, err) + assert.Equal(t, "bar", 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 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", &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"` + } + os.Setenv("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"` + } + os.Setenv("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"` + } + os.Setenv("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"` + } + os.Setenv("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"` + } + os.Setenv("FOO", "bar") + os.Args = []string{"example"} + MustParse(&args) + assert.Equal(t, "bar", args.Foo) +} diff --git a/usage.go b/usage.go new file mode 100644 index 0000000..61f0ad6 --- /dev/null +++ b/usage.go @@ -0,0 +1,134 @@ +package arg + +import ( + "fmt" + "io" + "os" + "path/filepath" + "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) + } + } + + fmt.Fprintf(w, "usage: %s", filepath.Base(os.Args[0])) + + // 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{isBool: true, long: "help", short: "h", help: "display this help 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) + } + // Check if spec.dest is zero value or not + // If it isn't a default value have been added + v := spec.dest + if v.IsValid() { + z := reflect.Zero(v.Type()) + if v.Type().Comparable() && z.Type().Comparable() && v.Interface() != z.Interface() { + fmt.Fprintf(w, " [default: %v]", v) + } + } + fmt.Fprint(w, "\n") +} + +func synopsis(spec *spec, form string) string { + if spec.isBool { + return form + } + return form + " " + strings.ToUpper(spec.long) +} diff --git a/usage_test.go b/usage_test.go new file mode 100644 index 0000000..255018d --- /dev/null +++ b/usage_test.go @@ -0,0 +1,81 @@ +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] [--workers WORKERS] INPUT [OUTPUT [OUTPUT ...]]\n" + + expectedHelp := `usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--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 + --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"` + Workers int `arg:"-w,env:WORKERS,help:number of workers to start"` + } + args.Name = "Foo Bar" + args.Value = 42 + p, err := NewParser(&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(&args) + require.NoError(t, err) + + os.Args[0] = "example" + var help bytes.Buffer + p.WriteHelp(&help) + assert.Equal(t, expectedHelp, help.String()) +}