summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/go.yml6
-rw-r--r--README.md54
-rw-r--r--go.mod2
-rw-r--r--go.sum2
-rw-r--r--parse.go55
-rw-r--r--parse_test.go76
-rw-r--r--usage.go4
-rw-r--r--usage_test.go31
8 files changed, 210 insertions, 20 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 .
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 {