Merge remote-tracking branch 'origin/master' into default-value-issue
This commit is contained in:
commit
3d95a706a6
|
@ -15,17 +15,17 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
go: ['1.13', '1.14', '1.15', '1.16']
|
go: ['1.17', '1.18', '1.19']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: go
|
- id: go
|
||||||
name: Set up Go
|
name: Set up Go
|
||||||
uses: actions/setup-go@v1
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build -v .
|
run: go build -v .
|
||||||
|
|
50
README.md
50
README.md
|
@ -180,6 +180,24 @@ var args struct {
|
||||||
arg.MustParse(&args)
|
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
|
### Arguments with multiple values
|
||||||
```go
|
```go
|
||||||
var args struct {
|
var args struct {
|
||||||
|
@ -444,6 +462,9 @@ Options:
|
||||||
|
|
||||||
### Description strings
|
### 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
|
```go
|
||||||
type args struct {
|
type args struct {
|
||||||
Foo string
|
Foo string
|
||||||
|
@ -469,6 +490,35 @@ Options:
|
||||||
--help, -h display this help and exit
|
--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
|
### Subcommands
|
||||||
|
|
||||||
*Introduced in version 1.1.0*
|
*Introduced in version 1.1.0*
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -1,7 +1,7 @@
|
||||||
module github.com/alexflint/go-arg
|
module github.com/alexflint/go-arg
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexflint/go-scalar v1.1.0
|
github.com/alexflint/go-scalar v1.2.0
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
2
go.sum
2
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 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM=
|
||||||
github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
|
55
parse.go
55
parse.go
|
@ -83,18 +83,7 @@ func MustParse(dest ...interface{}) *Parser {
|
||||||
return nil // just in case osExit was monkey-patched
|
return nil // just in case osExit was monkey-patched
|
||||||
}
|
}
|
||||||
|
|
||||||
err = p.Parse(flags())
|
p.MustParse(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)
|
|
||||||
}
|
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,6 +111,10 @@ type Config struct {
|
||||||
|
|
||||||
// IgnoreEnv instructs the library not to read environment variables
|
// IgnoreEnv instructs the library not to read environment variables
|
||||||
IgnoreEnv bool
|
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
|
// Parser represents a set of command line options with destination values
|
||||||
|
@ -131,6 +124,7 @@ type Parser struct {
|
||||||
config Config
|
config Config
|
||||||
version string
|
version string
|
||||||
description string
|
description string
|
||||||
|
epilogue string
|
||||||
|
|
||||||
// the following field changes during processing of command line arguments
|
// the following field changes during processing of command line arguments
|
||||||
lastCmd *command
|
lastCmd *command
|
||||||
|
@ -152,6 +146,14 @@ type Described interface {
|
||||||
Description() string
|
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.
|
// 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) {
|
func walkFields(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool) {
|
||||||
walkFieldsImpl(t, visit, nil)
|
walkFieldsImpl(t, visit, nil)
|
||||||
|
@ -246,6 +248,9 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
||||||
if dest, ok := dest.(Described); ok {
|
if dest, ok := dest.(Described); ok {
|
||||||
p.description = dest.Description()
|
p.description = dest.Description()
|
||||||
}
|
}
|
||||||
|
if dest, ok := dest.(Epilogued); ok {
|
||||||
|
p.epilogue = dest.Epilogue()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &p, nil
|
return &p, nil
|
||||||
|
@ -470,6 +475,20 @@ func (p *Parser) Parse(args []string) error {
|
||||||
return err
|
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
|
// process environment vars for the given arguments
|
||||||
func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error {
|
func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error {
|
||||||
for _, spec := range specs {
|
for _, spec := range specs {
|
||||||
|
@ -564,7 +583,9 @@ func (p *Parser) process(args []string) error {
|
||||||
|
|
||||||
// instantiate the field to point to a new struct
|
// instantiate the field to point to a new struct
|
||||||
v := p.val(subcmd.dest)
|
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
|
// add the new options to the set of allowed options
|
||||||
specs = append(specs, subcmd.specs...)
|
specs = append(specs, subcmd.specs...)
|
||||||
|
@ -696,7 +717,13 @@ func (p *Parser) process(args []string) error {
|
||||||
}
|
}
|
||||||
return errors.New(msg)
|
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)
|
p.val(spec.dest).Set(spec.defaultValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,21 @@ func TestInt(t *testing.T) {
|
||||||
assert.EqualValues(t, 8, *args.Ptr)
|
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) {
|
func TestNegativeInt(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo int
|
Foo int
|
||||||
|
@ -817,6 +832,19 @@ func TestEnvironmentVariableIgnored(t *testing.T) {
|
||||||
assert.Equal(t, "", args.Foo)
|
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) {
|
func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Sub *struct {
|
Sub *struct {
|
||||||
|
@ -833,6 +861,54 @@ func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
|
||||||
assert.Equal(t, "", args.Sub.Foo)
|
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 {
|
type textUnmarshaler struct {
|
||||||
val int
|
val int
|
||||||
}
|
}
|
||||||
|
|
4
usage.go
4
usage.go
|
@ -290,6 +290,10 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
|
||||||
printTwoCols(w, subcmd.name, subcmd.help, "", "")
|
printTwoCols(w, subcmd.name, subcmd.help, "", "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.epilogue != "" {
|
||||||
|
fmt.Fprintln(w, "\n"+p.epilogue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) printOption(w io.Writer, spec *spec) {
|
func (p *Parser) printOption(w io.Writer, spec *spec) {
|
||||||
|
|
|
@ -284,6 +284,37 @@ Options:
|
||||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
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) {
|
func TestUsageForRequiredPositionals(t *testing.T) {
|
||||||
expectedUsage := "Usage: example REQUIRED1 REQUIRED2\n"
|
expectedUsage := "Usage: example REQUIRED1 REQUIRED2\n"
|
||||||
var args struct {
|
var args struct {
|
||||||
|
|
Loading…
Reference in New Issue