From 408290f7c2a968a0de255813e125a9ebb0a9dda6 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 31 Oct 2015 16:15:24 -0700 Subject: [PATCH 01/63] basic first version working --- parse.go | 268 ++++++++++++++++++++++++++++++++++++++++++++++++++ parse_test.go | 88 +++++++++++++++++ 2 files changed, 356 insertions(+) create mode 100644 parse.go create mode 100644 parse_test.go diff --git a/parse.go b/parse.go new file mode 100644 index 0000000..b58b51e --- /dev/null +++ b/parse.go @@ -0,0 +1,268 @@ +package arguments + +import ( + "fmt" + "os" + "reflect" + "strconv" + "strings" +) + +// MustParse processes command line arguments and exits upon failure. +func MustParse(dest interface{}) { + err := Parse(dest) + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +// Parse processes command line arguments and stores the result in args. +func Parse(dest interface{}) error { + return ParseFrom(dest, os.Args) +} + +// ParseFrom processes command line arguments and stores the result in args. +func ParseFrom(dest interface{}, args []string) error { + v := reflect.ValueOf(dest) + if v.Kind() != reflect.Ptr { + panic(fmt.Sprintf("%s is not a pointer type", v.Type().Name())) + } + v = v.Elem() + + // Parse the spec + spec, err := extractSpec(v.Type()) + if err != nil { + return err + } + + // Process args + err = processArgs(v, spec, args) + if err != nil { + return err + } + + // Validate + return validate(spec) +} + +// spec represents information about an argument extracted from struct tags +type spec struct { + field reflect.StructField + index int + long string + short string + multiple bool + required bool + positional bool + help string + wasPresent bool +} + +// extractSpec gets specifications for each argument from the tags in a struct +func extractSpec(t reflect.Type) ([]*spec, error) { + if t.Kind() != reflect.Struct { + panic(fmt.Sprintf("%s is not a struct pointer", t.Name())) + } + + var specs []*spec + 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), + field: field, + index: 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()) + } + + // 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 + default: + return nil, fmt.Errorf("unrecognized tag '%s' on field %s", key, tag) + } + } + } + specs = append(specs, &spec) + } + return specs, nil +} + +// processArgs processes arguments using a pre-constructed spec +func processArgs(dest reflect.Value, specs []*spec, args []string) error { + // construct a map from arg name to spec + specByName := make(map[string]*spec) + for _, spec := range specs { + if spec.long != "" { + specByName[spec.long] = spec + } + if spec.short != "" { + specByName[spec.short] = spec + } + } + + // 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 := specByName[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++; i < len(args) && !strings.HasPrefix(args[i], "-"); i++ { + values = append(values, args[i]) + } + } else { + values = append(values, value) + } + setSlice(dest, spec, values) + continue + } + + // if it's a flag and it has no value then set the value to true + if spec.field.Type.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(dest.Field(spec.index), value) + if err != nil { + return fmt.Errorf("error processing %s: %v", arg, err) + } + } + return nil +} + +// validate an argument spec after arguments have been parse +func validate(spec []*spec) error { + for _, arg := range spec { + if arg.required && !arg.wasPresent { + return fmt.Errorf("--%s is required", strings.ToLower(arg.field.Name)) + } + } + return nil +} + +// parse a value as the apropriate type and store it in the struct +func setSlice(dest reflect.Value, spec *spec, values []string) error { + // TODO + 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 writable") + } + + 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..4864ebc --- /dev/null +++ b/parse_test.go @@ -0,0 +1,88 @@ +package arguments + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func split(s string) []string { + return strings.Split(s, " ") +} + +func TestStringSingle(t *testing.T) { + var args struct { + Foo string + } + err := ParseFrom(&args, split("--foo bar")) + require.NoError(t, err) + assert.Equal(t, "bar", args.Foo) +} + +func TestMixed(t *testing.T) { + var args struct { + Foo string `arg:"-f"` + Bar int + Ham bool + Spam float32 + } + args.Bar = 3 + err := ParseFrom(&args, split("-spam=1.2 -ham -f xyz")) + require.NoError(t, err) + assert.Equal(t, "xyz", args.Foo) + assert.Equal(t, 3, args.Bar) + assert.Equal(t, true, args.Ham) + assert.Equal(t, 1.2, args.Spam) +} + +func TestRequired(t *testing.T) { + var args struct { + Foo string `arg:"required"` + } + err := ParseFrom(&args, nil) + require.Error(t, err, "--foo is required") +} + +func TestShortFlag(t *testing.T) { + var args struct { + Foo string `arg:"-f"` + } + + err := ParseFrom(&args, split("-f xyz")) + require.NoError(t, err) + assert.Equal(t, "xyz", args.Foo) + + err = ParseFrom(&args, split("-foo xyz")) + require.NoError(t, err) + assert.Equal(t, "xyz", args.Foo) + + err = ParseFrom(&args, split("--foo xyz")) + 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 := ParseFrom(&args, split("-v")) + 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 := ParseFrom(&args, split("-V")) + require.NoError(t, err) + assert.False(t, args.Lower) + assert.True(t, args.Upper) +} From 8397a40f4cafd39c553df848854e022d33149fa5 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 31 Oct 2015 17:05:14 -0700 Subject: [PATCH 02/63] positional arguments working --- parse.go | 78 +++++++++++++++++++++++++++++++++++++++++++-------- parse_test.go | 44 ++++++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 13 deletions(-) diff --git a/parse.go b/parse.go index b58b51e..74b9175 100644 --- a/parse.go +++ b/parse.go @@ -2,6 +2,7 @@ package arguments import ( "fmt" + "log" "os" "reflect" "strconv" @@ -82,6 +83,7 @@ func extractSpec(t reflect.Type) ([]*spec, error) { // Get the scalar type for this field scalarType := field.Type + log.Println(field.Name, field.Type, field.Type.Kind()) if scalarType.Kind() == reflect.Slice { spec.multiple = true scalarType = scalarType.Elem() @@ -133,14 +135,17 @@ func extractSpec(t reflect.Type) ([]*spec, error) { // processArgs processes arguments using a pre-constructed spec func processArgs(dest reflect.Value, specs []*spec, args []string) error { - // construct a map from arg name to spec - specByName := make(map[string]*spec) + // construct a map from --option to spec + optionMap := make(map[string]*spec) for _, spec := range specs { + if spec.positional { + continue + } if spec.long != "" { - specByName[spec.long] = spec + optionMap[spec.long] = spec } if spec.short != "" { - specByName[spec.short] = spec + optionMap[spec.short] = spec } } @@ -170,7 +175,7 @@ func processArgs(dest reflect.Value, specs []*spec, args []string) error { } // lookup the spec for this option - spec, ok := specByName[opt] + spec, ok := optionMap[opt] if !ok { return fmt.Errorf("unknown argument %s", arg) } @@ -180,13 +185,17 @@ func processArgs(dest reflect.Value, specs []*spec, args []string) error { if spec.multiple { var values []string if value == "" { - for i++; i < len(args) && !strings.HasPrefix(args[i], "-"); i++ { - values = append(values, args[i]) + for i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { + values = append(values, args[i+1]) + i++ } } else { values = append(values, value) } - setSlice(dest, spec, values) + err := setSlice(dest.Field(spec.index), values) + if err != nil { + return fmt.Errorf("error processing %s: %v", arg, err) + } continue } @@ -209,13 +218,38 @@ func processArgs(dest reflect.Value, specs []*spec, args []string) error { return fmt.Errorf("error processing %s: %v", arg, err) } } + + // process positionals + for _, spec := range specs { + label := strings.ToLower(spec.field.Name) + if spec.positional { + if spec.multiple { + err := setSlice(dest.Field(spec.index), positionals) + if err != nil { + return fmt.Errorf("error processing %s: %v", label, err) + } + positionals = nil + } else if len(positionals) > 0 { + err := setScalar(dest.Field(spec.index), positionals[0]) + if err != nil { + return fmt.Errorf("error processing %s: %v", label, err) + } + positionals = positionals[1:] + } else if spec.required { + return fmt.Errorf("%s is required", label) + } + } + } + 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.required && !arg.wasPresent { + if !arg.positional && arg.required && !arg.wasPresent { return fmt.Errorf("--%s is required", strings.ToLower(arg.field.Name)) } } @@ -223,15 +257,35 @@ func validate(spec []*spec) error { } // parse a value as the apropriate type and store it in the struct -func setSlice(dest reflect.Value, spec *spec, values []string) error { - // TODO +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.Addr() + } + dest.Set(reflect.Append(dest, v.Elem())) + } 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 writable") + return fmt.Errorf("field is not exported") } switch v.Kind() { diff --git a/parse_test.go b/parse_test.go index 4864ebc..b8e3fa7 100644 --- a/parse_test.go +++ b/parse_test.go @@ -25,14 +25,16 @@ 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 := ParseFrom(&args, split("-spam=1.2 -ham -f xyz")) + err := ParseFrom(&args, split("123 -spam=1.2 -ham -f xyz")) 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.Equal(t, 1.2, args.Spam) } @@ -86,3 +88,43 @@ func TestCaseSensitive2(t *testing.T) { 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 := ParseFrom(&args, split("foo")) + require.NoError(t, err) + assert.Equal(t, "foo", args.Input) + assert.Equal(t, "", args.Output) +} + +func TestRequiredPositional(t *testing.T) { + var args struct { + Input string `arg:"positional"` + Output string `arg:"positional,required"` + } + err := ParseFrom(&args, split("foo")) + assert.Error(t, err) +} + +func TestTooManyPositional(t *testing.T) { + var args struct { + Input string `arg:"positional"` + Output string `arg:"positional"` + } + err := ParseFrom(&args, split("foo bar baz")) + assert.Error(t, err) +} + +func TestMultiple(t *testing.T) { + var args struct { + Foo []int + Bar []string + } + err := ParseFrom(&args, split("--foo 1 2 3 --bar x y z")) + require.NoError(t, err) + assert.Equal(t, []int{1, 2, 3}, args.Foo) + assert.Equal(t, []string{"x", "y", "z"}, args.Bar) +} From b9ad104f3301e7d078cd9ba16410eae3f6e772aa Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 31 Oct 2015 18:26:58 -0700 Subject: [PATCH 03/63] added usage generation --- parse.go | 193 ++++++++++++++++++++++++++------------------------ parse_test.go | 26 +++---- 2 files changed, 114 insertions(+), 105 deletions(-) diff --git a/parse.go b/parse.go index 74b9175..d9382c9 100644 --- a/parse.go +++ b/parse.go @@ -1,17 +1,28 @@ -package arguments +package arg import ( "fmt" - "log" "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 + wasPresent bool +} + // MustParse processes command line arguments and exits upon failure. -func MustParse(dest interface{}) { - err := Parse(dest) +func MustParse(dest ...interface{}) { + err := Parse(dest...) if err != nil { fmt.Println(err) os.Exit(1) @@ -19,122 +30,121 @@ func MustParse(dest interface{}) { } // Parse processes command line arguments and stores the result in args. -func Parse(dest interface{}) error { - return ParseFrom(dest, os.Args) +func Parse(dest ...interface{}) error { + return ParseFrom(os.Args[1:], dest...) } // ParseFrom processes command line arguments and stores the result in args. -func ParseFrom(dest interface{}, args []string) error { - v := reflect.ValueOf(dest) - if v.Kind() != reflect.Ptr { - panic(fmt.Sprintf("%s is not a pointer type", v.Type().Name())) +func ParseFrom(args []string, dest ...interface{}) error { + // Add the help option if one is not already defined + var internal struct { + Help bool `arg:"-h"` } - v = v.Elem() // Parse the spec - spec, err := extractSpec(v.Type()) + dest = append(dest, &internal) + spec, err := extractSpec(dest...) if err != nil { return err } // Process args - err = processArgs(v, spec, args) + err = processArgs(spec, args) if err != nil { return err } + // If -h or --help were specified then print help + if internal.Help { + writeUsage(os.Stdout, spec) + os.Exit(0) + } + // Validate return validate(spec) } -// spec represents information about an argument extracted from struct tags -type spec struct { - field reflect.StructField - index int - long string - short string - multiple bool - required bool - positional bool - help string - wasPresent bool -} - // extractSpec gets specifications for each argument from the tags in a struct -func extractSpec(t reflect.Type) ([]*spec, error) { - if t.Kind() != reflect.Struct { - panic(fmt.Sprintf("%s is not a struct pointer", t.Name())) - } - +func extractSpec(dests ...interface{}) ([]*spec, error) { var specs []*spec - 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 + 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())) } - - spec := spec{ - long: strings.ToLower(field.Name), - field: field, - index: i, + v = v.Elem() + if v.Kind() != reflect.Struct { + panic(fmt.Sprintf("%T is not a struct pointer", dest)) } - // Get the scalar type for this field - scalarType := field.Type - log.Println(field.Name, field.Type, field.Type.Kind()) - if scalarType.Kind() == reflect.Slice { - spec.multiple = true - scalarType = scalarType.Elem() - if scalarType.Kind() == reflect.Ptr { - scalarType = scalarType.Elem() + 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 } - } - // 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()) - } + spec := spec{ + long: strings.ToLower(field.Name), + dest: v.Field(i), + } - // 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] + // 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()) + } + + // 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) + 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 + default: + return nil, fmt.Errorf("unrecognized tag '%s' on field %s", key, tag) } - spec.short = key[1:] - case key == "required": - spec.required = true - case key == "positional": - spec.positional = true - case key == "help": - spec.help = value - default: - return nil, fmt.Errorf("unrecognized tag '%s' on field %s", key, tag) } } + specs = append(specs, &spec) } - specs = append(specs, &spec) } return specs, nil } // processArgs processes arguments using a pre-constructed spec -func processArgs(dest reflect.Value, specs []*spec, args []string) error { +func processArgs(specs []*spec, args []string) error { // construct a map from --option to spec optionMap := make(map[string]*spec) for _, spec := range specs { @@ -192,7 +202,7 @@ func processArgs(dest reflect.Value, specs []*spec, args []string) error { } else { values = append(values, value) } - err := setSlice(dest.Field(spec.index), values) + err := setSlice(spec.dest, values) if err != nil { return fmt.Errorf("error processing %s: %v", arg, err) } @@ -200,7 +210,7 @@ func processArgs(dest reflect.Value, specs []*spec, args []string) error { } // if it's a flag and it has no value then set the value to true - if spec.field.Type.Kind() == reflect.Bool && value == "" { + if spec.dest.Kind() == reflect.Bool && value == "" { value = "true" } @@ -213,7 +223,7 @@ func processArgs(dest reflect.Value, specs []*spec, args []string) error { i++ } - err := setScalar(dest.Field(spec.index), value) + err := setScalar(spec.dest, value) if err != nil { return fmt.Errorf("error processing %s: %v", arg, err) } @@ -221,22 +231,21 @@ func processArgs(dest reflect.Value, specs []*spec, args []string) error { // process positionals for _, spec := range specs { - label := strings.ToLower(spec.field.Name) if spec.positional { if spec.multiple { - err := setSlice(dest.Field(spec.index), positionals) + err := setSlice(spec.dest, positionals) if err != nil { - return fmt.Errorf("error processing %s: %v", label, err) + return fmt.Errorf("error processing %s: %v", spec.long, err) } positionals = nil } else if len(positionals) > 0 { - err := setScalar(dest.Field(spec.index), positionals[0]) + err := setScalar(spec.dest, positionals[0]) if err != nil { - return fmt.Errorf("error processing %s: %v", label, err) + return fmt.Errorf("error processing %s: %v", spec.long, err) } positionals = positionals[1:] } else if spec.required { - return fmt.Errorf("%s is required", label) + return fmt.Errorf("%s is required", spec.long) } } } @@ -250,7 +259,7 @@ func processArgs(dest reflect.Value, specs []*spec, args []string) error { func validate(spec []*spec) error { for _, arg := range spec { if !arg.positional && arg.required && !arg.wasPresent { - return fmt.Errorf("--%s is required", strings.ToLower(arg.field.Name)) + return fmt.Errorf("--%s is required", arg.long) } } return nil diff --git a/parse_test.go b/parse_test.go index b8e3fa7..9ad5944 100644 --- a/parse_test.go +++ b/parse_test.go @@ -1,4 +1,4 @@ -package arguments +package arg import ( "strings" @@ -16,7 +16,7 @@ func TestStringSingle(t *testing.T) { var args struct { Foo string } - err := ParseFrom(&args, split("--foo bar")) + err := ParseFrom(split("--foo bar"), &args) require.NoError(t, err) assert.Equal(t, "bar", args.Foo) } @@ -30,7 +30,7 @@ func TestMixed(t *testing.T) { Spam float32 } args.Bar = 3 - err := ParseFrom(&args, split("123 -spam=1.2 -ham -f xyz")) + err := ParseFrom(split("123 -spam=1.2 -ham -f xyz"), &args) require.NoError(t, err) assert.Equal(t, "xyz", args.Foo) assert.Equal(t, 3, args.Bar) @@ -43,7 +43,7 @@ func TestRequired(t *testing.T) { var args struct { Foo string `arg:"required"` } - err := ParseFrom(&args, nil) + err := ParseFrom(nil, &args) require.Error(t, err, "--foo is required") } @@ -52,15 +52,15 @@ func TestShortFlag(t *testing.T) { Foo string `arg:"-f"` } - err := ParseFrom(&args, split("-f xyz")) + err := ParseFrom(split("-f xyz"), &args) require.NoError(t, err) assert.Equal(t, "xyz", args.Foo) - err = ParseFrom(&args, split("-foo xyz")) + err = ParseFrom(split("-foo xyz"), &args) require.NoError(t, err) assert.Equal(t, "xyz", args.Foo) - err = ParseFrom(&args, split("--foo xyz")) + err = ParseFrom(split("--foo xyz"), &args) require.NoError(t, err) assert.Equal(t, "xyz", args.Foo) } @@ -71,7 +71,7 @@ func TestCaseSensitive(t *testing.T) { Upper bool `arg:"-V"` } - err := ParseFrom(&args, split("-v")) + err := ParseFrom(split("-v"), &args) require.NoError(t, err) assert.True(t, args.Lower) assert.False(t, args.Upper) @@ -83,7 +83,7 @@ func TestCaseSensitive2(t *testing.T) { Upper bool `arg:"-V"` } - err := ParseFrom(&args, split("-V")) + err := ParseFrom(split("-V"), &args) require.NoError(t, err) assert.False(t, args.Lower) assert.True(t, args.Upper) @@ -94,7 +94,7 @@ func TestPositional(t *testing.T) { Input string `arg:"positional"` Output string `arg:"positional"` } - err := ParseFrom(&args, split("foo")) + err := ParseFrom(split("foo"), &args) require.NoError(t, err) assert.Equal(t, "foo", args.Input) assert.Equal(t, "", args.Output) @@ -105,7 +105,7 @@ func TestRequiredPositional(t *testing.T) { Input string `arg:"positional"` Output string `arg:"positional,required"` } - err := ParseFrom(&args, split("foo")) + err := ParseFrom(split("foo"), &args) assert.Error(t, err) } @@ -114,7 +114,7 @@ func TestTooManyPositional(t *testing.T) { Input string `arg:"positional"` Output string `arg:"positional"` } - err := ParseFrom(&args, split("foo bar baz")) + err := ParseFrom(split("foo bar baz"), &args) assert.Error(t, err) } @@ -123,7 +123,7 @@ func TestMultiple(t *testing.T) { Foo []int Bar []string } - err := ParseFrom(&args, split("--foo 1 2 3 --bar x y z")) + err := ParseFrom(split("--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) From c131173edd687346343b1cde1cb719e07365e9e7 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 31 Oct 2015 18:30:06 -0700 Subject: [PATCH 04/63] Initial commit --- .gitignore | 24 ++++++++++++++++++++++++ LICENSE | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE 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/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. + From 76293a5a725eb962839b8c69a2bda1296e1fdbdf Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 31 Oct 2015 18:32:20 -0700 Subject: [PATCH 05/63] add usage.go --- usage.go | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 usage.go diff --git a/usage.go b/usage.go new file mode 100644 index 0000000..06a3ebd --- /dev/null +++ b/usage.go @@ -0,0 +1,112 @@ +package arg + +import ( + "fmt" + "io" + "os" + "reflect" + "strings" +) + +// Usage prints usage information to stdout information and exits with status zero +func Usage(dest ...interface{}) { + if err := WriteUsage(os.Stdout, dest...); err != nil { + fmt.Println(err) + } + os.Exit(0) +} + +// Fail prints usage information to stdout and exits with non-zero status +func Fail(msg string, dest ...interface{}) { + fmt.Println(msg) + if err := WriteUsage(os.Stdout, dest...); err != nil { + fmt.Println(err) + } + os.Exit(1) +} + +// WriteUsage writes usage information to the given writer +func WriteUsage(w io.Writer, dest ...interface{}) error { + spec, err := extractSpec(dest...) + if err != nil { + return err + } + writeUsage(w, spec) + return nil +} + +func synopsis(spec *spec, form string) string { + if spec.dest.Kind() == reflect.Bool { + return form + } else { + return form + " " + strings.ToUpper(spec.long) + } +} + +// writeUsage writes usage information to the given writer +func writeUsage(w io.Writer, specs []*spec) { + var positionals, options []*spec + for _, spec := range specs { + if spec.positional { + positionals = append(positionals, spec) + } else { + options = append(options, spec) + } + } + + fmt.Fprint(w, "usage: ") + + // write the option component of the one-line usage message + for _, spec := range options { + if !spec.required { + fmt.Fprint(w, "[") + } + fmt.Fprint(w, synopsis(spec, "--"+spec.long)) + if !spec.required { + fmt.Fprint(w, "]") + } + fmt.Fprint(w, " ") + } + + // write the positional component of the one-line usage message + for _, spec := range positionals { + up := strings.ToUpper(spec.long) + if spec.multiple { + fmt.Fprintf(w, "[%s [%s ...]]", up) + } else { + fmt.Fprint(w, up) + } + fmt.Fprint(w, " ") + } + fmt.Fprint(w, "\n") + + // write the list of positionals + if len(positionals) > 0 { + fmt.Fprint(w, "\npositional arguments:\n") + for _, spec := range positionals { + fmt.Fprintf(w, " %s\n", spec.long) + } + } + + // write the list of options + if len(options) > 0 { + fmt.Fprint(w, "\noptions:\n") + const colWidth = 25 + for _, spec := range options { + left := fmt.Sprint(synopsis(spec, "--"+spec.long)) + if spec.short != "" { + left += ", " + fmt.Sprint(synopsis(spec, "-"+spec.short)) + } + fmt.Print(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") + } + } +} From 7e1437715baf10561c97609628b8f4136a47dd67 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 31 Oct 2015 18:32:43 -0700 Subject: [PATCH 06/63] add example.go --- example/example.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 example/example.go diff --git a/example/example.go b/example/example.go new file mode 100644 index 0000000..20efeec --- /dev/null +++ b/example/example.go @@ -0,0 +1,14 @@ +package main + +import "github.com/alexflint/go-arg" + +func main() { + var args struct { + Input string `arg:"positional"` + Output string `arg:"positional"` + Foo string `arg:"help:this argument is foo"` + VeryLongArgument int `arg:"help:this argument is very long"` + Bar float64 `arg:"-b"` + } + arg.MustParse(&args) +} From 22c73471e6ceb1674072338cea99c3124ca2a648 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 31 Oct 2015 18:46:56 -0700 Subject: [PATCH 07/63] add README --- README.md | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..d45a31f --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +Argument parsing for Go. + +```golang +var args struct { + Foo string + Bar bool +} +arg.MustParse(&args) +fmt.Println(args.Foo) +fmt.Println(args.Bar) +``` + +```bash +$ ./example --foo=hello --bar +hello +True +``` + +Setting defaults values: + +```golang +var args struct { + Foo string + Bar bool +} +args.Foo = "default value" +arg.MustParse(&args) +``` + +Marking options as required + +```golang +var args struct { + Foo string `arg:"required"` + Bar bool +} +arg.MustParse(&args) +``` + +Positional argument: + +```golang +var args struct { + Input string `arg:"positional"` + Output []string `arg:"positional"` + Verbose bool +} +arg.MustParse(&args) +fmt.Println("Input:", input) +fmt.Println("Output:", output) +``` + +``` +$ ./example src.txt x.out y.out z.out +Input: src.txt +Output: [x.out y.out z.out] +``` + +Usage strings: +```bash +$ ./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 +``` + +Options with multiple values: +``` +var args struct { + Database string + IDs []int64 +} +arg.MustParse(&args) +fmt.Printf("Fetching the following IDs from %s: %q", args.Database, args.IDs) +``` + +```bash +./example -database foo -ids 1 2 3 +Fetching the following IDs from foo: [1 2 3] +``` From 04e96c0c6b8bb92fba62bcc5a080e7727ad3edfd Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 31 Oct 2015 18:48:38 -0700 Subject: [PATCH 08/63] udpate readme --- README.md | 14 ++++++++------ example/example.go | 10 +++++----- parse.go | 2 +- usage.go | 2 +- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d45a31f..f3db6c9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -Argument parsing for Go. +# Argument parsing for Go ```golang +import "github.com/alexflint/go-arg" + var args struct { Foo string Bar bool @@ -16,7 +18,7 @@ hello True ``` -Setting defaults values: +### Default values ```golang var args struct { @@ -27,7 +29,7 @@ args.Foo = "default value" arg.MustParse(&args) ``` -Marking options as required +### Marking options as required ```golang var args struct { @@ -37,7 +39,7 @@ var args struct { arg.MustParse(&args) ``` -Positional argument: +### Positional argument ```golang var args struct { @@ -56,7 +58,7 @@ Input: src.txt Output: [x.out y.out z.out] ``` -Usage strings: +### Usage strings ```bash $ ./example -h usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]] @@ -73,7 +75,7 @@ options: --help, -h print this help message ``` -Options with multiple values: +### Options with multiple values ``` var args struct { Database string diff --git a/example/example.go b/example/example.go index 20efeec..1b1c7e0 100644 --- a/example/example.go +++ b/example/example.go @@ -4,11 +4,11 @@ import "github.com/alexflint/go-arg" func main() { var args struct { - Input string `arg:"positional"` - Output string `arg:"positional"` - Foo string `arg:"help:this argument is foo"` - VeryLongArgument int `arg:"help:this argument is very long"` - Bar float64 `arg:"-b"` + 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) } diff --git a/parse.go b/parse.go index d9382c9..6e7f4bd 100644 --- a/parse.go +++ b/parse.go @@ -38,7 +38,7 @@ func Parse(dest ...interface{}) error { func ParseFrom(args []string, dest ...interface{}) error { // Add the help option if one is not already defined var internal struct { - Help bool `arg:"-h"` + Help bool `arg:"-h,help:print this help message"` } // Parse the spec diff --git a/usage.go b/usage.go index 06a3ebd..5155d82 100644 --- a/usage.go +++ b/usage.go @@ -72,7 +72,7 @@ func writeUsage(w io.Writer, specs []*spec) { for _, spec := range positionals { up := strings.ToUpper(spec.long) if spec.multiple { - fmt.Fprintf(w, "[%s [%s ...]]", up) + fmt.Fprintf(w, "[%s [%s ...]]", up, up) } else { fmt.Fprint(w, up) } From 65820885963ded66c34560ac619b323a8bbfc032 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 31 Oct 2015 18:49:20 -0700 Subject: [PATCH 09/63] udpate readme --- README.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f3db6c9..88441de 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Argument parsing for Go -```golang +```go import "github.com/alexflint/go-arg" var args struct { @@ -8,19 +8,17 @@ var args struct { Bar bool } arg.MustParse(&args) -fmt.Println(args.Foo) -fmt.Println(args.Bar) +fmt.Println(args.Foo, args.Bar) ``` -```bash +```shell $ ./example --foo=hello --bar -hello -True +hello True ``` ### Default values -```golang +```go var args struct { Foo string Bar bool @@ -31,7 +29,7 @@ arg.MustParse(&args) ### Marking options as required -```golang +```go var args struct { Foo string `arg:"required"` Bar bool @@ -41,7 +39,7 @@ arg.MustParse(&args) ### Positional argument -```golang +```go var args struct { Input string `arg:"positional"` Output []string `arg:"positional"` @@ -59,7 +57,7 @@ Output: [x.out y.out z.out] ``` ### Usage strings -```bash +```shell $ ./example -h usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]] @@ -85,7 +83,7 @@ arg.MustParse(&args) fmt.Printf("Fetching the following IDs from %s: %q", args.Database, args.IDs) ``` -```bash +```shell ./example -database foo -ids 1 2 3 Fetching the following IDs from foo: [1 2 3] ``` From aa20f7be3968590f4b4c88c42745b6a379eef746 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 31 Oct 2015 18:51:21 -0700 Subject: [PATCH 10/63] udpate readme --- README.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 88441de..7961bd0 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,9 @@ # Argument parsing for Go ```go -import "github.com/alexflint/go-arg" - var args struct { - Foo string - Bar bool + Foo string + Bar bool } arg.MustParse(&args) fmt.Println(args.Foo, args.Bar) @@ -57,6 +55,17 @@ Output: [x.out y.out z.out] ``` ### 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 ...]] @@ -74,7 +83,7 @@ options: ``` ### Options with multiple values -``` +```go var args struct { Database string IDs []int64 From 2ac8f555a511a2b47e001d0a9cfc9594b7ac62cd Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 31 Oct 2015 19:10:13 -0700 Subject: [PATCH 11/63] udpate readme --- README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7961bd0..a170492 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Argument parsing for Go +## Structured argument parsing for Go ```go var args struct { @@ -11,7 +11,7 @@ fmt.Println(args.Foo, args.Bar) ```shell $ ./example --foo=hello --bar -hello True +hello true ``` ### Default values @@ -41,7 +41,6 @@ arg.MustParse(&args) var args struct { Input string `arg:"positional"` Output []string `arg:"positional"` - Verbose bool } arg.MustParse(&args) fmt.Println("Input:", input) @@ -96,3 +95,13 @@ fmt.Printf("Fetching the following IDs from %s: %q", args.Database, args.IDs) ./example -database foo -ids 1 2 3 Fetching the following IDs from foo: [1 2 3] ``` + +### Rationale + +There are many command line argument parsing libraries for golang, 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. Bool arguments must have explicit values, so `./prog -debug=1` sets debug to true but `./myprog -debug` does not. + +Many third-party argument parsing libraries are geared for writing sophisticated command line interfaces. The excellent `codegangsta/cli` is perfect for implementing sophisticated command line tools, 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 golang already has an excellent way to describe named data structures using 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 just describe both with a single struct? From 6dc9bbbdfde8bed4cbbd25fd664ff6362b9acce0 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 31 Oct 2015 19:13:48 -0700 Subject: [PATCH 12/63] udpate readme --- README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a170492..9c1c20b 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,12 @@ $ ./example --foo=hello --bar hello true ``` +### Installation + +```shell +go get github.com/alexflint/go-arg +``` + ### Default values ```go @@ -98,10 +104,10 @@ Fetching the following IDs from foo: [1 2 3] ### Rationale -There are many command line argument parsing libraries for golang, including one in the standard library, so why build another? +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. Bool arguments must have explicit values, so `./prog -debug=1` sets debug to true but `./myprog -debug` does not. +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. Boolean arguments must have explicit values, so `./prog -debug=1` sets debug to true but `./myprog -debug` does not. -Many third-party argument parsing libraries are geared for writing sophisticated command line interfaces. The excellent `codegangsta/cli` is perfect for implementing sophisticated command line tools, with multiple sub-commands and nested flags, but is probably overkill for a simple script with a handful of flags. +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 golang already has an excellent way to describe named data structures using 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 just describe both with a single struct? +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? From 19d956870f660ed4ca00069e763c2806dade3772 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 31 Oct 2015 19:14:11 -0700 Subject: [PATCH 13/63] udpate readme --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9c1c20b..b08300a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ ## Structured argument parsing for Go +```shell +go get github.com/alexflint/go-arg +``` + ```go var args struct { Foo string @@ -14,12 +18,6 @@ $ ./example --foo=hello --bar hello true ``` -### Installation - -```shell -go get github.com/alexflint/go-arg -``` - ### Default values ```go From b666b30474ad3ef77ea4f22d0f88683cb7abbe5c Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 31 Oct 2015 19:15:08 -0700 Subject: [PATCH 14/63] udpate readme --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b08300a..3b69c59 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,5 @@ ## Structured argument parsing for Go -```shell -go get github.com/alexflint/go-arg -``` - ```go var args struct { Foo string @@ -29,7 +25,7 @@ args.Foo = "default value" arg.MustParse(&args) ``` -### Marking options as required +### Required options ```go var args struct { @@ -100,6 +96,12 @@ fmt.Printf("Fetching the following IDs from %s: %q", args.Database, args.IDs) Fetching the following IDs from foo: [1 2 3] ``` +### Installation + +```shell +go get 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? From 026a8246669cd8799d694060f46d5273a8cb6e84 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 31 Oct 2015 19:15:43 -0700 Subject: [PATCH 15/63] udpate readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3b69c59..426c82d 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ args.Foo = "default value" arg.MustParse(&args) ``` -### Required options +### Required arguments ```go var args struct { @@ -35,7 +35,7 @@ var args struct { arg.MustParse(&args) ``` -### Positional argument +### Positional arguments ```go var args struct { @@ -81,7 +81,7 @@ options: --help, -h print this help message ``` -### Options with multiple values +### Arguments with multiple values ```go var args struct { Database string From f427e9f317714fc985c39ca24990852e73be5ef0 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 31 Oct 2015 23:57:26 -0700 Subject: [PATCH 16/63] added parser struct --- parse.go | 76 +++++++++++++++++++++++++++++++++++---------------- parse_test.go | 32 ++++++++++++---------- usage.go | 25 +++++++++-------- 3 files changed, 84 insertions(+), 49 deletions(-) diff --git a/parse.go b/parse.go index 6e7f4bd..119efbd 100644 --- a/parse.go +++ b/parse.go @@ -1,8 +1,11 @@ package arg import ( + "errors" "fmt" + "io" "os" + "path/filepath" "reflect" "strconv" "strings" @@ -20,48 +23,74 @@ type spec struct { wasPresent bool } +// Parse returns this value to indicate 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{}) { - err := Parse(dest...) + p, err := NewParser(dest...) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + err = p.Parse(os.Args[1:]) if err != nil { fmt.Println(err) + writeUsage(os.Stdout, filepath.Base(os.Args[0]), p.spec) os.Exit(1) } } -// Parse processes command line arguments and stores the result in args. +// Parse processes command line arguments and stores them in dest. func Parse(dest ...interface{}) error { - return ParseFrom(os.Args[1:], dest...) + p, err := NewParser(dest...) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + return p.Parse(os.Args[1:]) } -// ParseFrom processes command line arguments and stores the result in args. -func ParseFrom(args []string, dest ...interface{}) error { - // Add the help option if one is not already defined - var internal struct { - Help bool `arg:"-h,help:print this help message"` - } +// Parser represents a set of command line options with destination values +type Parser struct { + spec []*spec +} - // Parse the spec - dest = append(dest, &internal) +// NewParser constructs a parser from a list of destination structs +func NewParser(dest ...interface{}) (*Parser, error) { spec, err := extractSpec(dest...) if err != nil { - return err + return nil, err } + return &Parser{spec: spec}, nil +} - // Process args - err = processArgs(spec, args) - if err != nil { - return err +// 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 + } } - // If -h or --help were specified then print help - if internal.Help { - writeUsage(os.Stdout, spec) - os.Exit(0) + // Process all command line arguments + err := process(p.spec, args) + if err != nil { + return err } // Validate - return validate(spec) + return validate(p.spec) +} + +// WriteUsage writes usage information to the given writer +func (p *Parser) WriteUsage(w io.Writer) { + writeUsage(w, filepath.Base(os.Args[0]), p.spec) } // extractSpec gets specifications for each argument from the tags in a struct @@ -143,8 +172,9 @@ func extractSpec(dests ...interface{}) ([]*spec, error) { return specs, nil } -// processArgs processes arguments using a pre-constructed spec -func processArgs(specs []*spec, args []string) error { +// process goes through arguments the 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 { diff --git a/parse_test.go b/parse_test.go index 9ad5944..4037ef5 100644 --- a/parse_test.go +++ b/parse_test.go @@ -8,15 +8,19 @@ import ( "github.com/stretchr/testify/require" ) -func split(s string) []string { - return strings.Split(s, " ") +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 := ParseFrom(split("--foo bar"), &args) + err := parse("--foo bar", &args) require.NoError(t, err) assert.Equal(t, "bar", args.Foo) } @@ -30,7 +34,7 @@ func TestMixed(t *testing.T) { Spam float32 } args.Bar = 3 - err := ParseFrom(split("123 -spam=1.2 -ham -f xyz"), &args) + 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) @@ -43,7 +47,7 @@ func TestRequired(t *testing.T) { var args struct { Foo string `arg:"required"` } - err := ParseFrom(nil, &args) + err := parse("", &args) require.Error(t, err, "--foo is required") } @@ -52,15 +56,15 @@ func TestShortFlag(t *testing.T) { Foo string `arg:"-f"` } - err := ParseFrom(split("-f xyz"), &args) + err := parse("-f xyz", &args) require.NoError(t, err) assert.Equal(t, "xyz", args.Foo) - err = ParseFrom(split("-foo xyz"), &args) + err = parse("-foo xyz", &args) require.NoError(t, err) assert.Equal(t, "xyz", args.Foo) - err = ParseFrom(split("--foo xyz"), &args) + err = parse("--foo xyz", &args) require.NoError(t, err) assert.Equal(t, "xyz", args.Foo) } @@ -71,7 +75,7 @@ func TestCaseSensitive(t *testing.T) { Upper bool `arg:"-V"` } - err := ParseFrom(split("-v"), &args) + err := parse("-v", &args) require.NoError(t, err) assert.True(t, args.Lower) assert.False(t, args.Upper) @@ -83,7 +87,7 @@ func TestCaseSensitive2(t *testing.T) { Upper bool `arg:"-V"` } - err := ParseFrom(split("-V"), &args) + err := parse("-V", &args) require.NoError(t, err) assert.False(t, args.Lower) assert.True(t, args.Upper) @@ -94,7 +98,7 @@ func TestPositional(t *testing.T) { Input string `arg:"positional"` Output string `arg:"positional"` } - err := ParseFrom(split("foo"), &args) + err := parse("foo", &args) require.NoError(t, err) assert.Equal(t, "foo", args.Input) assert.Equal(t, "", args.Output) @@ -105,7 +109,7 @@ func TestRequiredPositional(t *testing.T) { Input string `arg:"positional"` Output string `arg:"positional,required"` } - err := ParseFrom(split("foo"), &args) + err := parse("foo", &args) assert.Error(t, err) } @@ -114,7 +118,7 @@ func TestTooManyPositional(t *testing.T) { Input string `arg:"positional"` Output string `arg:"positional"` } - err := ParseFrom(split("foo bar baz"), &args) + err := parse("foo bar baz", &args) assert.Error(t, err) } @@ -123,7 +127,7 @@ func TestMultiple(t *testing.T) { Foo []int Bar []string } - err := ParseFrom(split("--foo 1 2 3 --bar x y z"), &args) + 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) diff --git a/usage.go b/usage.go index 5155d82..fdc2248 100644 --- a/usage.go +++ b/usage.go @@ -4,11 +4,12 @@ import ( "fmt" "io" "os" + "path/filepath" "reflect" "strings" ) -// Usage prints usage information to stdout information and exits with status zero +// Usage prints usage information to stdout and exits with status zero func Usage(dest ...interface{}) { if err := WriteUsage(os.Stdout, dest...); err != nil { fmt.Println(err) @@ -31,20 +32,12 @@ func WriteUsage(w io.Writer, dest ...interface{}) error { if err != nil { return err } - writeUsage(w, spec) + writeUsage(w, filepath.Base(os.Args[0]), spec) return nil } -func synopsis(spec *spec, form string) string { - if spec.dest.Kind() == reflect.Bool { - return form - } else { - return form + " " + strings.ToUpper(spec.long) - } -} - // writeUsage writes usage information to the given writer -func writeUsage(w io.Writer, specs []*spec) { +func writeUsage(w io.Writer, cmd string, specs []*spec) { var positionals, options []*spec for _, spec := range specs { if spec.positional { @@ -54,7 +47,7 @@ func writeUsage(w io.Writer, specs []*spec) { } } - fmt.Fprint(w, "usage: ") + fmt.Fprint(w, "usage: %s ", cmd) // write the option component of the one-line usage message for _, spec := range options { @@ -110,3 +103,11 @@ func writeUsage(w io.Writer, specs []*spec) { } } } + +func synopsis(spec *spec, form string) string { + if spec.dest.Kind() == reflect.Bool { + return form + } else { + return form + " " + strings.ToUpper(spec.long) + } +} From 30befae91a472e767b021961620ac0d93e6ff11d Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sun, 1 Nov 2015 00:13:23 -0700 Subject: [PATCH 17/63] move more stuff over to the parser struct --- parse.go | 85 +++++++++++++++++++++++--------------------------------- usage.go | 30 ++++---------------- 2 files changed, 40 insertions(+), 75 deletions(-) diff --git a/parse.go b/parse.go index 119efbd..dfa3421 100644 --- a/parse.go +++ b/parse.go @@ -3,9 +3,7 @@ package arg import ( "errors" "fmt" - "io" "os" - "path/filepath" "reflect" "strconv" "strings" @@ -23,10 +21,10 @@ type spec struct { wasPresent bool } -// Parse returns this value to indicate that -h or --help were provided +// 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. +// MustParse processes command line arguments and exits upon failure func MustParse(dest ...interface{}) { p, err := NewParser(dest...) if err != nil { @@ -34,19 +32,20 @@ func MustParse(dest ...interface{}) { os.Exit(1) } err = p.Parse(os.Args[1:]) + if err == ErrHelp { + p.WriteUsage(os.Stdout) + os.Exit(0) + } if err != nil { - fmt.Println(err) - writeUsage(os.Stdout, filepath.Base(os.Args[0]), p.spec) - os.Exit(1) + p.Fail(err.Error()) } } -// Parse processes command line arguments and stores them in dest. +// Parse processes command line arguments and stores them in dest func Parse(dest ...interface{}) error { p, err := NewParser(dest...) if err != nil { - fmt.Println(err) - os.Exit(1) + return err } return p.Parse(os.Args[1:]) } @@ -57,44 +56,7 @@ type Parser struct { } // NewParser constructs a parser from a list of destination structs -func NewParser(dest ...interface{}) (*Parser, error) { - spec, err := extractSpec(dest...) - if err != nil { - return nil, err - } - return &Parser{spec: spec}, 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) -} - -// WriteUsage writes usage information to the given writer -func (p *Parser) WriteUsage(w io.Writer) { - writeUsage(w, filepath.Base(os.Args[0]), p.spec) -} - -// extractSpec gets specifications for each argument from the tags in a struct -func extractSpec(dests ...interface{}) ([]*spec, error) { +func NewParser(dests ...interface{}) (*Parser, error) { var specs []*spec for _, dest := range dests { v := reflect.ValueOf(dest) @@ -169,10 +131,33 @@ func extractSpec(dests ...interface{}) ([]*spec, error) { specs = append(specs, &spec) } } - return specs, nil + 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 the arguments one-by-one, parses them, and assigns the result to +// 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 diff --git a/usage.go b/usage.go index fdc2248..689da73 100644 --- a/usage.go +++ b/usage.go @@ -9,37 +9,17 @@ import ( "strings" ) -// Usage prints usage information to stdout and exits with status zero -func Usage(dest ...interface{}) { - if err := WriteUsage(os.Stdout, dest...); err != nil { - fmt.Println(err) - } - os.Exit(0) -} - // Fail prints usage information to stdout and exits with non-zero status -func Fail(msg string, dest ...interface{}) { +func (p *Parser) Fail(msg string) { fmt.Println(msg) - if err := WriteUsage(os.Stdout, dest...); err != nil { - fmt.Println(err) - } + p.WriteUsage(os.Stdout) os.Exit(1) } // WriteUsage writes usage information to the given writer -func WriteUsage(w io.Writer, dest ...interface{}) error { - spec, err := extractSpec(dest...) - if err != nil { - return err - } - writeUsage(w, filepath.Base(os.Args[0]), spec) - return nil -} - -// writeUsage writes usage information to the given writer -func writeUsage(w io.Writer, cmd string, specs []*spec) { +func (p *Parser) WriteUsage(w io.Writer) { var positionals, options []*spec - for _, spec := range specs { + for _, spec := range p.spec { if spec.positional { positionals = append(positionals, spec) } else { @@ -47,7 +27,7 @@ func writeUsage(w io.Writer, cmd string, specs []*spec) { } } - fmt.Fprint(w, "usage: %s ", cmd) + fmt.Fprintf(w, "usage: %s ", filepath.Base(os.Args[0])) // write the option component of the one-line usage message for _, spec := range options { From beede9329ae104d7db320406a63621b5bda03a36 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sun, 1 Nov 2015 11:34:22 -0800 Subject: [PATCH 18/63] added runnable examples --- README.md | 12 +++++-- example_test.go | 89 +++++++++++++++++++++++++++++++++++++++++++++++++ parse.go | 34 +++++++++++++++++++ 3 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 example_test.go diff --git a/README.md b/README.md index 426c82d..a97cff5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ +[![GoDoc](https://godoc.org/github.com/alexflint/go-arg?status.svg)](https://godoc.org/github.com/alexflint/go-arg) + ## Structured argument parsing for Go +Declare the command line arguments your program accepts by defining a struct. + ```go var args struct { Foo string @@ -43,8 +47,8 @@ var args struct { Output []string `arg:"positional"` } arg.MustParse(&args) -fmt.Println("Input:", input) -fmt.Println("Output:", output) +fmt.Println("Input:", args.Input) +fmt.Println("Output:", args.Output) ``` ``` @@ -102,6 +106,10 @@ Fetching the following IDs from foo: [1 2 3] 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? diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..d2febf6 --- /dev/null +++ b/example_test.go @@ -0,0 +1,89 @@ +package arg + +import ( + "fmt" + "os" + + "github.com/alexflint/go-arg" +) + +// 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 + } + arg.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" + arg.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 + } + arg.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"` + } + arg.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 + } + arg.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_Usage() { + // 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"` + } + arg.MustParse(&args) +} diff --git a/parse.go b/parse.go index dfa3421..6e92633 100644 --- a/parse.go +++ b/parse.go @@ -1,3 +1,37 @@ +// Package arg parses command line arguments using the fields from a struct. +// Any exported field is interpreted as a command line option, so +// +// 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 --bar // bar is a boolean flag so its value is optional +// ./example -iter 1 // bar will default to its zero value +// ./example --bar=true // foo will default to its zero value +// +// The fastest way to learn 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 import ( From cd9f5188a8b221ea732e8146a5b8fc75c0498d91 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sun, 1 Nov 2015 11:40:09 -0800 Subject: [PATCH 19/63] fix example --- example/example.go | 14 -------------- parse.go | 30 +++++++++++++++--------------- 2 files changed, 15 insertions(+), 29 deletions(-) delete mode 100644 example/example.go diff --git a/example/example.go b/example/example.go deleted file mode 100644 index 1b1c7e0..0000000 --- a/example/example.go +++ /dev/null @@ -1,14 +0,0 @@ -package main - -import "github.com/alexflint/go-arg" - -func main() { - 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) -} diff --git a/parse.go b/parse.go index 6e92633..f8e8cf4 100644 --- a/parse.go +++ b/parse.go @@ -1,17 +1,17 @@ // Package arg parses command line arguments using the fields from a struct. // Any exported field is interpreted as a command line option, so // -// var args struct { -// Iter int -// Debug bool -// } -// arg.MustParse(&args) +// 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 --bar // bar is a boolean flag so its value is optional -// ./example -iter 1 // bar will default to its zero value -// ./example --bar=true // foo will default to its zero value +// ./example --iter=1 --bar // bar is a boolean flag so its value is optional +// ./example -iter 1 // bar will default to its zero value +// ./example --bar=true // foo will default to its zero value // // The fastest way to learn how to use go-arg is to read the examples below. // @@ -20,13 +20,13 @@ // // 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:"-"` -// } +// 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`), From f042ab6386919a178d014ea8b3cd958e39148a7a Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sun, 1 Nov 2015 13:24:35 -0800 Subject: [PATCH 20/63] add .travis.yml --- .travis.yml | 7 +++++++ example_test.go | 2 +- parse.go | 5 +++-- 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2097b79 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: go +go: + - 1.4 + - tip +install: + - go get ./... + - go get -t ./... diff --git a/example_test.go b/example_test.go index d2febf6..5e2a905 100644 --- a/example_test.go +++ b/example_test.go @@ -74,7 +74,7 @@ func Example_MultipleValues() { } // This example shows the usage string generated by go-arg -func Example_Usage() { +func Example_UsageString() { // These are the args you would pass in on the command line os.Args = []string{"--help"} diff --git a/parse.go b/parse.go index f8e8cf4..f9c4e62 100644 --- a/parse.go +++ b/parse.go @@ -1,5 +1,6 @@ // Package arg parses command line arguments using the fields from a struct. -// Any exported field is interpreted as a command line option, so +// +// For example, // // var args struct { // Iter int @@ -9,7 +10,7 @@ // // defines two command line arguments, which can be set using any of // -// ./example --iter=1 --bar // bar is a boolean flag so its value is optional +// ./example --iter=1 --bar // bar is a boolean flag so its value is set to true // ./example -iter 1 // bar will default to its zero value // ./example --bar=true // foo will default to its zero value // From 3ff6f256dc7eeb83ecc6d3ad0541c741663e7c69 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sun, 1 Nov 2015 13:26:43 -0800 Subject: [PATCH 21/63] remove extra go gets in travis --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2097b79..ff2c2cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,3 @@ language: go go: - 1.4 - tip -install: - - go get ./... - - go get -t ./... From 8b5a16fafeb0ed4591889a11a5c0c4a7e149bb1e Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sun, 1 Nov 2015 13:36:14 -0800 Subject: [PATCH 22/63] fix examples --- example_test.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/example_test.go b/example_test.go index 5e2a905..bdba6ac 100644 --- a/example_test.go +++ b/example_test.go @@ -3,8 +3,6 @@ package arg import ( "fmt" "os" - - "github.com/alexflint/go-arg" ) // This example demonstrates basic usage @@ -16,7 +14,7 @@ func Example_Basic() { Foo string Bar bool } - arg.MustParse(&args) + MustParse(&args) fmt.Println(args.Foo, args.Bar) } @@ -30,7 +28,7 @@ func Example_DefaultValues() { Bar bool } args.Foo = "default value" - arg.MustParse(&args) + MustParse(&args) fmt.Println(args.Foo, args.Bar) } @@ -43,7 +41,7 @@ func Example_RequiredArguments() { Foo string `arg:"required"` Bar bool } - arg.MustParse(&args) + MustParse(&args) } // This example demonstrates positional arguments @@ -55,7 +53,7 @@ func Example_PositionalArguments() { Input string `arg:"positional"` Output []string `arg:"positional"` } - arg.MustParse(&args) + MustParse(&args) fmt.Println("Input:", args.Input) fmt.Println("Output:", args.Output) } @@ -69,7 +67,7 @@ func Example_MultipleValues() { Database string IDs []int64 } - arg.MustParse(&args) + MustParse(&args) fmt.Printf("Fetching the following IDs from %s: %q", args.Database, args.IDs) } @@ -85,5 +83,5 @@ func Example_UsageString() { Dataset string `arg:"help:dataset to use"` Optimize int `arg:"-O,help:optimization level"` } - arg.MustParse(&args) + MustParse(&args) } From 4d271f53262262a5803db5cf5f43e6337d73d6d0 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sun, 1 Nov 2015 13:38:04 -0800 Subject: [PATCH 23/63] fix float test for go1.4 --- parse_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parse_test.go b/parse_test.go index 4037ef5..4eff473 100644 --- a/parse_test.go +++ b/parse_test.go @@ -40,7 +40,7 @@ func TestMixed(t *testing.T) { assert.Equal(t, 3, args.Bar) assert.Equal(t, uint(123), args.Baz) assert.Equal(t, true, args.Ham) - assert.Equal(t, 1.2, args.Spam) + assert.EqualValues(t, 1.2, args.Spam) } func TestRequired(t *testing.T) { From bcb41ba0489082c8b3b2bff9aece9047c3c375aa Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sun, 1 Nov 2015 13:40:27 -0800 Subject: [PATCH 24/63] add travis badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a97cff5..1a7377c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![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) ## Structured argument parsing for Go From 60f2612c0c59b979f71241405515e722bf6943f0 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sun, 1 Nov 2015 13:53:51 -0800 Subject: [PATCH 25/63] separate help into WriteUsage and WriteHelp --- README.md | 38 ++++++++++++++++++++++---------------- parse.go | 4 ++-- usage.go | 26 ++++++++++++++++++++------ 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 1a7377c..b3b5886 100644 --- a/README.md +++ b/README.md @@ -19,17 +19,6 @@ $ ./example --foo=hello --bar hello true ``` -### Default values - -```go -var args struct { - Foo string - Bar bool -} -args.Foo = "default value" -arg.MustParse(&args) -``` - ### Required arguments ```go @@ -40,6 +29,12 @@ var args struct { arg.MustParse(&args) ``` +```shell +$ ./example +usage: example --foo FOO [--bar] +error: --foo is required +``` + ### Positional arguments ```go @@ -79,11 +74,22 @@ positional arguments: output options: ---verbose, -v verbosity level ---dataset DATASET dataset to use ---optimize OPTIMIZE, -O OPTIMIZE - optimization level ---help, -h print this help message + --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 diff --git a/parse.go b/parse.go index f9c4e62..f94b3c8 100644 --- a/parse.go +++ b/parse.go @@ -64,11 +64,11 @@ func MustParse(dest ...interface{}) { p, err := NewParser(dest...) if err != nil { fmt.Println(err) - os.Exit(1) + os.Exit(-1) } err = p.Parse(os.Args[1:]) if err == ErrHelp { - p.WriteUsage(os.Stdout) + p.WriteHelp(os.Stdout) os.Exit(0) } if err != nil { diff --git a/usage.go b/usage.go index 689da73..dc3ef3c 100644 --- a/usage.go +++ b/usage.go @@ -11,9 +11,9 @@ import ( // Fail prints usage information to stdout and exits with non-zero status func (p *Parser) Fail(msg string) { - fmt.Println(msg) p.WriteUsage(os.Stdout) - os.Exit(1) + fmt.Println("error:", msg) + os.Exit(-1) } // WriteUsage writes usage information to the given writer @@ -29,7 +29,7 @@ func (p *Parser) WriteUsage(w io.Writer) { fmt.Fprintf(w, "usage: %s ", filepath.Base(os.Args[0])) - // write the option component of the one-line usage message + // write the option component of the usage message for _, spec := range options { if !spec.required { fmt.Fprint(w, "[") @@ -41,7 +41,7 @@ func (p *Parser) WriteUsage(w io.Writer) { fmt.Fprint(w, " ") } - // write the positional component of the one-line usage message + // write the positional component of the usage message for _, spec := range positionals { up := strings.ToUpper(spec.long) if spec.multiple { @@ -52,6 +52,20 @@ func (p *Parser) WriteUsage(w io.Writer) { fmt.Fprint(w, " ") } 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 { @@ -66,9 +80,9 @@ func (p *Parser) WriteUsage(w io.Writer) { fmt.Fprint(w, "\noptions:\n") const colWidth = 25 for _, spec := range options { - left := fmt.Sprint(synopsis(spec, "--"+spec.long)) + left := " " + synopsis(spec, "--"+spec.long) if spec.short != "" { - left += ", " + fmt.Sprint(synopsis(spec, "-"+spec.short)) + left += ", " + synopsis(spec, "-"+spec.short) } fmt.Print(left) if spec.help != "" { From 95bf6f25e0095ba2166439eb4e47aa08ddd5c49b Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sun, 1 Nov 2015 21:55:52 -0800 Subject: [PATCH 26/63] fix first example in docs - h/t eric somerlade --- parse.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/parse.go b/parse.go index f94b3c8..6f90611 100644 --- a/parse.go +++ b/parse.go @@ -10,11 +10,11 @@ // // defines two command line arguments, which can be set using any of // -// ./example --iter=1 --bar // bar is a boolean flag so its value is set to true -// ./example -iter 1 // bar will default to its zero value -// ./example --bar=true // foo will default to its zero value +// ./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 learn how to use go-arg is to read the examples below. +// 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. From 518564234843a9512210eb34d8900bb5d12b7c4c Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Tue, 3 Nov 2015 08:15:45 -0800 Subject: [PATCH 27/63] fix note on boolean flags in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b3b5886..27c85ec 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ https://godoc.org/github.com/alexflint/go-arg 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. Boolean arguments must have explicit values, so `./prog -debug=1` sets debug to true but `./myprog -debug` does not. +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. From 6a22722d8c8b9a240c64f83795857b5da85fc920 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Wed, 4 Nov 2015 09:12:41 -0800 Subject: [PATCH 28/63] add coveralls to .travis.yml --- .travis.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ff2c2cd..d4e2c01 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,9 @@ language: go go: - - 1.4 - 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 From 9111061915102e12f0e3481c3bf88aa23a3d7324 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Wed, 4 Nov 2015 09:47:58 -0800 Subject: [PATCH 29/63] add tests for usage info --- usage.go | 2 +- usage_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 usage_test.go diff --git a/usage.go b/usage.go index dc3ef3c..4852866 100644 --- a/usage.go +++ b/usage.go @@ -84,7 +84,7 @@ func (p *Parser) WriteHelp(w io.Writer) { if spec.short != "" { left += ", " + synopsis(spec, "-"+spec.short) } - fmt.Print(left) + fmt.Fprint(w, left) if spec.help != "" { if len(left)+2 < colWidth { fmt.Fprint(w, strings.Repeat(" ", colWidth-len(left))) diff --git a/usage_test.go b/usage_test.go new file mode 100644 index 0000000..6b8741d --- /dev/null +++ b/usage_test.go @@ -0,0 +1,46 @@ +package arg + +import ( + "bytes" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWriteUsage(t *testing.T) { + expectedUsage := "usage: example [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]] \n" + + expectedHelp := `usage: example [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]] + +positional arguments: + input + output + +options: + --verbose, -v verbosity level + --dataset DATASET dataset to use + --optimize OPTIMIZE, -O OPTIMIZE + optimization level +` + 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"` + } + 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()) +} From 70c56eff661a679ce7a9826e1d8062a524099355 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Wed, 4 Nov 2015 10:27:17 -0800 Subject: [PATCH 30/63] add more tests --- parse.go | 6 +- parse_test.go | 224 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+), 3 deletions(-) diff --git a/parse.go b/parse.go index 6f90611..923e749 100644 --- a/parse.go +++ b/parse.go @@ -333,10 +333,10 @@ func setSlice(dest reflect.Value, values []string) error { if err := setScalar(v.Elem(), s); err != nil { return err } - if ptr { - v = v.Addr() + if !ptr { + v = v.Elem() } - dest.Set(reflect.Append(dest, v.Elem())) + dest.Set(reflect.Append(dest, v)) } return nil } diff --git a/parse_test.go b/parse_test.go index 4eff473..eb6080c 100644 --- a/parse_test.go +++ b/parse_test.go @@ -1,6 +1,7 @@ package arg import ( + "os" "strings" "testing" @@ -69,6 +70,28 @@ func TestShortFlag(t *testing.T) { 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"` @@ -104,6 +127,19 @@ func TestPositional(t *testing.T) { 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"` @@ -132,3 +168,191 @@ func TestMultiple(t *testing.T) { 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"} + MustParse(&args) + assert.Equal(t, "bar", args.Foo) +} From 239d0c1c6fb1a30aabaf46ac937ad1e061dccd7d Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Wed, 4 Nov 2015 10:58:20 -0800 Subject: [PATCH 31/63] add coveralls badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 27c85ec..93f2574 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![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 From 383b8b84c14efaee9a36415704aaa8c22a167836 Mon Sep 17 00:00:00 2001 From: brettlangdon Date: Sat, 7 Nov 2015 09:39:23 -0500 Subject: [PATCH 32/63] Remove excess trailing whitespace from Usage generation --- usage.go | 8 +++++--- usage_test.go | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/usage.go b/usage.go index 4852866..723719a 100644 --- a/usage.go +++ b/usage.go @@ -27,10 +27,12 @@ func (p *Parser) WriteUsage(w io.Writer) { } } - fmt.Fprintf(w, "usage: %s ", filepath.Base(os.Args[0])) + 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, "[") } @@ -38,18 +40,18 @@ func (p *Parser) WriteUsage(w io.Writer) { if !spec.required { fmt.Fprint(w, "]") } - 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, " ") } fmt.Fprint(w, "\n") } diff --git a/usage_test.go b/usage_test.go index 6b8741d..83da1c1 100644 --- a/usage_test.go +++ b/usage_test.go @@ -10,9 +10,9 @@ import ( ) func TestWriteUsage(t *testing.T) { - expectedUsage := "usage: example [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]] \n" + expectedUsage := "usage: example [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]]\n" - expectedHelp := `usage: example [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]] + expectedHelp := `usage: example [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]] positional arguments: input From 935b2a1bd5710aede269d46752e59fba916abaea Mon Sep 17 00:00:00 2001 From: Fredrik Wallgren Date: Mon, 9 Nov 2015 10:27:15 +0100 Subject: [PATCH 33/63] Write usage message to stderr on error When the parsing of parameters/flags fails eg. when a required flag is missing, print the usage statement and error to stderr instead of stdout. --- usage.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usage.go b/usage.go index 4852866..a1ed5a2 100644 --- a/usage.go +++ b/usage.go @@ -9,9 +9,9 @@ import ( "strings" ) -// Fail prints usage information to stdout and exits with non-zero status +// Fail prints usage information to stderr and exits with non-zero status func (p *Parser) Fail(msg string) { - p.WriteUsage(os.Stdout) + p.WriteUsage(os.Stderr) fmt.Println("error:", msg) os.Exit(-1) } From df17f4df4588e6ff41b0ff3045382775d6a8f748 Mon Sep 17 00:00:00 2001 From: Fredrik Wallgren Date: Wed, 11 Nov 2015 10:29:01 +0100 Subject: [PATCH 34/63] Fix bug with error not being written to stderr Only the usage message was written to stderr, the error was written with the standard fmt.Println. --- usage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usage.go b/usage.go index 13134d0..a80c817 100644 --- a/usage.go +++ b/usage.go @@ -12,7 +12,7 @@ import ( // Fail prints usage information to stderr and exits with non-zero status func (p *Parser) Fail(msg string) { p.WriteUsage(os.Stderr) - fmt.Println("error:", msg) + fmt.Fprintln(os.Stderr, "error:", msg) os.Exit(-1) } From d6a447ed7c63971839fc22d3d0f1c623aa556d2f Mon Sep 17 00:00:00 2001 From: Fredrik Wallgren Date: Wed, 11 Nov 2015 14:08:28 +0100 Subject: [PATCH 35/63] Fix lint warning --- usage.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/usage.go b/usage.go index 13134d0..82962c8 100644 --- a/usage.go +++ b/usage.go @@ -103,7 +103,6 @@ func (p *Parser) WriteHelp(w io.Writer) { func synopsis(spec *spec, form string) string { if spec.dest.Kind() == reflect.Bool { return form - } else { - return form + " " + strings.ToUpper(spec.long) } + return form + " " + strings.ToUpper(spec.long) } From ab43eae565dcc5aefbd03cf0bbe1fb8ce5692290 Mon Sep 17 00:00:00 2001 From: Fredrik Wallgren Date: Wed, 11 Nov 2015 14:16:39 +0100 Subject: [PATCH 36/63] Move package documentation to doc.go --- doc.go | 36 ++++++++++++++++++++++++++++++++++++ parse.go | 35 ----------------------------------- 2 files changed, 36 insertions(+), 35 deletions(-) create mode 100644 doc.go 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/parse.go b/parse.go index 923e749..a9e509f 100644 --- a/parse.go +++ b/parse.go @@ -1,38 +1,3 @@ -// 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 import ( From 330a0da571888c4ec6dac345140a7ee663d00d6e Mon Sep 17 00:00:00 2001 From: Fredrik Wallgren Date: Wed, 11 Nov 2015 10:15:57 +0100 Subject: [PATCH 37/63] Add built ins to options in help output Adds help to the options in help output with an easy way to add more built ins. --- parse.go | 6 ++++++ usage.go | 44 ++++++++++++++++++++++++-------------------- usage_test.go | 1 + 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/parse.go b/parse.go index 923e749..8bb2c2d 100644 --- a/parse.go +++ b/parse.go @@ -54,6 +54,7 @@ type spec struct { positional bool help string wasPresent bool + isBool bool } // ErrHelp indicates that -h or --help were provided @@ -135,6 +136,11 @@ func NewParser(dests ...interface{}) (*Parser, error) { 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, ",") { diff --git a/usage.go b/usage.go index 824f0eb..9404015 100644 --- a/usage.go +++ b/usage.go @@ -5,7 +5,6 @@ import ( "io" "os" "path/filepath" - "reflect" "strings" ) @@ -78,30 +77,35 @@ func (p *Parser) WriteHelp(w io.Writer) { } // write the list of options - if len(options) > 0 { - fmt.Fprint(w, "\noptions:\n") - const colWidth = 25 - for _, spec := range options { - 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) - } - fmt.Fprint(w, "\n") + 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) { + const colWidth = 25 + 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) } + fmt.Fprint(w, "\n") } func synopsis(spec *spec, form string) string { - if spec.dest.Kind() == reflect.Bool { + if spec.isBool { return form } return form + " " + strings.ToUpper(spec.long) diff --git a/usage_test.go b/usage_test.go index 83da1c1..5a9199c 100644 --- a/usage_test.go +++ b/usage_test.go @@ -23,6 +23,7 @@ options: --dataset DATASET dataset to use --optimize OPTIMIZE, -O OPTIMIZE optimization level + --help, -h display this help and exit ` var args struct { Input string `arg:"positional"` From b0d37d1fb2b491a861f788a41aabbe66cbf83110 Mon Sep 17 00:00:00 2001 From: Fredrik Wallgren Date: Thu, 19 Nov 2015 14:34:09 +0100 Subject: [PATCH 38/63] Add default values to usage Check if the value isn't it's zero value and if not add a default value to the usage text. --- usage.go | 8 +++++++- usage_test.go | 10 ++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/usage.go b/usage.go index 9404015..64ed901 100644 --- a/usage.go +++ b/usage.go @@ -76,7 +76,6 @@ func (p *Parser) WriteHelp(w io.Writer) { } } - // write the list of options fmt.Fprint(w, "\noptions:\n") for _, spec := range options { printOption(w, spec) @@ -101,6 +100,13 @@ func printOption(w io.Writer, spec *spec) { } 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 + z := reflect.Zero(v.Type()) + if v.Interface() != z.Interface() { + fmt.Fprintf(w, " [default: %v]", v) + } fmt.Fprint(w, "\n") } diff --git a/usage_test.go b/usage_test.go index 5a9199c..01c9542 100644 --- a/usage_test.go +++ b/usage_test.go @@ -10,15 +10,17 @@ import ( ) func TestWriteUsage(t *testing.T) { - expectedUsage := "usage: example [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]]\n" + expectedUsage := "usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]]\n" - expectedHelp := `usage: example [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]] + expectedHelp := `usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]] positional arguments: input output 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 @@ -28,10 +30,14 @@ options: var args struct { Input string `arg:"positional"` Output []string `arg:"positional"` + 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"` } + args.Name = "Foo Bar" + args.Value = 42 p, err := NewParser(&args) require.NoError(t, err) From d45bd4523c98b9190de5cd7a43ee32087451fe5f Mon Sep 17 00:00:00 2001 From: brettlangdon Date: Sat, 21 Nov 2015 18:59:40 -0500 Subject: [PATCH 39/63] Display help text for positional arguments --- usage.go | 15 ++++++++++++++- usage_test.go | 27 +++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/usage.go b/usage.go index 9404015..2c38fe7 100644 --- a/usage.go +++ b/usage.go @@ -68,11 +68,24 @@ func (p *Parser) WriteHelp(w io.Writer) { p.WriteUsage(w) + // the width of the left column + const colWidth = 25 + // write the list of positionals if len(positionals) > 0 { fmt.Fprint(w, "\npositional arguments:\n") for _, spec := range positionals { - fmt.Fprintf(w, " %s\n", spec.long) + 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") } } diff --git a/usage_test.go b/usage_test.go index 5a9199c..4a56e6e 100644 --- a/usage_test.go +++ b/usage_test.go @@ -16,7 +16,7 @@ func TestWriteUsage(t *testing.T) { positional arguments: input - output + output positional output options: --verbose, -v verbosity level @@ -27,7 +27,7 @@ options: ` var args struct { Input string `arg:"positional"` - Output []string `arg:"positional"` + Output []string `arg:"positional,help:positional output"` Verbose bool `arg:"-v,help:verbosity level"` Dataset string `arg:"help:dataset to use"` Optimize int `arg:"-O,help:optimization level"` @@ -45,3 +45,26 @@ options: 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()) +} From 670c7b787d9a475c663e3868eeb226ff7c5e78ad Mon Sep 17 00:00:00 2001 From: Fredrik Wallgren Date: Sun, 22 Nov 2015 00:58:27 +0100 Subject: [PATCH 40/63] Fix merge conflicts --- usage.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/usage.go b/usage.go index 64ed901..fa01229 100644 --- a/usage.go +++ b/usage.go @@ -5,6 +5,7 @@ import ( "io" "os" "path/filepath" + "reflect" "strings" ) @@ -76,6 +77,7 @@ func (p *Parser) WriteHelp(w io.Writer) { } } + // write the list of options fmt.Fprint(w, "\noptions:\n") for _, spec := range options { printOption(w, spec) @@ -103,9 +105,11 @@ func printOption(w io.Writer, spec *spec) { // Check if spec.dest is zero value or not // If it isn't a default value have been added v := spec.dest - z := reflect.Zero(v.Type()) - if v.Interface() != z.Interface() { - fmt.Fprintf(w, " [default: %v]", v) + if v.IsValid() { + z := reflect.Zero(v.Type()) + if v.Interface() != z.Interface() { + fmt.Fprintf(w, " [default: %v]", v) + } } fmt.Fprint(w, "\n") } From e4e9e194271bfee0bc5e60b2f0cb7eee667b10b0 Mon Sep 17 00:00:00 2001 From: Alex Rakoczy Date: Fri, 4 Dec 2015 09:59:13 -0500 Subject: [PATCH 41/63] Fix error when printing usage for multi-value arguments We try to compare []strings, which are uncomparable types: `panic: runtime error: comparing uncomparable type []string` --- usage.go | 2 +- usage_test.go | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/usage.go b/usage.go index fa01229..23f7aa5 100644 --- a/usage.go +++ b/usage.go @@ -107,7 +107,7 @@ func printOption(w io.Writer, spec *spec) { v := spec.dest if v.IsValid() { z := reflect.Zero(v.Type()) - if v.Interface() != z.Interface() { + if v.Type().Comparable() && z.Type().Comparable() && v.Interface() != z.Interface() { fmt.Fprintf(w, " [default: %v]", v) } } diff --git a/usage_test.go b/usage_test.go index 01c9542..07edc18 100644 --- a/usage_test.go +++ b/usage_test.go @@ -10,9 +10,9 @@ import ( ) func TestWriteUsage(t *testing.T) { - expectedUsage := "usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]]\n" + expectedUsage := "usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] INPUT [OUTPUT [OUTPUT ...]]\n" - expectedHelp := `usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]] + expectedHelp := `usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] INPUT [OUTPUT [OUTPUT ...]] positional arguments: input @@ -25,6 +25,7 @@ options: --dataset DATASET dataset to use --optimize OPTIMIZE, -O OPTIMIZE optimization level + --ids IDS Ids --help, -h display this help and exit ` var args struct { @@ -35,6 +36,7 @@ options: 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"` } args.Name = "Foo Bar" args.Value = 42 From 0c0f9a53aceb964ef68ea55c57a36a9e374f00e8 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Tue, 5 Jan 2016 13:52:33 -0800 Subject: [PATCH 42/63] MustParse returns *Parser --- parse.go | 3 ++- parse_test.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/parse.go b/parse.go index b6eae4e..219a947 100644 --- a/parse.go +++ b/parse.go @@ -26,7 +26,7 @@ type spec struct { var ErrHelp = errors.New("help requested by user") // MustParse processes command line arguments and exits upon failure -func MustParse(dest ...interface{}) { +func MustParse(dest ...interface{}) *Parser { p, err := NewParser(dest...) if err != nil { fmt.Println(err) @@ -40,6 +40,7 @@ func MustParse(dest ...interface{}) { if err != nil { p.Fail(err.Error()) } + return p } // Parse processes command line arguments and stores them in dest diff --git a/parse_test.go b/parse_test.go index eb6080c..7fca76a 100644 --- a/parse_test.go +++ b/parse_test.go @@ -353,6 +353,7 @@ func TestMustParse(t *testing.T) { Foo string } os.Args = []string{"example", "--foo", "bar"} - MustParse(&args) + parser := MustParse(&args) assert.Equal(t, "bar", args.Foo) + assert.NotNil(t, parser) } From f89698667c0b2138445c719101fe7a4d55764738 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Tue, 5 Jan 2016 13:57:01 -0800 Subject: [PATCH 43/63] add custom validation example to README --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 93f2574..4a11914 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,18 @@ fmt.Printf("Fetching the following IDs from %s: %q", args.Database, args.IDs) Fetching the following IDs from foo: [1 2 3] ``` +### Custom validation +``` +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) +} +``` + ### Installation ```shell From 9aad09fe14abea44e32a2dabf4768e1eb31527ea Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Tue, 5 Jan 2016 14:00:29 -0800 Subject: [PATCH 44/63] fix example code --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4a11914..f4c8d11 100644 --- a/README.md +++ b/README.md @@ -109,17 +109,23 @@ 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) + 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 From 4197d283e41bbce1a2cac20ea56fd1156a677db1 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Mon, 18 Jan 2016 08:24:21 -0800 Subject: [PATCH 45/63] extract common colWidth constant --- usage.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/usage.go b/usage.go index 65f7c2d..61f0ad6 100644 --- a/usage.go +++ b/usage.go @@ -9,6 +9,9 @@ import ( "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) @@ -69,9 +72,6 @@ func (p *Parser) WriteHelp(w io.Writer) { p.WriteUsage(w) - // the width of the left column - const colWidth = 25 - // write the list of positionals if len(positionals) > 0 { fmt.Fprint(w, "\npositional arguments:\n") @@ -101,7 +101,6 @@ func (p *Parser) WriteHelp(w io.Writer) { } func printOption(w io.Writer, spec *spec) { - const colWidth = 25 left := " " + synopsis(spec, "--"+spec.long) if spec.short != "" { left += ", " + synopsis(spec, "-"+spec.short) From b1ec8c909335d0a72b6887aea5e7428f3cff60a8 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Mon, 18 Jan 2016 10:31:01 -0800 Subject: [PATCH 46/63] make it possible to override the name of the program --- parse.go | 26 +++++++++++++++++++++----- parse_test.go | 2 +- usage.go | 6 ++---- usage_test.go | 22 ++++++++++++++++++++-- 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/parse.go b/parse.go index 219a947..32b9b9d 100644 --- a/parse.go +++ b/parse.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "reflect" "strconv" "strings" @@ -27,7 +28,7 @@ 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...) + p, err := NewParser(Config{}, dest...) if err != nil { fmt.Println(err) os.Exit(-1) @@ -45,20 +46,26 @@ func MustParse(dest ...interface{}) *Parser { // Parse processes command line arguments and stores them in dest func Parse(dest ...interface{}) error { - p, err := NewParser(dest...) + 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 + spec []*spec + config Config } // NewParser constructs a parser from a list of destination structs -func NewParser(dests ...interface{}) (*Parser, error) { +func NewParser(config Config, dests ...interface{}) (*Parser, error) { var specs []*spec for _, dest := range dests { v := reflect.ValueOf(dest) @@ -138,7 +145,16 @@ func NewParser(dests ...interface{}) (*Parser, error) { specs = append(specs, &spec) } } - return &Parser{spec: specs}, nil + if config.Program == "" { + config.Program = "program" + if len(os.Args) > 0 { + config.Program = filepath.Base(os.Args[0]) + } + } + return &Parser{ + spec: specs, + config: config, + }, nil } // Parse processes the given command line option, storing the results in the field diff --git a/parse_test.go b/parse_test.go index 7fca76a..1189588 100644 --- a/parse_test.go +++ b/parse_test.go @@ -10,7 +10,7 @@ import ( ) func parse(cmdline string, dest interface{}) error { - p, err := NewParser(dest) + p, err := NewParser(Config{}, dest) if err != nil { return err } diff --git a/usage.go b/usage.go index 61f0ad6..f9d2eb9 100644 --- a/usage.go +++ b/usage.go @@ -4,7 +4,6 @@ import ( "fmt" "io" "os" - "path/filepath" "reflect" "strings" ) @@ -30,7 +29,7 @@ func (p *Parser) WriteUsage(w io.Writer) { } } - fmt.Fprintf(w, "usage: %s", filepath.Base(os.Args[0])) + fmt.Fprintf(w, "usage: %s", p.config.Program) // write the option component of the usage message for _, spec := range options { @@ -114,8 +113,7 @@ func printOption(w io.Writer, spec *spec) { } fmt.Fprint(w, spec.help) } - // Check if spec.dest is zero value or not - // If it isn't a default value have been added + // 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()) diff --git a/usage_test.go b/usage_test.go index 2375e81..130cd45 100644 --- a/usage_test.go +++ b/usage_test.go @@ -40,7 +40,7 @@ options: } args.Name = "Foo Bar" args.Value = 42 - p, err := NewParser(&args) + p, err := NewParser(Config{}, &args) require.NoError(t, err) os.Args[0] = "example" @@ -68,7 +68,25 @@ options: VeryLongPositionalWithHelp string `arg:"positional,help:this positional argument is very long"` } - p, err := NewParser(&args) + 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" From 8dd29d34bf0186945d53ba1ca0cde2324952a6e9 Mon Sep 17 00:00:00 2001 From: brettlangdon Date: Mon, 18 Jan 2016 13:42:04 -0500 Subject: [PATCH 47/63] Add support for environment variables --- README.md | 35 +++++++++++++++++++++++++++++++++++ parse.go | 17 +++++++++++++++++ parse_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ usage_test.go | 7 +++++-- 4 files changed, 107 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f4c8d11..3d1d12f 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,41 @@ 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 { diff --git a/parse.go b/parse.go index 219a947..ce3892f 100644 --- a/parse.go +++ b/parse.go @@ -18,6 +18,7 @@ type spec struct { required bool positional bool help string + env string wasPresent bool isBool bool } @@ -130,6 +131,13 @@ func NewParser(dests ...interface{}) (*Parser, error) { 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) } @@ -179,6 +187,15 @@ func process(specs []*spec, args []string) error { 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 diff --git a/parse_test.go b/parse_test.go index 7fca76a..f3e7350 100644 --- a/parse_test.go +++ b/parse_test.go @@ -357,3 +357,53 @@ func TestMustParse(t *testing.T) { 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_test.go b/usage_test.go index 2375e81..255018d 100644 --- a/usage_test.go +++ b/usage_test.go @@ -10,9 +10,9 @@ import ( ) func TestWriteUsage(t *testing.T) { - expectedUsage := "usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] INPUT [OUTPUT [OUTPUT ...]]\n" + 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] INPUT [OUTPUT [OUTPUT ...]] + expectedHelp := `usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--workers WORKERS] INPUT [OUTPUT [OUTPUT ...]] positional arguments: input @@ -26,6 +26,8 @@ options: --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 { @@ -37,6 +39,7 @@ options: 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 From ed2b19f2bbf787a888bf02eb707a17f4878f4109 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 23 Jan 2016 18:28:35 -0800 Subject: [PATCH 48/63] add support for time.Duration fields --- parse.go | 40 -------------------------------- parse_test.go | 39 ++++++++++++++++++++++++++++++- scalar.go | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 41 deletions(-) create mode 100644 scalar.go diff --git a/parse.go b/parse.go index ce3892f..39eb52c 100644 --- a/parse.go +++ b/parse.go @@ -5,7 +5,6 @@ import ( "fmt" "os" "reflect" - "strconv" "strings" ) @@ -329,42 +328,3 @@ func setSlice(dest reflect.Value, values []string) error { } 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 index f3e7350..5e9baf2 100644 --- a/parse_test.go +++ b/parse_test.go @@ -4,6 +4,7 @@ import ( "os" "strings" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -17,7 +18,7 @@ func parse(cmdline string, dest interface{}) error { return p.Parse(strings.Split(cmdline, " ")) } -func TestStringSingle(t *testing.T) { +func TestString(t *testing.T) { var args struct { Foo string } @@ -26,6 +27,42 @@ func TestStringSingle(t *testing.T) { 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 TestMixed(t *testing.T) { var args struct { Foo string `arg:"-f"` diff --git a/scalar.go b/scalar.go new file mode 100644 index 0000000..a3bafe4 --- /dev/null +++ b/scalar.go @@ -0,0 +1,63 @@ +package arg + +import ( + "encoding" + "fmt" + "reflect" + "strconv" + "time" +) + +var ( + durationType = reflect.TypeOf(time.Duration(0)) + textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem() +) + +// 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 time.Duration then use time.ParseDuration + if v.Type() == durationType { + x, err := time.ParseDuration(s) + if err != nil { + return err + } + v.Set(reflect.ValueOf(x)) + return nil + } + + 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("not a scalar type: %s", v.Kind()) + } + return nil +} From 64a4bab5506099047596a99d4a9b71de8d69798e Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 23 Jan 2016 18:35:08 -0800 Subject: [PATCH 49/63] add test for invalid durations --- parse_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/parse_test.go b/parse_test.go index 5e9baf2..c30809d 100644 --- a/parse_test.go +++ b/parse_test.go @@ -63,6 +63,14 @@ func TestDuration(t *testing.T) { 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 TestMixed(t *testing.T) { var args struct { Foo string `arg:"-f"` From 865cc5a973b8802737e029c1fc962dc0748fc549 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 23 Jan 2016 19:40:15 -0800 Subject: [PATCH 50/63] add support for pointers and TextUnmarshaler --- parse.go | 57 +++++++++++++++++++++----------- parse_test.go | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++- scalar.go | 31 +++++++++++++----- 3 files changed, 150 insertions(+), 29 deletions(-) diff --git a/parse.go b/parse.go index 39eb52c..3895ce9 100644 --- a/parse.go +++ b/parse.go @@ -1,6 +1,7 @@ package arg import ( + "encoding" "errors" "fmt" "os" @@ -20,11 +21,15 @@ type spec struct { env string wasPresent bool isBool bool + fieldName string // for generating helpful errors } // ErrHelp indicates that -h or --help were provided var ErrHelp = errors.New("help requested by user") +// The TextUnmarshaler type in reflection form +var textUnsmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem() + // MustParse processes command line arguments and exits upon failure func MustParse(dest ...interface{}) *Parser { p, err := NewParser(dest...) @@ -80,31 +85,42 @@ func NewParser(dests ...interface{}) (*Parser, error) { } spec := spec{ - long: strings.ToLower(field.Name), - dest: v.Field(i), + long: strings.ToLower(field.Name), + dest: v.Field(i), + fieldName: t.Name() + "." + field.Name, } - // Get the scalar type for this field - scalarType := field.Type - if scalarType.Kind() == reflect.Slice { - spec.multiple = true - scalarType = scalarType.Elem() + // 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 recieved happend + // to exercise those fields. + if !field.Type.Implements(textUnsmarshalerType) { + scalarType := field.Type + // Look inside pointer types + if scalarType.Kind() == reflect.Ptr { + scalarType = scalarType.Elem() + } + // Check for bool + if scalarType.Kind() == reflect.Bool { + spec.isBool = true + } + // Look inside slice types + if scalarType.Kind() == reflect.Slice { + spec.multiple = true + scalarType = scalarType.Elem() + } + // Look inside pointer types (again, in case of []*Type) 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 + // 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()) + } } // Look at the tag @@ -248,7 +264,8 @@ func process(specs []*spec, args []string) error { } // if it's a flag and it has no value then set the value to true - if spec.dest.Kind() == reflect.Bool && value == "" { + // use isBool because this takes account of TextUnmarshaler + if spec.isBool && value == "" { value = "true" } diff --git a/parse_test.go b/parse_test.go index c30809d..a915910 100644 --- a/parse_test.go +++ b/parse_test.go @@ -15,7 +15,11 @@ func parse(cmdline string, dest interface{}) error { if err != nil { return err } - return p.Parse(strings.Split(cmdline, " ")) + var parts []string + if len(cmdline) > 0 { + parts = strings.Split(cmdline, " ") + } + return p.Parse(parts) } func TestString(t *testing.T) { @@ -71,6 +75,25 @@ func TestInvalidDuration(t *testing.T) { 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"` @@ -359,6 +382,14 @@ func TestUnsupportedType(t *testing.T) { } 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{} } @@ -452,3 +483,61 @@ func TestEnvironmentVariableRequired(t *testing.T) { 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) +} diff --git a/scalar.go b/scalar.go index a3bafe4..67b4540 100644 --- a/scalar.go +++ b/scalar.go @@ -8,19 +8,33 @@ import ( "time" ) -var ( - durationType = reflect.TypeOf(time.Duration(0)) - textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem() -) - // 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 time.Duration then use time.ParseDuration - if v.Type() == durationType { + // 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: x, err := time.ParseDuration(s) if err != nil { return err @@ -29,6 +43,7 @@ func setScalar(v reflect.Value, s string) error { return nil } + // Switch on kind so that we can handle derived types switch v.Kind() { case reflect.String: v.SetString(s) @@ -57,7 +72,7 @@ func setScalar(v reflect.Value, s string) error { } v.SetFloat(x) default: - return fmt.Errorf("not a scalar type: %s", v.Kind()) + return fmt.Errorf("cannot parse argument into %s", v.Type().String()) } return nil } From 95761fa14ae625b1929f9b43403e4e1a4910e192 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 23 Jan 2016 20:08:00 -0800 Subject: [PATCH 51/63] update readme with new additions --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3d1d12f..ae7aa6c 100644 --- a/README.md +++ b/README.md @@ -24,16 +24,16 @@ hello true ```go var args struct { - Foo string `arg:"required"` - Bar bool + ID int `arg:"required"` + Timeout time.Duration } arg.MustParse(&args) ``` ```shell $ ./example -usage: example --foo FOO [--bar] -error: --foo is required +usage: example --id ID [--timeout TIMEOUT] +error: --id is required ``` ### Positional arguments @@ -161,6 +161,53 @@ usage: samples [--foo FOO] [--bar BAR] error: you must provide one of --foo and --bar ``` +### 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" +``` + ### Installation ```shell From 8fee8f7bbe5933cfe3ba8c82479b91a8e777e5a0 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 23 Jan 2016 20:11:51 -0800 Subject: [PATCH 52/63] move installation instructions to top --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ae7aa6c..28ff388 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ ## 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 @@ -208,12 +212,6 @@ usage: example [--name NAME] error: error processing --name: missing period in "oops" ``` -### Installation - -```shell -go get github.com/alexflint/go-arg -``` - ### Documentation https://godoc.org/github.com/alexflint/go-arg From e389d7f782c50124fbac2f2bbcb4c5794f8e2f44 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 23 Jan 2016 20:49:57 -0800 Subject: [PATCH 53/63] add support for IP address, email address, and MAC address --- parse.go | 76 ++++++++++++++++++++++++++++++------------------------- scalar.go | 68 +++++++++++++++++++++++++++++++++++++++++++++++-- usage.go | 4 +-- 3 files changed, 110 insertions(+), 38 deletions(-) diff --git a/parse.go b/parse.go index 3895ce9..6768699 100644 --- a/parse.go +++ b/parse.go @@ -1,7 +1,6 @@ package arg import ( - "encoding" "errors" "fmt" "os" @@ -20,16 +19,13 @@ type spec struct { help string env string wasPresent bool - isBool bool + boolean bool fieldName string // for generating helpful errors } // ErrHelp indicates that -h or --help were provided var ErrHelp = errors.New("help requested by user") -// The TextUnmarshaler type in reflection form -var textUnsmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem() - // MustParse processes command line arguments and exits upon failure func MustParse(dest ...interface{}) *Parser { p, err := NewParser(dest...) @@ -94,33 +90,10 @@ func NewParser(dests ...interface{}) (*Parser, error) { // wait until setScalar because it means that a program with invalid argument // fields will always fail regardless of whether the arguments it recieved happend // to exercise those fields. - if !field.Type.Implements(textUnsmarshalerType) { - scalarType := field.Type - // Look inside pointer types - if scalarType.Kind() == reflect.Ptr { - scalarType = scalarType.Elem() - } - // Check for bool - if scalarType.Kind() == reflect.Bool { - spec.isBool = true - } - // Look inside slice types - if scalarType.Kind() == reflect.Slice { - spec.multiple = true - scalarType = scalarType.Elem() - } - // Look inside pointer types (again, in case of []*Type) - 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()) - } + var parseable bool + parseable, spec.boolean, spec.multiple = canParse(field.Type) + if !parseable { + return nil, fmt.Errorf("%s.%s: %s fields are not supported", t.Name(), field.Name, field.Type.String()) } // Look at the tag @@ -264,8 +237,8 @@ func process(specs []*spec, args []string) error { } // if it's a flag and it has no value then set the value to true - // use isBool because this takes account of TextUnmarshaler - if spec.isBool && value == "" { + // use boolean because this takes account of TextUnmarshaler + if spec.boolean && value == "" { value = "true" } @@ -345,3 +318,38 @@ func setSlice(dest reflect.Value, values []string) error { } 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 +} diff --git a/scalar.go b/scalar.go index 67b4540..ac56978 100644 --- a/scalar.go +++ b/scalar.go @@ -3,11 +3,57 @@ 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() { @@ -35,12 +81,30 @@ func setScalar(v reflect.Value, s string) error { // Switch on concrete type switch scalar.(type) { case time.Duration: - x, err := time.ParseDuration(s) + duration, err := time.ParseDuration(s) if err != nil { return err } - v.Set(reflect.ValueOf(x)) + 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)) + case net.IP: + ip := net.ParseIP(s) + if ip == nil { + return fmt.Errorf(`invalid IP address: "%s"`, s) + } + v.Set(reflect.ValueOf(ip)) + case net.HardwareAddr: + ip, err := net.ParseMAC(s) + if err != nil { + return err + } + v.Set(reflect.ValueOf(ip)) } // Switch on kind so that we can handle derived types diff --git a/usage.go b/usage.go index 61f0ad6..eea6d29 100644 --- a/usage.go +++ b/usage.go @@ -97,7 +97,7 @@ func (p *Parser) WriteHelp(w io.Writer) { } // write the list of built in options - printOption(w, &spec{isBool: true, long: "help", short: "h", help: "display this help and exit"}) + printOption(w, &spec{boolean: true, long: "help", short: "h", help: "display this help and exit"}) } func printOption(w io.Writer, spec *spec) { @@ -127,7 +127,7 @@ func printOption(w io.Writer, spec *spec) { } func synopsis(spec *spec, form string) string { - if spec.isBool { + if spec.boolean { return form } return form + " " + strings.ToUpper(spec.long) From 9a30acda0542a376f35ce2fc0cc166d9ac48c709 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 23 Jan 2016 20:55:40 -0800 Subject: [PATCH 54/63] added tests for IP address parsing --- parse_test.go | 38 ++++++++++++++++++++++++++++++++++++++ scalar.go | 3 +++ 2 files changed, 41 insertions(+) diff --git a/parse_test.go b/parse_test.go index a915910..5714ebf 100644 --- a/parse_test.go +++ b/parse_test.go @@ -1,6 +1,7 @@ package arg import ( + "net" "os" "strings" "testing" @@ -541,3 +542,40 @@ func TestSliceUnmarhsaler(t *testing.T) { 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) +} diff --git a/scalar.go b/scalar.go index ac56978..e79b002 100644 --- a/scalar.go +++ b/scalar.go @@ -93,18 +93,21 @@ func setScalar(v reflect.Value, s string) error { 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 From c9584269b970b94251033e860d49b305198ca730 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 23 Jan 2016 20:58:43 -0800 Subject: [PATCH 55/63] added tests for MAC and email addresses --- parse_test.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/parse_test.go b/parse_test.go index 5714ebf..e33fe76 100644 --- a/parse_test.go +++ b/parse_test.go @@ -2,6 +2,7 @@ package arg import ( "net" + "net/mail" "os" "strings" "testing" @@ -579,3 +580,37 @@ func TestInvalidIPAddress(t *testing.T) { 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, "", args.Recipient.String()) +} + +func TestInvalidMailAddr(t *testing.T) { + var args struct { + Recipient mail.Address + } + err := parse("--recipient xxx", &args) + assert.Error(t, err) +} From 1488562b1ebdb57ebdc74e640900153fb624b2e6 Mon Sep 17 00:00:00 2001 From: Fredrik Wallgren Date: Mon, 29 Feb 2016 22:05:26 +0100 Subject: [PATCH 56/63] Allow override of defaults for slice arguments This commit fixes a bug where if a multiple value argument (slice) has default values, the submitted values will be appended to the default. Not overriding them as expected. --- parse.go | 5 +++++ parse_test.go | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/parse.go b/parse.go index c959656..b08a298 100644 --- a/parse.go +++ b/parse.go @@ -322,6 +322,11 @@ func setSlice(dest reflect.Value, values []string) error { 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 { diff --git a/parse_test.go b/parse_test.go index 964c9a7..8ee6a79 100644 --- a/parse_test.go +++ b/parse_test.go @@ -250,6 +250,19 @@ func TestMultipleWithEq(t *testing.T) { 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 From e71d6514f40a7e4b99825987b408c421ced3e13f Mon Sep 17 00:00:00 2001 From: Fredrik Wallgren Date: Sun, 6 Mar 2016 21:07:01 +0100 Subject: [PATCH 57/63] Print defaults for multiples Check if the default value supplied is a slice and not nil, if so print the list of values supplied. Test case for slice argument with and without default values. Default values for slices was not printed because slice is not comparable, but the zero value for slices is nil. --- usage.go | 2 +- usage_test.go | 25 ++++++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/usage.go b/usage.go index 9e9ce77..2ee3953 100644 --- a/usage.go +++ b/usage.go @@ -117,7 +117,7 @@ func printOption(w io.Writer, spec *spec) { v := spec.dest if v.IsValid() { z := reflect.Zero(v.Type()) - if v.Type().Comparable() && z.Type().Comparable() && v.Interface() != z.Interface() { + if (v.Type().Comparable() && z.Type().Comparable() && v.Interface() != z.Interface()) || v.Kind() == reflect.Slice && !v.IsNil() { fmt.Fprintf(w, " [default: %v]", v) } } diff --git a/usage_test.go b/usage_test.go index fd2ba3a..b63a7d0 100644 --- a/usage_test.go +++ b/usage_test.go @@ -10,9 +10,9 @@ import ( ) 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" + 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] [--workers WORKERS] INPUT [OUTPUT [OUTPUT ...]] + expectedHelp := `usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] INPUT [OUTPUT [OUTPUT ...]] positional arguments: input @@ -26,23 +26,26 @@ options: --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"` - Workers int `arg:"-w,env:WORKERS,help:number of workers to start"` + 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) From 5800b89ce9817f99b805776fe86046e2a26dc536 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sun, 31 Jul 2016 09:14:44 -0700 Subject: [PATCH 58/63] fix example function names --- example_test.go | 12 ++++++------ parse.go | 6 +++--- parse_test.go | 16 +++++++++++----- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/example_test.go b/example_test.go index bdba6ac..6fb5197 100644 --- a/example_test.go +++ b/example_test.go @@ -6,7 +6,7 @@ import ( ) // This example demonstrates basic usage -func Example_Basic() { +func Example() { // These are the args you would pass in on the command line os.Args = []string{"./example", "--foo=hello", "--bar"} @@ -19,7 +19,7 @@ func Example_Basic() { } // This example demonstrates arguments that have default values -func Example_DefaultValues() { +func Example_defaultValues() { // These are the args you would pass in on the command line os.Args = []string{"--help"} @@ -33,7 +33,7 @@ func Example_DefaultValues() { } // This example demonstrates arguments that are required -func Example_RequiredArguments() { +func Example_requiredArguments() { // These are the args you would pass in on the command line os.Args = []string{"--foo=1", "--bar"} @@ -45,7 +45,7 @@ func Example_RequiredArguments() { } // This example demonstrates positional arguments -func Example_PositionalArguments() { +func Example_positionalArguments() { // These are the args you would pass in on the command line os.Args = []string{"./example", "in", "out1", "out2", "out3"} @@ -59,7 +59,7 @@ func Example_PositionalArguments() { } // This example demonstrates arguments that have multiple values -func Example_MultipleValues() { +func Example_multipleValues() { // The args you would pass in on the command line os.Args = []string{"--help"} @@ -72,7 +72,7 @@ func Example_MultipleValues() { } // This example shows the usage string generated by go-arg -func Example_UsageString() { +func Example_usageString() { // These are the args you would pass in on the command line os.Args = []string{"--help"} diff --git a/parse.go b/parse.go index b08a298..b1193d0 100644 --- a/parse.go +++ b/parse.go @@ -95,8 +95,8 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) { // 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 recieved happend - // to exercise those fields. + // 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 { @@ -309,7 +309,7 @@ func validate(spec []*spec) error { return nil } -// parse a value as the apropriate type and store it in the struct +// 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") diff --git a/parse_test.go b/parse_test.go index 8ee6a79..ab0cfd7 100644 --- a/parse_test.go +++ b/parse_test.go @@ -12,6 +12,12 @@ import ( "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 { @@ -453,7 +459,7 @@ func TestEnvironmentVariable(t *testing.T) { var args struct { Foo string `arg:"env"` } - os.Setenv("FOO", "bar") + setenv(t, "FOO", "bar") os.Args = []string{"example"} MustParse(&args) assert.Equal(t, "bar", args.Foo) @@ -463,7 +469,7 @@ func TestEnvironmentVariableOverrideName(t *testing.T) { var args struct { Foo string `arg:"env:BAZ"` } - os.Setenv("BAZ", "bar") + setenv(t, "BAZ", "bar") os.Args = []string{"example"} MustParse(&args) assert.Equal(t, "bar", args.Foo) @@ -473,7 +479,7 @@ func TestEnvironmentVariableOverrideArgument(t *testing.T) { var args struct { Foo string `arg:"env"` } - os.Setenv("FOO", "bar") + setenv(t, "FOO", "bar") os.Args = []string{"example", "--foo", "baz"} MustParse(&args) assert.Equal(t, "baz", args.Foo) @@ -483,7 +489,7 @@ func TestEnvironmentVariableError(t *testing.T) { var args struct { Foo int `arg:"env"` } - os.Setenv("FOO", "bar") + setenv(t, "FOO", "bar") os.Args = []string{"example"} err := Parse(&args) assert.Error(t, err) @@ -493,7 +499,7 @@ func TestEnvironmentVariableRequired(t *testing.T) { var args struct { Foo string `arg:"env,required"` } - os.Setenv("FOO", "bar") + setenv(t, "FOO", "bar") os.Args = []string{"example"} MustParse(&args) assert.Equal(t, "bar", args.Foo) From 6e9648cac63a02c95dbc2b49fe49a7c456d8454c Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sun, 31 Jul 2016 10:16:17 -0700 Subject: [PATCH 59/63] add goreportcard to readme.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 28ff388..66a9b46 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [![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) +[![Report Card](https://goreportcard.com/badge/github.com/alexflint/go-arg)](https://goreportcard.com/badge/github.com/alexflint/go-arg) ## Structured argument parsing for Go From c453aa1a28b0cc9baac51cf5cc04df2688d3cd25 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Thu, 8 Sep 2016 21:18:19 -0700 Subject: [PATCH 60/63] add support for version string --- parse.go | 43 ++++++++++++++++++++++++++++++++----------- usage.go | 7 +++++++ usage_test.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/parse.go b/parse.go index b1193d0..f5fdd7f 100644 --- a/parse.go +++ b/parse.go @@ -27,6 +27,9 @@ type spec struct { // 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...) @@ -39,6 +42,10 @@ func MustParse(dest ...interface{}) *Parser { p.WriteHelp(os.Stdout) os.Exit(0) } + if err == ErrVersion { + fmt.Println(p.version) + os.Exit(0) + } if err != nil { p.Fail(err.Error()) } @@ -61,14 +68,28 @@ type Config struct { // Parser represents a set of command line options with destination values type Parser struct { - spec []*spec - config Config + 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 } // NewParser constructs a parser from a list of destination structs func NewParser(config Config, dests ...interface{}) (*Parser, error) { - var specs []*spec + 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())) @@ -138,19 +159,16 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) { } } } - specs = append(specs, &spec) + p.spec = append(p.spec, &spec) } } - if config.Program == "" { - config.Program = "program" + if p.config.Program == "" { + p.config.Program = "program" if len(os.Args) > 0 { - config.Program = filepath.Base(os.Args[0]) + p.config.Program = filepath.Base(os.Args[0]) } } - return &Parser{ - spec: specs, - config: config, - }, nil + return &p, nil } // Parse processes the given command line option, storing the results in the field @@ -161,6 +179,9 @@ func (p *Parser) Parse(args []string) error { if arg == "-h" || arg == "--help" { return ErrHelp } + if arg == "--version" { + return ErrVersion + } if arg == "--" { break } diff --git a/usage.go b/usage.go index 2ee3953..f096f58 100644 --- a/usage.go +++ b/usage.go @@ -29,6 +29,10 @@ func (p *Parser) WriteUsage(w io.Writer) { } } + if p.version != "" { + fmt.Fprintln(w, p.version) + } + fmt.Fprintf(w, "usage: %s", p.config.Program) // write the option component of the usage message @@ -97,6 +101,9 @@ func (p *Parser) WriteHelp(w io.Writer) { // 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) { diff --git a/usage_test.go b/usage_test.go index b63a7d0..e60efdb 100644 --- a/usage_test.go +++ b/usage_test.go @@ -100,3 +100,32 @@ options: 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() + } +} From f882700b723834ad1371307b81930cde4b81c0aa Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Thu, 8 Sep 2016 21:26:12 -0700 Subject: [PATCH 61/63] add to readme --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 66a9b46..4aa311e 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,28 @@ 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 +``` + ### Custom parsing You can implement your own argument parser by implementing `encoding.TextUnmarshaler`: From 12fa37d10d7e48cd451f4da1facc9b4ec8d631fa Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Mon, 10 Oct 2016 10:48:28 +1030 Subject: [PATCH 62/63] add support for embedded structs --- parse.go | 49 ++++++++++++++++++++++++++++++++++++++----------- parse_test.go | 21 +++++++++++++++++++++ 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/parse.go b/parse.go index f5fdd7f..26b530a 100644 --- a/parse.go +++ b/parse.go @@ -21,7 +21,6 @@ type spec struct { env string wasPresent bool boolean bool - fieldName string // for generating helpful errors } // ErrHelp indicates that -h or --help were provided @@ -81,6 +80,19 @@ type Versioned interface { 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{ @@ -99,19 +111,22 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) { panic(fmt.Sprintf("%T is not a struct pointer", dest)) } - t := v.Type() - for i := 0; i < t.NumField(); i++ { + var errs []string + walkFields(v, func(field reflect.StructField, val reflect.Value, t reflect.Type) bool { // Check for the ignore switch in the tag - field := t.Field(i) tag := field.Tag.Get("arg") if tag == "-" { - continue + 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: v.Field(i), - fieldName: t.Name() + "." + field.Name, + long: strings.ToLower(field.Name), + dest: val, } // Check whether this field is supported. It's good to do this here rather than @@ -121,7 +136,9 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) { var parseable bool parseable, spec.boolean, spec.multiple = canParse(field.Type) if !parseable { - return nil, fmt.Errorf("%s.%s: %s fields are not supported", t.Name(), field.Name, field.Type.String()) + 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 @@ -138,7 +155,9 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) { 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) + 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": @@ -155,11 +174,19 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) { spec.env = strings.ToUpper(field.Name) } default: - return nil, fmt.Errorf("unrecognized tag '%s' on field %s", key, tag) + 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 == "" { diff --git a/parse_test.go b/parse_test.go index ab0cfd7..dffebf4 100644 --- a/parse_test.go +++ b/parse_test.go @@ -633,3 +633,24 @@ func TestInvalidMailAddr(t *testing.T) { 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) +} From 03900620e2d015e9573ac7a20e71ed091a308ba0 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Mon, 10 Oct 2016 10:52:42 +1030 Subject: [PATCH 63/63] add not on embedding to readme --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index 4aa311e..bc761de 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,34 @@ $ ./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`: