diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 3dbb91d..dcb52cf 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -15,17 +15,17 @@ jobs: strategy: fail-fast: false matrix: - go: ['1.13', '1.14', '1.15', '1.16'] + go: ['1.17', '1.18', '1.19'] steps: - id: go name: Set up Go - uses: actions/setup-go@v1 + uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Build run: go build -v . diff --git a/README.md b/README.md index dab2996..f105b17 100644 --- a/README.md +++ b/README.md @@ -134,10 +134,10 @@ arg.MustParse(&args) ```shell $ ./example -h -Usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]] +Usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]] Positional arguments: - INPUT + INPUT OUTPUT Options: @@ -180,6 +180,24 @@ var args struct { arg.MustParse(&args) ``` +#### Ignoring environment variables and/or default values + +The values in an existing structure can be kept in-tact by ignoring environment +variables and/or default values. + +```go +var args struct { + Test string `arg:"-t,env:TEST" default:"something"` +} + +p, err := arg.NewParser(arg.Config{ + IgnoreEnv: true, + IgnoreDefault: true, +}, &args) + +err = p.Parse(os.Args) +``` + ### Arguments with multiple values ```go var args struct { @@ -444,6 +462,9 @@ Options: ### Description strings +A descriptive message can be added at the top of the help text by implementing +a `Description` function that returns a string. + ```go type args struct { Foo string @@ -469,6 +490,35 @@ Options: --help, -h display this help and exit ``` +Similarly an epilogue can be added at the end of the help text by implementing +the `Epilogue` function. + +```go +type args struct { + Foo string +} + +func (args) Epilogue() string { + return "For more information visit github.com/alexflint/go-arg" +} + +func main() { + var args args + arg.MustParse(&args) +} +``` + +```shell +$ ./example -h +Usage: example [--foo FOO] + +Options: + --foo FOO + --help, -h display this help and exit + +For more information visit github.com/alexflint/go-arg +``` + ### Subcommands *Introduced in version 1.1.0* diff --git a/go.mod b/go.mod index 0823012..44ddff5 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/alexflint/go-arg require ( - github.com/alexflint/go-scalar v1.1.0 + github.com/alexflint/go-scalar v1.2.0 github.com/stretchr/testify v1.7.0 ) diff --git a/go.sum b/go.sum index 1170bc7..5b536f9 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM= github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= +github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= +github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/parse.go b/parse.go index 2fb7b1c..dc48455 100644 --- a/parse.go +++ b/parse.go @@ -83,18 +83,7 @@ func MustParse(dest ...interface{}) *Parser { return nil // just in case osExit was monkey-patched } - err = p.Parse(flags()) - switch { - case err == ErrHelp: - p.writeHelpForSubcommand(stdout, p.lastCmd) - osExit(0) - case err == ErrVersion: - fmt.Fprintln(stdout, p.version) - osExit(0) - case err != nil: - p.failWithSubcommand(err.Error(), p.lastCmd) - } - + p.MustParse(flags()) return p } @@ -122,6 +111,10 @@ type Config struct { // IgnoreEnv instructs the library not to read environment variables IgnoreEnv bool + + // IgnoreDefault instructs the library not to reset the variables to the + // default values, including pointers to sub commands + IgnoreDefault bool } // Parser represents a set of command line options with destination values @@ -131,6 +124,7 @@ type Parser struct { config Config version string description string + epilogue string // the following field changes during processing of command line arguments lastCmd *command @@ -152,6 +146,14 @@ type Described interface { Description() string } +// Epilogued is the interface that the destination struct should implement to +// add an epilogue string at the bottom of the help message. +type Epilogued interface { + // Epilogue returns the string that will be printed on a line by itself + // at the end of the help message. + Epilogue() string +} + // walkFields calls a function for each field of a struct, recursively expanding struct fields. func walkFields(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool) { walkFieldsImpl(t, visit, nil) @@ -246,6 +248,9 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) { if dest, ok := dest.(Described); ok { p.description = dest.Description() } + if dest, ok := dest.(Epilogued); ok { + p.epilogue = dest.Epilogue() + } } return &p, nil @@ -470,6 +475,20 @@ func (p *Parser) Parse(args []string) error { return err } +func (p *Parser) MustParse(args []string) { + err := p.Parse(args) + switch { + case err == ErrHelp: + p.writeHelpForSubcommand(stdout, p.lastCmd) + osExit(0) + case err == ErrVersion: + fmt.Fprintln(stdout, p.version) + osExit(0) + case err != nil: + p.failWithSubcommand(err.Error(), p.lastCmd) + } +} + // process environment vars for the given arguments func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error { for _, spec := range specs { @@ -564,7 +583,9 @@ func (p *Parser) process(args []string) error { // instantiate the field to point to a new struct v := p.val(subcmd.dest) - v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers + } // add the new options to the set of allowed options specs = append(specs, subcmd.specs...) @@ -696,7 +717,13 @@ func (p *Parser) process(args []string) error { } return errors.New(msg) } - if spec.defaultValue.IsValid() { + + if spec.defaultValue.IsValid() && !p.config.IgnoreDefault { + // One issue here is that if the user now modifies the value then + // the default value stored in the spec will be corrupted. There + // is no general way to "deep-copy" values in Go, and we still + // support the old-style method for specifying defaults as + // Go values assigned directly to the struct field, so we are stuck. p.val(spec.dest).Set(spec.defaultValue) } } diff --git a/parse_test.go b/parse_test.go index 7747d05..5d38306 100644 --- a/parse_test.go +++ b/parse_test.go @@ -96,6 +96,21 @@ func TestInt(t *testing.T) { assert.EqualValues(t, 8, *args.Ptr) } +func TestHexOctBin(t *testing.T) { + var args struct { + Hex int + Oct int + Bin int + Underscored int + } + err := parse("--hex 0xA --oct 0o10 --bin 0b101 --underscored 123_456", &args) + require.NoError(t, err) + assert.EqualValues(t, 10, args.Hex) + assert.EqualValues(t, 8, args.Oct) + assert.EqualValues(t, 5, args.Bin) + assert.EqualValues(t, 123456, args.Underscored) +} + func TestNegativeInt(t *testing.T) { var args struct { Foo int @@ -817,6 +832,19 @@ func TestEnvironmentVariableIgnored(t *testing.T) { assert.Equal(t, "", args.Foo) } +func TestDefaultValuesIgnored(t *testing.T) { + var args struct { + Foo string `default:"bad"` + } + + p, err := NewParser(Config{IgnoreDefault: true}, &args) + require.NoError(t, err) + + err = p.Parse(nil) + assert.NoError(t, err) + assert.Equal(t, "", args.Foo) +} + func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) { var args struct { Sub *struct { @@ -833,6 +861,54 @@ func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) { assert.Equal(t, "", args.Sub.Foo) } +func TestParserMustParseEmptyArgs(t *testing.T) { + // this mirrors TestEmptyArgs + p, err := NewParser(Config{}, &struct{}{}) + require.NoError(t, err) + assert.NotNil(t, p) + p.MustParse(nil) +} + +func TestParserMustParse(t *testing.T) { + tests := []struct { + name string + args versioned + cmdLine []string + code int + output string + }{ + {name: "help", args: struct{}{}, cmdLine: []string{"--help"}, code: 0, output: "display this help and exit"}, + {name: "version", args: versioned{}, cmdLine: []string{"--version"}, code: 0, output: "example 3.2.1"}, + {name: "invalid", args: struct{}{}, cmdLine: []string{"invalid"}, code: -1, output: ""}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + originalExit := osExit + originalStdout := stdout + defer func() { + osExit = originalExit + stdout = originalStdout + }() + + var exitCode *int + osExit = func(code int) { exitCode = &code } + var b bytes.Buffer + stdout = &b + + p, err := NewParser(Config{}, &tt.args) + require.NoError(t, err) + assert.NotNil(t, p) + + p.MustParse(tt.cmdLine) + assert.NotNil(t, exitCode) + assert.Equal(t, tt.code, *exitCode) + assert.Contains(t, b.String(), tt.output) + }) + } +} + type textUnmarshaler struct { val int } diff --git a/usage.go b/usage.go index 7d2a517..7a480c3 100644 --- a/usage.go +++ b/usage.go @@ -290,6 +290,10 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) { printTwoCols(w, subcmd.name, subcmd.help, "", "") } } + + if p.epilogue != "" { + fmt.Fprintln(w, "\n"+p.epilogue) + } } func (p *Parser) printOption(w io.Writer, spec *spec) { diff --git a/usage_test.go b/usage_test.go index 8fb32c8..be5894a 100644 --- a/usage_test.go +++ b/usage_test.go @@ -284,6 +284,37 @@ Options: assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String())) } +type epilogued struct{} + +// Epilogued returns the epilogue for this program +func (epilogued) Epilogue() string { + return "For more information visit github.com/alexflint/go-arg" +} + +func TestUsageWithEpilogue(t *testing.T) { + expectedUsage := "Usage: example" + + expectedHelp := ` +Usage: example + +Options: + --help, -h display this help and exit + +For more information visit github.com/alexflint/go-arg +` + os.Args[0] = "example" + p, err := NewParser(Config{}, &epilogued{}) + require.NoError(t, err) + + var help bytes.Buffer + p.WriteHelp(&help) + assert.Equal(t, expectedHelp[1:], help.String()) + + var usage bytes.Buffer + p.WriteUsage(&usage) + assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String())) +} + func TestUsageForRequiredPositionals(t *testing.T) { expectedUsage := "Usage: example REQUIRED1 REQUIRED2\n" var args struct {