From a87d80089a78b707d9e4fbd7061e54e7e834688d Mon Sep 17 00:00:00 2001 From: Sebastiaan Pasterkamp <26205277+SebastiaanPasterkamp@users.noreply.github.com> Date: Sun, 2 Jan 2022 15:06:37 +0100 Subject: [PATCH 1/6] Add 'IgnoreDefault' option --- README.md | 22 ++++++++++++++++++++-- parse.go | 10 ++++++++-- parse_test.go | 13 +++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index dab2996..7f1fdca 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 { diff --git a/parse.go b/parse.go index 7588dfb..9f93502 100644 --- a/parse.go +++ b/parse.go @@ -121,6 +121,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 @@ -527,7 +531,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 !p.config.IgnoreDefault || 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...) @@ -659,7 +665,7 @@ func (p *Parser) process(args []string) error { } return errors.New(msg) } - if spec.defaultVal != "" { + if !p.config.IgnoreDefault && spec.defaultVal != "" { err := scalar.ParseValue(p.val(spec.dest), spec.defaultVal) if err != nil { return fmt.Errorf("error processing default value for %s: %v", name, err) diff --git a/parse_test.go b/parse_test.go index 2d0ef7a..aebe5ff 100644 --- a/parse_test.go +++ b/parse_test.go @@ -816,6 +816,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 { From b48371a62f7beed42eadc9b719ea4f059aa24ef2 Mon Sep 17 00:00:00 2001 From: Sebastiaan Pasterkamp <26205277+SebastiaanPasterkamp@users.noreply.github.com> Date: Sun, 5 Jun 2022 17:54:46 +0200 Subject: [PATCH 2/6] Simplify sub-command initialization w/o IgnoreDefault --- parse.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parse.go b/parse.go index 9f93502..a883a10 100644 --- a/parse.go +++ b/parse.go @@ -531,7 +531,7 @@ func (p *Parser) process(args []string) error { // instantiate the field to point to a new struct v := p.val(subcmd.dest) - if !p.config.IgnoreDefault || v.IsNil() { + if v.IsNil() { v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers } From c8b9567d1ba7f0ab20f93b60e5a8344c2c23c110 Mon Sep 17 00:00:00 2001 From: Sebastiaan Pasterkamp <26205277+SebastiaanPasterkamp@users.noreply.github.com> Date: Sat, 17 Sep 2022 12:39:31 +0200 Subject: [PATCH 3/6] Feat: Add epilog after help text Similar to the Description at the top of the help text an Epilog is added at the bottom. Resolves #189 --- README.md | 32 ++++++++++++++++++++++++++++++++ parse.go | 12 ++++++++++++ usage.go | 4 ++++ usage_test.go | 31 +++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+) diff --git a/README.md b/README.md index 7f1fdca..f105b17 100644 --- a/README.md +++ b/README.md @@ -462,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 @@ -487,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/parse.go b/parse.go index a883a10..c8cd79e 100644 --- a/parse.go +++ b/parse.go @@ -134,6 +134,7 @@ type Parser struct { config Config version string description string + epilogue string // the following field changes during processing of command line arguments lastCmd *command @@ -155,6 +156,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) @@ -236,6 +245,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 diff --git a/usage.go b/usage.go index e936811..7ba06cc 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 1744536..fd67fc8 100644 --- a/usage_test.go +++ b/usage_test.go @@ -285,6 +285,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 { From ea0f540c400aa3f0646c94a65458abe866f752b6 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sun, 2 Oct 2022 13:05:04 -0700 Subject: [PATCH 4/6] update to latest go-scalar, add test for hex, oct, and binary integer literals --- go.mod | 2 +- go.sum | 2 ++ parse_test.go | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 67ac880..944b9bc 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_test.go b/parse_test.go index aebe5ff..4ea6bc4 100644 --- a/parse_test.go +++ b/parse_test.go @@ -95,6 +95,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 From 7f4979a06ec60712a6a7ad2650b504df6363ec29 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sun, 2 Oct 2022 13:16:32 -0700 Subject: [PATCH 5/6] update to latest 3 versions of Go for CI --- .github/workflows/go.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 . From 4fc9666f79d7a9a84be9963e34b5f2479cd340b6 Mon Sep 17 00:00:00 2001 From: Daniele Sluijters Date: Wed, 5 Oct 2022 17:59:23 +0200 Subject: [PATCH 6/6] Implement MustParse on Parse This moves most of the body of the MustParse function into a MustParse method on a Parser. The MustParse function is now implemented by calling the MustParse function on the Parser it implicitly creates. Closes: #194 --- parse.go | 27 +++++++++++++++------------ parse_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/parse.go b/parse.go index c8cd79e..dc87947 100644 --- a/parse.go +++ b/parse.go @@ -82,18 +82,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 } @@ -449,6 +438,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 { diff --git a/parse_test.go b/parse_test.go index 4ea6bc4..7e84def 100644 --- a/parse_test.go +++ b/parse_test.go @@ -860,6 +860,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 }