changed NewParser to take options at the end rather than config at the front

This commit is contained in:
Alex Flint 2022-10-04 11:28:34 -07:00
parent a1e2b672ea
commit 4aea783023
5 changed files with 79 additions and 107 deletions

View File

@ -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)

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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")