From f427e9f317714fc985c39ca24990852e73be5ef0 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 31 Oct 2015 23:57:26 -0700 Subject: [PATCH] added parser struct --- parse.go | 84 ++++++++++++++++++++++++++++++++++----------------- parse_test.go | 32 +++++++++++--------- usage.go | 25 +++++++-------- 3 files changed, 88 insertions(+), 53 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) } -} - -// Parse processes command line arguments and stores the result in 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(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"` + 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 the spec - dest = append(dest, &internal) +// 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 p.Parse(os.Args[1:]) +} + +// Parser represents a set of command line options with destination values +type Parser struct { + spec []*spec +} + +// NewParser constructs a parser from a list of destination structs +func NewParser(dest ...interface{}) (*Parser, error) { spec, err := extractSpec(dest...) if err != nil { - return err + 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 args - err = processArgs(spec, args) + // Process all command line arguments + err := process(p.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) + 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) + } +}