From 4aea7830230f37e75ee4a1edceca6346213d3a7b Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Tue, 4 Oct 2022 11:28:34 -0700 Subject: [PATCH] changed NewParser to take options at the end rather than config at the front --- v2/example_test.go | 4 +- v2/parse.go | 94 ++++++++++++++++++++----------------------- v2/parse_test.go | 8 ++-- v2/subcommand_test.go | 10 ++--- v2/usage_test.go | 70 ++++++++++++-------------------- 5 files changed, 79 insertions(+), 107 deletions(-) diff --git a/v2/example_test.go b/v2/example_test.go index fd64777..e769d60 100644 --- a/v2/example_test.go +++ b/v2/example_test.go @@ -314,7 +314,7 @@ func Example_writeHelpForSubcommand() { osExit = func(int) {} stdout = os.Stdout - p, err := NewParser(Config{}, &args) + p, err := NewParser(&args, WithProgramName("example")) if err != nil { fmt.Println(err) os.Exit(1) @@ -363,7 +363,7 @@ func Example_writeHelpForSubcommandNested() { osExit = func(int) {} stdout = os.Stdout - p, err := NewParser(Config{}, &args) + p, err := NewParser(&args, WithProgramName("example")) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/v2/parse.go b/v2/parse.go index ce02bd4..f5bcab4 100644 --- a/v2/parse.go +++ b/v2/parse.go @@ -1,7 +1,6 @@ package arg import ( - "encoding" "encoding/csv" "errors" "fmt" @@ -73,7 +72,7 @@ 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) + p, err := NewParser(dest) if err != nil { fmt.Fprintln(stdout, err) osExit(-1) @@ -96,25 +95,18 @@ func MustParse(dest interface{}) *Parser { } // Parse processes command line arguments and stores them in dest -func Parse(dest interface{}) error { - p, err := NewParser(Config{}, dest) +func Parse(dest interface{}, options ...ParserOption) error { + p, err := NewParser(dest, options...) if err != nil { return err } return p.Parse(os.Args, os.Environ()) } -// Config represents configuration options for an argument parser -type Config struct { - // Program is the name of the program used in the help text - Program string -} - // Parser represents a set of command line options with destination values type Parser struct { cmd *Command // the top-level command root reflect.Value // destination struct to fill will values - config Config // configuration passed to NewParser version string // version from the argument struct prologue string // prologue for help text (from the argument struct) epilogue string // epilogue for help text (from the argument struct) @@ -170,58 +162,58 @@ func walkFieldsImpl(t reflect.Type, visit func(field reflect.StructField, owner } } +// the ParserOption interface matches options for the parser constructor +type ParserOption interface { + parserOption() +} + +type programNameParserOption struct { + s string +} + +func (programNameParserOption) parserOption() {} + +// WithProgramName overrides the name of the program as displayed in help test +func WithProgramName(name string) ParserOption { + return programNameParserOption{s: name} +} + // NewParser constructs a parser from a list of destination structs -func NewParser(config Config, dest interface{}) (*Parser, error) { - // first pick a name for the command for use in the usage text - var name string - switch { - case config.Program != "": - name = config.Program - case len(os.Args) > 0: - name = filepath.Base(os.Args[0]) - default: - name = "program" - } - - // construct a parser - p := Parser{ - cmd: &Command{name: name}, - config: config, - seen: make(map[*Argument]bool), - } - - // make a list of roots - p.root = reflect.ValueOf(dest) - - // process each of the destination values +func NewParser(dest interface{}, options ...ParserOption) (*Parser, error) { + // check the destination type t := reflect.TypeOf(dest) if t.Kind() != reflect.Ptr { panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", t)) } - cmd, err := cmdFromStruct(name, path{}, t) + // pick a program name for help text and usage output + program := "program" + if len(os.Args) > 0 { + program = filepath.Base(os.Args[0]) + } + + // apply the options + for _, opt := range options { + switch opt := opt.(type) { + case programNameParserOption: + program = opt.s + } + } + + // build the root command from the struct + cmd, err := cmdFromStruct(program, path{}, t) if err != nil { return nil, err } - // add nonzero field values as defaults - for _, arg := range cmd.args { - if v := p.val(arg.dest); v.IsValid() && !isZero(v) { - if defaultVal, ok := v.Interface().(encoding.TextMarshaler); ok { - str, err := defaultVal.MarshalText() - if err != nil { - return nil, fmt.Errorf("%v: error marshaling default value to string: %v", arg.dest, err) - } - arg.defaultVal = string(str) - } else { - arg.defaultVal = fmt.Sprintf("%v", v) - } - } + // construct the parser + p := Parser{ + seen: make(map[*Argument]bool), + root: reflect.ValueOf(dest), + cmd: cmd, } - p.cmd.args = append(p.cmd.args, cmd.args...) - p.cmd.subcommands = append(p.cmd.subcommands, cmd.subcommands...) - + // check for version, prologue, and epilogue if dest, ok := dest.(Versioned); ok { p.version = dest.Version() } diff --git a/v2/parse_test.go b/v2/parse_test.go index 148bd09..042712c 100644 --- a/v2/parse_test.go +++ b/v2/parse_test.go @@ -24,7 +24,7 @@ func pparse(cmdline string, dest interface{}) (*Parser, error) { } func parseWithEnv(dest interface{}, cmdline string, env ...string) (*Parser, error) { - p, err := NewParser(Config{}, dest) + p, err := NewParser(dest) if err != nil { return nil, err } @@ -813,7 +813,7 @@ func TestDefaultValuesIgnored(t *testing.T) { // just checking that default values are not automatically applied // in ProcessCommandLine or ProcessEnvironment - p, err := NewParser(Config{}, &args) + p, err := NewParser(&args) require.NoError(t, err) err = p.ProcessCommandLine(nil) @@ -1293,7 +1293,7 @@ func TestReuseParser(t *testing.T) { Foo string `arg:"required"` } - p, err := NewParser(Config{}, &args) + p, err := NewParser(&args) require.NoError(t, err) err = p.Parse([]string{"program", "--foo=abc"}, nil) @@ -1405,7 +1405,7 @@ func TestUnexportedFieldsSkipped(t *testing.T) { unexported struct{} } - _, err := NewParser(Config{}, &args) + _, err := NewParser(&args) require.NoError(t, err) } diff --git a/v2/subcommand_test.go b/v2/subcommand_test.go index 9f7c8c5..31dc2dd 100644 --- a/v2/subcommand_test.go +++ b/v2/subcommand_test.go @@ -15,7 +15,7 @@ func TestSubcommandNotAPointer(t *testing.T) { var args struct { A string `arg:"subcommand"` } - _, err := NewParser(Config{}, &args) + _, err := NewParser(&args) assert.Error(t, err) } @@ -23,7 +23,7 @@ func TestSubcommandNotAPointerToStruct(t *testing.T) { var args struct { A struct{} `arg:"subcommand"` } - _, err := NewParser(Config{}, &args) + _, err := NewParser(&args) assert.Error(t, err) } @@ -32,7 +32,7 @@ func TestPositionalAndSubcommandNotAllowed(t *testing.T) { A string `arg:"positional"` B *struct{} `arg:"subcommand"` } - _, err := NewParser(Config{}, &args) + _, err := NewParser(&args) assert.Error(t, err) } @@ -54,7 +54,7 @@ func TestSubcommandNamesBeforeParsing(t *testing.T) { var args struct { List *listCmd `arg:"subcommand"` } - p, err := NewParser(Config{}, &args) + p, err := NewParser(&args) require.NoError(t, err) assert.Nil(t, p.Subcommand()) assert.Nil(t, p.SubcommandNames()) @@ -400,7 +400,7 @@ func TestValForNilStruct(t *testing.T) { Sub *subcmd `arg:"subcommand"` } - p, err := NewParser(Config{}, &cmd) + p, err := NewParser(&cmd) require.NoError(t, err) typ := reflect.TypeOf(cmd) diff --git a/v2/usage_test.go b/v2/usage_test.go index b306506..7a5e11d 100644 --- a/v2/usage_test.go +++ b/v2/usage_test.go @@ -50,19 +50,19 @@ Options: --optimize OPTIMIZE, -O OPTIMIZE optimization level --ids IDS Ids - --values VALUES Values [default: [3.14 42 256]] + --values VALUES Values --workers WORKERS, -w WORKERS number of workers to start [default: 10, env: WORKERS] --testenv TESTENV, -a TESTENV [env: TEST_ENV] - --file FILE, -f FILE File with mandatory extension [default: scratch.txt] + --file FILE, -f FILE File with mandatory extension --help, -h display this help and exit ` var args struct { Input string `arg:"positional,required"` Output []string `arg:"positional" help:"list of outputs"` - Name string `help:"name to use"` - Value int `help:"secret value"` + Name string `help:"name to use" default:"Foo Bar"` + Value int `help:"secret value" default:"42"` Verbose bool `arg:"-v" help:"verbosity level"` Dataset string `help:"dataset to use"` Optimize int `arg:"-O" help:"optimization level"` @@ -72,11 +72,7 @@ Options: TestEnv string `arg:"-a,env:TEST_ENV"` File *NameDotName `arg:"-f" help:"File with mandatory extension"` } - args.Name = "Foo Bar" - args.Value = 42 - args.Values = []float64{3.14, 42, 256} - args.File = &NameDotName{"scratch", "txt"} - p, err := NewParser(Config{Program: "example"}, &args) + p, err := NewParser(&args, WithProgramName("example")) require.NoError(t, err) os.Args[0] = "example" @@ -112,11 +108,10 @@ Options: --help, -h display this help and exit ` var args struct { - Label string + Label string `default:"cat"` Content string `default:"dog"` } - args.Label = "cat" - p, err := NewParser(Config{Program: "example"}, &args) + p, err := NewParser(&args, WithProgramName("example")) require.NoError(t, err) args.Label = "should_ignore_this" @@ -130,16 +125,6 @@ Options: assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String())) } -func TestUsageCannotMarshalToString(t *testing.T) { - var args struct { - Name *MyEnum - } - v := MyEnum(42) - args.Name = &v - _, err := NewParser(Config{Program: "example"}, &args) - assert.EqualError(t, err, `args.Name: error marshaling default value to string: There was a problem`) -} - func TestUsageLongPositionalWithHelp_legacyForm(t *testing.T) { expectedUsage := "Usage: example [VERYLONGPOSITIONALWITHHELP]" @@ -157,7 +142,7 @@ Options: VeryLongPositionalWithHelp string `arg:"positional,help:this positional argument is very long but cannot include commas"` } - p, err := NewParser(Config{Program: "example"}, &args) + p, err := NewParser(&args, WithProgramName("example")) require.NoError(t, err) var help bytes.Buffer @@ -186,7 +171,7 @@ Options: VeryLongPositionalWithHelp string `arg:"positional" help:"this positional argument is very long, and includes: commas, colons etc"` } - p, err := NewParser(Config{Program: "example"}, &args) + p, err := NewParser(&args, WithProgramName("example")) require.NoError(t, err) var help bytes.Buffer @@ -207,10 +192,7 @@ Usage: myprogram Options: --help, -h display this help and exit ` - config := Config{ - Program: "myprogram", - } - p, err := NewParser(config, &struct{}{}) + p, err := NewParser(&struct{}{}, WithProgramName("myprogram")) require.NoError(t, err) os.Args[0] = "example" @@ -242,8 +224,7 @@ Options: --help, -h display this help and exit --version display version and exit ` - os.Args[0] = "example" - p, err := NewParser(Config{}, &versioned{}) + p, err := NewParser(&versioned{}, WithProgramName("example")) require.NoError(t, err) var help bytes.Buffer @@ -272,8 +253,7 @@ Usage: example Options: --help, -h display this help and exit ` - os.Args[0] = "example" - p, err := NewParser(Config{}, &described{}) + p, err := NewParser(&described{}, WithProgramName("example")) require.NoError(t, err) var help bytes.Buffer @@ -304,7 +284,7 @@ Options: For more information visit github.com/alexflint/go-arg ` os.Args[0] = "example" - p, err := NewParser(Config{}, &epilogued{}) + p, err := NewParser(&epilogued{}, WithProgramName("example")) require.NoError(t, err) var help bytes.Buffer @@ -323,7 +303,7 @@ func TestUsageForRequiredPositionals(t *testing.T) { Required2 string `arg:"positional,required"` } - p, err := NewParser(Config{Program: "example"}, &args) + p, err := NewParser(&args, WithProgramName("example")) require.NoError(t, err) var usage bytes.Buffer @@ -340,7 +320,7 @@ func TestUsageForMixedPositionals(t *testing.T) { Optional2 string `arg:"positional"` } - p, err := NewParser(Config{Program: "example"}, &args) + p, err := NewParser(&args, WithProgramName("example")) require.NoError(t, err) var usage bytes.Buffer @@ -356,7 +336,7 @@ func TestUsageForRepeatedPositionals(t *testing.T) { Repeated []string `arg:"positional,required"` } - p, err := NewParser(Config{Program: "example"}, &args) + p, err := NewParser(&args, WithProgramName("example")) require.NoError(t, err) var usage bytes.Buffer @@ -374,7 +354,7 @@ func TestUsageForMixedAndRepeatedPositionals(t *testing.T) { Repeated []string `arg:"positional"` } - p, err := NewParser(Config{Program: "example"}, &args) + p, err := NewParser(&args, WithProgramName("example")) require.NoError(t, err) var usage bytes.Buffer @@ -398,7 +378,7 @@ Options: RequiredMultiple []string `arg:"positional,required" help:"required multiple positional"` } - p, err := NewParser(Config{Program: "example"}, &args) + p, err := NewParser(&args, WithProgramName("example")) require.NoError(t, err) var help bytes.Buffer @@ -440,7 +420,7 @@ Global options: } os.Args[0] = "example" - p, err := NewParser(Config{}, &args) + p, err := NewParser(&args) require.NoError(t, err) _ = p.Parse([]string{"child", "nested", "value"}, nil) @@ -458,7 +438,7 @@ func TestNonexistentSubcommand(t *testing.T) { var args struct { sub *struct{} `arg:"subcommand"` } - p, err := NewParser(Config{}, &args) + p, err := NewParser(&args) require.NoError(t, err) var b bytes.Buffer @@ -497,7 +477,7 @@ Options: ShortOnly string `arg:"-a,--" help:"some help" default:"some val" placeholder:"PLACEHOLDER"` ShortOnly2 string `arg:"-b,--,required" help:"some help2"` } - p, err := NewParser(Config{Program: "example"}, &args) + p, err := NewParser(&args, WithProgramName("example")) assert.NoError(t, err) var help bytes.Buffer @@ -524,7 +504,7 @@ Options: Dog string Cat string `arg:"-c,--"` } - p, err := NewParser(Config{Program: "example"}, &args) + p, err := NewParser(&args, WithProgramName("example")) assert.NoError(t, err) var help bytes.Buffer @@ -552,7 +532,7 @@ Options: EnvOnlyOverriden string `arg:"--,env:CUSTOM"` } - p, err := NewParser(Config{Program: "example"}, &args) + p, err := NewParser(&args, WithProgramName("example")) assert.NoError(t, err) var help bytes.Buffer @@ -586,7 +566,7 @@ error: something went wrong var args struct { Foo int } - p, err := NewParser(Config{Program: "example"}, &args) + p, err := NewParser(&args, WithProgramName("example")) require.NoError(t, err) p.Fail("something went wrong") @@ -616,7 +596,7 @@ error: something went wrong var args struct { Sub *struct{} `arg:"subcommand"` } - p, err := NewParser(Config{Program: "example"}, &args) + p, err := NewParser(&args, WithProgramName("example")) require.NoError(t, err) err = p.FailSubcommand("something went wrong", "sub")