Compare commits

..

No commits in common. "register" and "v1.4.3" have entirely different histories.

13 changed files with 308 additions and 1066 deletions

View File

@ -15,17 +15,17 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
go: ['1.17', '1.18', '1.19'] go: ['1.13', '1.14', '1.15', '1.16']
steps: steps:
- id: go - id: go
name: Set up Go name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v1
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Build - name: Build
run: go build -v . run: go build -v .

View File

@ -1,23 +0,0 @@
# git remote add gitwit git@git.wit.org:wit/arg.git
redomod:
rm -f go.*
GO111MODULE= go mod init
GO111MODULE= go mod tidy
github:
git push origin register
git push origin devel
git push origin jcarr
git push origin --tags
# git push github register
# git push github devel
# git push github --tags
@echo
@echo check https://git.wit.org/wit/arg
@echo
# init-github:
# git push -u github master
# git push -u github devel
# git push github --tags

View File

@ -134,10 +134,10 @@ arg.MustParse(&args)
```shell ```shell
$ ./example -h $ ./example -h
Usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]] Usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]]
Positional arguments: Positional arguments:
INPUT INPUT
OUTPUT OUTPUT
Options: Options:
@ -180,24 +180,6 @@ 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 {
@ -462,9 +444,6 @@ 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
@ -490,35 +469,6 @@ 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*

View File

@ -162,7 +162,8 @@ func Example_helpText() {
} }
// This is only necessary when running inside golang's runnable example harness // This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {} osExit = func(int) {}
stdout = os.Stdout
MustParse(&args) MustParse(&args)
@ -194,7 +195,8 @@ func Example_helpPlaceholder() {
} }
// This is only necessary when running inside golang's runnable example harness // This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {} osExit = func(int) {}
stdout = os.Stdout
MustParse(&args) MustParse(&args)
@ -234,7 +236,8 @@ func Example_helpTextWithSubcommand() {
} }
// This is only necessary when running inside golang's runnable example harness // This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {} osExit = func(int) {}
stdout = os.Stdout
MustParse(&args) MustParse(&args)
@ -271,7 +274,8 @@ func Example_helpTextWhenUsingSubcommand() {
} }
// This is only necessary when running inside golang's runnable example harness // This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {} osExit = func(int) {}
stdout = os.Stdout
MustParse(&args) MustParse(&args)
@ -307,9 +311,10 @@ func Example_writeHelpForSubcommand() {
} }
// This is only necessary when running inside golang's runnable example harness // This is only necessary when running inside golang's runnable example harness
exit := func(int) {} osExit = func(int) {}
stdout = os.Stdout
p, err := NewParser(Config{Exit: exit}, &args) p, err := NewParser(Config{}, &args)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
@ -355,9 +360,10 @@ func Example_writeHelpForSubcommandNested() {
} }
// This is only necessary when running inside golang's runnable example harness // This is only necessary when running inside golang's runnable example harness
exit := func(int) {} osExit = func(int) {}
stdout = os.Stdout
p, err := NewParser(Config{Exit: exit}, &args) p, err := NewParser(Config{}, &args)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
@ -391,7 +397,8 @@ func Example_errorText() {
} }
// This is only necessary when running inside golang's runnable example harness // This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {} osExit = func(int) {}
stderr = os.Stdout
MustParse(&args) MustParse(&args)
@ -414,7 +421,8 @@ func Example_errorTextForSubcommand() {
} }
// This is only necessary when running inside golang's runnable example harness // This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {} osExit = func(int) {}
stderr = os.Stdout
MustParse(&args) MustParse(&args)
@ -449,7 +457,8 @@ func Example_subcommand() {
} }
// This is only necessary when running inside golang's runnable example harness // This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {} osExit = func(int) {}
stderr = os.Stdout
MustParse(&args) MustParse(&args)
@ -496,45 +505,3 @@ func Example_allSupportedTypes() {
// output: // output:
} }
func Example_envVarOnly() {
os.Args = split("./example")
_ = os.Setenv("AUTH_KEY", "my_key")
defer os.Unsetenv("AUTH_KEY")
var args struct {
AuthKey string `arg:"--,env:AUTH_KEY"`
}
MustParse(&args)
fmt.Println(args.AuthKey)
// output: my_key
}
func Example_envVarOnlyShouldIgnoreFlag() {
os.Args = split("./example --=my_key")
var args struct {
AuthKey string `arg:"--,env:AUTH_KEY"`
}
err := Parse(&args)
fmt.Println(err)
// output: unknown argument --=my_key
}
func Example_envVarOnlyShouldIgnoreShortFlag() {
os.Args = split("./example -=my_key")
var args struct {
AuthKey string `arg:"--,env:AUTH_KEY"`
}
err := Parse(&args)
fmt.Println(err)
// output: unknown argument -=my_key
}

14
go.mod
View File

@ -1,14 +1,8 @@
module go.wit.com/dev/alexflint/go-arg module github.com/alexflint/go-arg
go 1.21.4
require ( require (
github.com/alexflint/go-scalar v1.2.0 github.com/alexflint/go-scalar v1.1.0
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.7.0
) )
require ( go 1.13
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

14
go.sum
View File

@ -1,13 +1,15 @@
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM=
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= github.com/alexflint/go-scalar v1.1.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 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=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

304
parse.go
View File

@ -5,7 +5,6 @@ import (
"encoding/csv" "encoding/csv"
"errors" "errors"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -44,25 +43,23 @@ func (p path) Child(f reflect.StructField) path {
// spec represents a command line option // spec represents a command line option
type spec struct { type spec struct {
dest path dest path
field reflect.StructField // the struct field from which this option was created field reflect.StructField // the struct field from which this option was created
long string // the --long form for this option, or empty if none long string // the --long form for this option, or empty if none
short string // the -s short form for this option, or empty if none short string // the -s short form for this option, or empty if none
cardinality cardinality // determines how many tokens will be present (possible values: zero, one, multiple) cardinality cardinality // determines how many tokens will be present (possible values: zero, one, multiple)
required bool // if true, this option must be present on the command line required bool // if true, this option must be present on the command line
positional bool // if true, this option will be looked for in the positional flags positional bool // if true, this option will be looked for in the positional flags
separate bool // if true, each slice and map entry will have its own --flag separate bool // if true, each slice and map entry will have its own --flag
help string // the help text for this option help string // the help text for this option
env string // the name of the environment variable for this option, or empty for none env string // the name of the environment variable for this option, or empty for none
defaultValue reflect.Value // default value for this option defaultVal string // default value for this option
defaultString string // default value for this option, in string form to be displayed in help text placeholder string // name of the data in help
placeholder string // name of the data in help
} }
// command represents a named subcommand, or the top-level command // command represents a named subcommand, or the top-level command
type command struct { type command struct {
name string name string
aliases []string
help string help string
dest path dest path
specs []*spec specs []*spec
@ -70,55 +67,33 @@ type command struct {
parent *command parent *command
} }
// ErrHelp indicates that the builtin -h or --help were provided // ErrHelp indicates that -h or --help were provided
var ErrHelp = errors.New("help requested by user") var ErrHelp = errors.New("help requested by user")
// ErrVersion indicates that the builtin --version was provided // ErrVersion indicates that --version was provided
var ErrVersion = errors.New("version requested by user") var ErrVersion = errors.New("version requested by user")
// for monkey patching in example code
var mustParseExit = os.Exit
// This stores the args sent from modules
var register []interface{}
/*
Use this in your packages to register
variables with go-arg. Then add this to your init()
package 'foo'
function init() {
args.Register(&argsFoo)
}
*/
func Register(dest ...interface{}) {
register = append(register, dest...)
}
// MustParse processes command line arguments and exits upon failure // MustParse processes command line arguments and exits upon failure
func MustParse(dest ...interface{}) *Parser { func MustParse(dest ...interface{}) *Parser {
register = append(register, dest...) p, err := NewParser(Config{}, dest...)
return mustParse(Config{Exit: mustParseExit}, register...)
}
// mustParse is a helper that facilitates testing
func mustParse(config Config, dest ...interface{}) *Parser {
if config.Exit == nil {
config.Exit = os.Exit
}
if config.Out == nil {
config.Out = os.Stdout
}
p, err := NewParser(config, dest...)
if err != nil { if err != nil {
fmt.Fprintln(config.Out, err) fmt.Fprintln(stdout, err)
config.Exit(-1) osExit(-1)
return nil 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 return p
} }
@ -146,20 +121,6 @@ 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
// StrictSubcommands intructs the library not to allow global commands after
// subcommand
StrictSubcommands bool
// Exit is called to terminate the process with an error code (defaults to os.Exit)
Exit func(int)
// Out is where help text, usage text, and failure messages are printed (defaults to os.Stdout)
Out io.Writer
} }
// Parser represents a set of command line options with destination values // Parser represents a set of command line options with destination values
@ -169,10 +130,9 @@ 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
subcommand []string lastCmd *command
} }
// Versioned is the interface that the destination struct should implement to // Versioned is the interface that the destination struct should implement to
@ -191,14 +151,6 @@ 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)
@ -222,14 +174,6 @@ func walkFieldsImpl(t reflect.Type, visit func(field reflect.StructField, owner
// NewParser constructs a parser from a list of destination structs // NewParser constructs a parser from a list of destination structs
func NewParser(config Config, dests ...interface{}) (*Parser, error) { func NewParser(config Config, dests ...interface{}) (*Parser, error) {
// fill in defaults
if config.Exit == nil {
config.Exit = os.Exit
}
if config.Out == nil {
config.Out = os.Stdout
}
// first pick a name for the command for use in the usage text // first pick a name for the command for use in the usage text
var name string var name string
switch { switch {
@ -264,31 +208,18 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
return nil, err return nil, err
} }
// for backwards compatibility, add nonzero field values as defaults // add nonzero field values as defaults
// this applies only to the top-level command, not to subcommands (this inconsistency
// is the reason that this method for setting default values was deprecated)
for _, spec := range cmd.specs { for _, spec := range cmd.specs {
// get the value if v := p.val(spec.dest); v.IsValid() && !isZero(v) {
v := p.val(spec.dest) if defaultVal, ok := v.Interface().(encoding.TextMarshaler); ok {
str, err := defaultVal.MarshalText()
// if the value is the "zero value" (e.g. nil pointer, empty struct) then ignore if err != nil {
if isZero(v) { return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
continue }
} spec.defaultVal = string(str)
} else {
// store as a default spec.defaultVal = fmt.Sprintf("%v", v)
spec.defaultValue = v
// we need a string to display in help text
// if MarshalText is implemented then use that
if m, ok := v.Interface().(encoding.TextMarshaler); ok {
s, err := m.MarshalText()
if err != nil {
return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
} }
spec.defaultString = string(s)
} else {
spec.defaultString = fmt.Sprintf("%v", v)
} }
} }
@ -301,9 +232,6 @@ 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
@ -360,9 +288,13 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
spec.help = help spec.help = help
} }
defaultVal, hasDefault := field.Tag.Lookup("default")
if hasDefault {
spec.defaultVal = defaultVal
}
// Look at the tag // Look at the tag
var isSubcommand bool // tracks whether this field is a subcommand var isSubcommand bool // tracks whether this field is a subcommand
for _, key := range strings.Split(tag, ",") { for _, key := range strings.Split(tag, ",") {
if key == "" { if key == "" {
continue continue
@ -380,13 +312,18 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
case strings.HasPrefix(key, "--"): case strings.HasPrefix(key, "--"):
spec.long = key[2:] spec.long = key[2:]
case strings.HasPrefix(key, "-"): case strings.HasPrefix(key, "-"):
if len(key) > 2 { if len(key) != 2 {
errs = append(errs, fmt.Sprintf("%s.%s: short arguments must be one character only", errs = append(errs, fmt.Sprintf("%s.%s: short arguments must be one character only",
t.Name(), field.Name)) t.Name(), field.Name))
return false return false
} }
spec.short = key[1:] spec.short = key[1:]
case key == "required": case key == "required":
if hasDefault {
errs = append(errs, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified",
t.Name(), field.Name))
return false
}
spec.required = true spec.required = true
case key == "positional": case key == "positional":
spec.positional = true spec.positional = true
@ -403,24 +340,18 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
} }
case key == "subcommand": case key == "subcommand":
// decide on a name for the subcommand // decide on a name for the subcommand
var cmdnames []string cmdname := value
if value == "" { if cmdname == "" {
cmdnames = []string{strings.ToLower(field.Name)} cmdname = strings.ToLower(field.Name)
} else {
cmdnames = strings.Split(value, "|")
}
for i := range cmdnames {
cmdnames[i] = strings.TrimSpace(cmdnames[i])
} }
// parse the subcommand recursively // parse the subcommand recursively
subcmd, err := cmdFromStruct(cmdnames[0], subdest, field.Type) subcmd, err := cmdFromStruct(cmdname, subdest, field.Type)
if err != nil { if err != nil {
errs = append(errs, err.Error()) errs = append(errs, err.Error())
return false return false
} }
subcmd.aliases = cmdnames[1:]
subcmd.parent = &cmd subcmd.parent = &cmd
subcmd.help = field.Tag.Get("help") subcmd.help = field.Tag.Get("help")
@ -441,60 +372,27 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
spec.placeholder = strings.ToUpper(spec.field.Name) spec.placeholder = strings.ToUpper(spec.field.Name)
} }
// if this is a subcommand then we've done everything we need to do // Check whether this field is supported. It's good to do this here rather than
if isSubcommand {
return false
}
// check whether this field is supported. It's good to do this here rather than
// wait until ParseValue because it means that a program with invalid argument // wait until ParseValue because it means that a program with invalid argument
// fields will always fail regardless of whether the arguments it received // fields will always fail regardless of whether the arguments it received
// exercised those fields. // exercised those fields.
var err error if !isSubcommand {
spec.cardinality, err = cardinalityOf(field.Type) cmd.specs = append(cmd.specs, &spec)
if err != nil {
errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported",
t.Name(), field.Name, field.Type.String()))
return false
}
defaultString, hasDefault := field.Tag.Lookup("default") var err error
if hasDefault { spec.cardinality, err = cardinalityOf(field.Type)
// we do not support default values for maps and slices if err != nil {
if spec.cardinality == multiple { errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported",
t.Name(), field.Name, field.Type.String()))
return false
}
if spec.cardinality == multiple && hasDefault {
errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields", errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields",
t.Name(), field.Name)) t.Name(), field.Name))
return false return false
} }
// a required field cannot also have a default value
if spec.required {
errs = append(errs, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified",
t.Name(), field.Name))
return false
}
// parse the default value
spec.defaultString = defaultString
if field.Type.Kind() == reflect.Ptr {
// here we have a field of type *T and we create a new T, no need to dereference
// in order for the value to be settable
spec.defaultValue = reflect.New(field.Type.Elem())
} else {
// here we have a field of type T and we create a new T and then dereference it
// so that the resulting value is settable
spec.defaultValue = reflect.New(field.Type).Elem()
}
err := scalar.ParseValue(spec.defaultValue, defaultString)
if err != nil {
errs = append(errs, fmt.Sprintf("%s.%s: error processing default value: %v", t.Name(), field.Name, err))
return false
}
} }
// add the spec to the list of specs
cmd.specs = append(cmd.specs, &spec)
// if this was an embedded field then we already returned true up above // if this was an embedded field then we already returned true up above
return false return false
}) })
@ -535,20 +433,6 @@ 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(p.config.Out, p.subcommand...)
p.config.Exit(0)
case err == ErrVersion:
fmt.Fprintln(p.config.Out, p.version)
p.config.Exit(0)
case err != nil:
p.FailSubcommand(err.Error(), p.subcommand...)
}
}
// 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 {
@ -602,7 +486,7 @@ func (p *Parser) process(args []string) error {
// union of specs for the chain of subcommands encountered so far // union of specs for the chain of subcommands encountered so far
curCmd := p.cmd curCmd := p.cmd
p.subcommand = nil p.lastCmd = curCmd
// make a copy of the specs because we will add to this list each time we expand a subcommand // make a copy of the specs because we will add to this list each time we expand a subcommand
specs := make([]*spec, len(curCmd.specs)) specs := make([]*spec, len(curCmd.specs))
@ -616,15 +500,6 @@ func (p *Parser) process(args []string) error {
} }
} }
// determine if the current command has a version option spec
var hasVersionOption bool
for _, spec := range curCmd.specs {
if spec.long == "version" {
hasVersionOption = true
break
}
}
// process each string from the command line // process each string from the command line
var allpositional bool var allpositional bool
var positionals []string var positionals []string
@ -652,17 +527,10 @@ 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)
if v.IsNil() { v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers
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
if p.config.StrictSubcommands { specs = append(specs, subcmd.specs...)
specs = make([]*spec, len(subcmd.specs))
copy(specs, subcmd.specs)
} else {
specs = append(specs, subcmd.specs...)
}
// capture environment vars for these new options // capture environment vars for these new options
if !p.config.IgnoreEnv { if !p.config.IgnoreEnv {
@ -673,7 +541,7 @@ func (p *Parser) process(args []string) error {
} }
curCmd = subcmd curCmd = subcmd
p.subcommand = append(p.subcommand, arg) p.lastCmd = curCmd
continue continue
} }
@ -682,9 +550,7 @@ func (p *Parser) process(args []string) error {
case "-h", "--help": case "-h", "--help":
return ErrHelp return ErrHelp
case "--version": case "--version":
if !hasVersionOption && p.version != "" { return ErrVersion
return ErrVersion
}
} }
// check for an equals sign, as in "--foo=bar" // check for an equals sign, as in "--foo=bar"
@ -698,7 +564,7 @@ func (p *Parser) process(args []string) error {
// lookup the spec for this option (note that the "specs" slice changes as // lookup the spec for this option (note that the "specs" slice changes as
// we expand subcommands so it is better not to use a map) // we expand subcommands so it is better not to use a map)
spec := findOption(specs, opt) spec := findOption(specs, opt)
if spec == nil || opt == "" { if spec == nil {
return fmt.Errorf("unknown argument %s", arg) return fmt.Errorf("unknown argument %s", arg)
} }
wasPresent[spec] = true wasPresent[spec] = true
@ -787,26 +653,17 @@ func (p *Parser) process(args []string) error {
} }
if spec.required { if spec.required {
if spec.short == "" && spec.long == "" {
msg := fmt.Sprintf("environment variable %s is required", spec.env)
return errors.New(msg)
}
msg := fmt.Sprintf("%s is required", name) msg := fmt.Sprintf("%s is required", name)
if spec.env != "" { if spec.env != "" {
msg += " (or environment variable " + spec.env + ")" msg += " (or environment variable " + spec.env + ")"
} }
return errors.New(msg) return errors.New(msg)
} }
if spec.defaultVal != "" {
if spec.defaultValue.IsValid() && !p.config.IgnoreDefault { err := scalar.ParseValue(p.val(spec.dest), spec.defaultVal)
// One issue here is that if the user now modifies the value then if err != nil {
// the default value stored in the spec will be corrupted. There return fmt.Errorf("error processing default value for %s: %v", name, err)
// 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)
} }
} }
@ -867,11 +724,6 @@ func findSubcommand(cmds []*command, name string) *command {
if cmd.name == name { if cmd.name == name {
return cmd return cmd
} }
for _, alias := range cmd.aliases {
if alias == name {
return cmd
}
}
} }
return nil return nil
} }

View File

@ -2,7 +2,6 @@ package arg
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"net" "net"
"net/mail" "net/mail"
@ -96,21 +95,6 @@ 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
@ -227,14 +211,6 @@ func TestRequiredWithEnv(t *testing.T) {
require.Error(t, err, "--foo is required (or environment variable FOO)") require.Error(t, err, "--foo is required (or environment variable FOO)")
} }
func TestRequiredWithEnvOnly(t *testing.T) {
var args struct {
Foo string `arg:"required,--,-,env:FOO"`
}
_, err := parseWithEnv("", []string{}, &args)
require.Error(t, err, "environment variable FOO is required")
}
func TestShortFlag(t *testing.T) { func TestShortFlag(t *testing.T) {
var args struct { var args struct {
Foo string `arg:"-f"` Foo string `arg:"-f"`
@ -840,37 +816,6 @@ 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 TestRequiredEnvironmentOnlyVariableIsMissing(t *testing.T) {
var args struct {
Foo string `arg:"required,--,env:FOO"`
}
_, err := parseWithEnv("", []string{""}, &args)
assert.Error(t, err)
}
func TestOptionalEnvironmentOnlyVariable(t *testing.T) {
var args struct {
Foo string `arg:"env:FOO"`
}
_, err := parseWithEnv("", []string{}, &args)
assert.NoError(t, err)
}
func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) { func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
var args struct { var args struct {
Sub *struct { Sub *struct {
@ -883,51 +828,10 @@ func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
err = p.Parse([]string{"sub"}) err = p.Parse([]string{"sub"})
require.NoError(t, err) assert.NoError(t, err)
require.NotNil(t, args.Sub)
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) {
var exitCode int
var stdout bytes.Buffer
exit := func(code int) { exitCode = code }
p, err := NewParser(Config{Exit: exit, Out: &stdout}, &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, stdout.String(), tt.output)
})
}
}
type textUnmarshaler struct { type textUnmarshaler struct {
val int val int
} }
@ -1381,55 +1285,11 @@ func TestReuseParser(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} }
func TestNoVersion(t *testing.T) { func TestVersion(t *testing.T) {
var args struct{} var args struct{}
err := parse("--version", &args)
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
err = p.Parse([]string{"--version"})
assert.Error(t, err)
assert.NotEqual(t, ErrVersion, err)
}
func TestBuiltinVersion(t *testing.T) {
var args struct{}
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
p.version = "example 3.2.1"
err = p.Parse([]string{"--version"})
assert.Equal(t, ErrVersion, err) assert.Equal(t, ErrVersion, err)
}
func TestArgsVersion(t *testing.T) {
var args struct {
Version bool `arg:"--version"`
}
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
err = p.Parse([]string{"--version"})
require.NoError(t, err)
require.Equal(t, args.Version, true)
}
func TestArgsAndBuiltinVersion(t *testing.T) {
var args struct {
Version bool `arg:"--version"`
}
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
p.version = "example 3.2.1"
err = p.Parse([]string{"--version"})
require.NoError(t, err)
require.Equal(t, args.Version, true)
} }
func TestMultipleTerminates(t *testing.T) { func TestMultipleTerminates(t *testing.T) {
@ -1460,21 +1320,13 @@ func TestDefaultOptionValues(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 123, args.A) assert.Equal(t, 123, args.A)
if assert.NotNil(t, args.B) { assert.Equal(t, 123, *args.B)
assert.Equal(t, 123, *args.B)
}
assert.Equal(t, "xyz", args.C) assert.Equal(t, "xyz", args.C)
if assert.NotNil(t, args.D) { assert.Equal(t, "abc", *args.D)
assert.Equal(t, "abc", *args.D)
}
assert.Equal(t, 4.56, args.E) assert.Equal(t, 4.56, args.E)
if assert.NotNil(t, args.F) { assert.Equal(t, 1.23, *args.F)
assert.Equal(t, 1.23, *args.F) assert.True(t, args.G)
}
assert.True(t, args.G) assert.True(t, args.G)
if assert.NotNil(t, args.H) {
assert.True(t, *args.H)
}
} }
func TestDefaultUnparseable(t *testing.T) { func TestDefaultUnparseable(t *testing.T) {
@ -1483,7 +1335,7 @@ func TestDefaultUnparseable(t *testing.T) {
} }
err := parse("", &args) err := parse("", &args)
assert.EqualError(t, err, `.A: error processing default value: strconv.ParseInt: parsing "x": invalid syntax`) assert.EqualError(t, err, `error processing default value for --a: strconv.ParseInt: parsing "x": invalid syntax`)
} }
func TestDefaultPositionalValues(t *testing.T) { func TestDefaultPositionalValues(t *testing.T) {
@ -1502,21 +1354,13 @@ func TestDefaultPositionalValues(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 456, args.A) assert.Equal(t, 456, args.A)
if assert.NotNil(t, args.B) { assert.Equal(t, 789, *args.B)
assert.Equal(t, 789, *args.B)
}
assert.Equal(t, "abc", args.C) assert.Equal(t, "abc", args.C)
if assert.NotNil(t, args.D) { assert.Equal(t, "abc", *args.D)
assert.Equal(t, "abc", *args.D)
}
assert.Equal(t, 1.23, args.E) assert.Equal(t, 1.23, args.E)
if assert.NotNil(t, args.F) { assert.Equal(t, 1.23, *args.F)
assert.Equal(t, 1.23, *args.F) assert.True(t, args.G)
}
assert.True(t, args.G) assert.True(t, args.G)
if assert.NotNil(t, args.H) {
assert.True(t, *args.H)
}
} }
func TestDefaultValuesNotAllowedWithRequired(t *testing.T) { func TestDefaultValuesNotAllowedWithRequired(t *testing.T) {
@ -1530,7 +1374,7 @@ func TestDefaultValuesNotAllowedWithRequired(t *testing.T) {
func TestDefaultValuesNotAllowedWithSlice(t *testing.T) { func TestDefaultValuesNotAllowedWithSlice(t *testing.T) {
var args struct { var args struct {
A []int `default:"invalid"` // default values not allowed with slices A []int `default:"123"` // required not allowed with default!
} }
err := parse("", &args) err := parse("", &args)
@ -1547,193 +1391,68 @@ func TestUnexportedFieldsSkipped(t *testing.T) {
} }
func TestMustParseInvalidParser(t *testing.T) { func TestMustParseInvalidParser(t *testing.T) {
originalExit := osExit
originalStdout := stdout
defer func() {
osExit = originalExit
stdout = originalStdout
}()
var exitCode int var exitCode int
var stdout bytes.Buffer osExit = func(code int) { exitCode = code }
exit := func(code int) { exitCode = code } stdout = &bytes.Buffer{}
var args struct { var args struct {
CannotParse struct{} CannotParse struct{}
} }
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args) parser := MustParse(&args)
assert.Nil(t, parser) assert.Nil(t, parser)
assert.Equal(t, -1, exitCode) assert.Equal(t, -1, exitCode)
} }
func TestMustParsePrintsHelp(t *testing.T) { func TestMustParsePrintsHelp(t *testing.T) {
originalExit := osExit
originalStdout := stdout
originalArgs := os.Args originalArgs := os.Args
defer func() { defer func() {
osExit = originalExit
stdout = originalStdout
os.Args = originalArgs os.Args = originalArgs
}() }()
var exitCode *int
osExit = func(code int) { exitCode = &code }
os.Args = []string{"someprogram", "--help"} os.Args = []string{"someprogram", "--help"}
stdout = &bytes.Buffer{}
var exitCode int
var stdout bytes.Buffer
exit := func(code int) { exitCode = code }
var args struct{} var args struct{}
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args) parser := MustParse(&args)
assert.NotNil(t, parser) assert.NotNil(t, parser)
assert.Equal(t, 0, exitCode) require.NotNil(t, exitCode)
assert.Equal(t, 0, *exitCode)
} }
func TestMustParsePrintsVersion(t *testing.T) { func TestMustParsePrintsVersion(t *testing.T) {
originalExit := osExit
originalStdout := stdout
originalArgs := os.Args originalArgs := os.Args
defer func() { defer func() {
osExit = originalExit
stdout = originalStdout
os.Args = originalArgs os.Args = originalArgs
}() }()
var exitCode int var exitCode *int
var stdout bytes.Buffer osExit = func(code int) { exitCode = &code }
exit := func(code int) { exitCode = code }
os.Args = []string{"someprogram", "--version"} os.Args = []string{"someprogram", "--version"}
var b bytes.Buffer
stdout = &b
var args versioned var args versioned
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args) parser := MustParse(&args)
require.NotNil(t, parser) require.NotNil(t, parser)
assert.Equal(t, 0, exitCode) require.NotNil(t, exitCode)
assert.Equal(t, "example 3.2.1\n", stdout.String()) assert.Equal(t, 0, *exitCode)
} assert.Equal(t, "example 3.2.1\n", b.String())
type mapWithUnmarshalText struct {
val map[string]string
}
func (v *mapWithUnmarshalText) UnmarshalText(data []byte) error {
return json.Unmarshal(data, &v.val)
}
func TestTextUnmarshalerEmpty(t *testing.T) {
// based on https://github.com/alexflint/go-arg/issues/184
var args struct {
Config mapWithUnmarshalText `arg:"--config"`
}
err := parse("", &args)
require.NoError(t, err)
assert.Empty(t, args.Config)
}
func TestTextUnmarshalerEmptyPointer(t *testing.T) {
// a slight variant on https://github.com/alexflint/go-arg/issues/184
var args struct {
Config *mapWithUnmarshalText `arg:"--config"`
}
err := parse("", &args)
require.NoError(t, err)
assert.Nil(t, args.Config)
}
// similar to the above but also implements MarshalText
type mapWithMarshalText struct {
val map[string]string
}
func (v *mapWithMarshalText) MarshalText(data []byte) error {
return json.Unmarshal(data, &v.val)
}
func (v *mapWithMarshalText) UnmarshalText(data []byte) error {
return json.Unmarshal(data, &v.val)
}
func TestTextMarshalerUnmarshalerEmpty(t *testing.T) {
// based on https://github.com/alexflint/go-arg/issues/184
var args struct {
Config mapWithMarshalText `arg:"--config"`
}
err := parse("", &args)
require.NoError(t, err)
assert.Empty(t, args.Config)
}
func TestTextMarshalerUnmarshalerEmptyPointer(t *testing.T) {
// a slight variant on https://github.com/alexflint/go-arg/issues/184
var args struct {
Config *mapWithMarshalText `arg:"--config"`
}
err := parse("", &args)
require.NoError(t, err)
assert.Nil(t, args.Config)
}
func TestSubcommandGlobalFlag_Before(t *testing.T) {
var args struct {
Global bool `arg:"-g"`
Sub *struct {
} `arg:"subcommand"`
}
p, err := NewParser(Config{StrictSubcommands: false}, &args)
require.NoError(t, err)
err = p.Parse([]string{"-g", "sub"})
assert.NoError(t, err)
assert.True(t, args.Global)
}
func TestSubcommandGlobalFlag_InCommand(t *testing.T) {
var args struct {
Global bool `arg:"-g"`
Sub *struct {
} `arg:"subcommand"`
}
p, err := NewParser(Config{StrictSubcommands: false}, &args)
require.NoError(t, err)
err = p.Parse([]string{"sub", "-g"})
assert.NoError(t, err)
assert.True(t, args.Global)
}
func TestSubcommandGlobalFlag_Before_Strict(t *testing.T) {
var args struct {
Global bool `arg:"-g"`
Sub *struct {
} `arg:"subcommand"`
}
p, err := NewParser(Config{StrictSubcommands: true}, &args)
require.NoError(t, err)
err = p.Parse([]string{"-g", "sub"})
assert.NoError(t, err)
assert.True(t, args.Global)
}
func TestSubcommandGlobalFlag_InCommand_Strict(t *testing.T) {
var args struct {
Global bool `arg:"-g"`
Sub *struct {
} `arg:"subcommand"`
}
p, err := NewParser(Config{StrictSubcommands: true}, &args)
require.NoError(t, err)
err = p.Parse([]string{"sub", "-g"})
assert.Error(t, err)
}
func TestSubcommandGlobalFlag_InCommand_Strict_Inner(t *testing.T) {
var args struct {
Global bool `arg:"-g"`
Sub *struct {
Guard bool `arg:"-g"`
} `arg:"subcommand"`
}
p, err := NewParser(Config{StrictSubcommands: true}, &args)
require.NoError(t, err)
err = p.Parse([]string{"sub", "-g"})
require.NoError(t, err)
assert.False(t, args.Global)
require.NotNil(t, args.Sub)
assert.True(t, args.Sub.Guard)
} }

View File

@ -13,9 +13,9 @@ import (
var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem() var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()
// cardinality tracks how many tokens are expected for a given spec // cardinality tracks how many tokens are expected for a given spec
// - zero is a boolean, which does to expect any value // - zero is a boolean, which does to expect any value
// - one is an ordinary option that will be parsed from a single token // - one is an ordinary option that will be parsed from a single token
// - multiple is a slice or map that can accept zero or more tokens // - multiple is a slice or map that can accept zero or more tokens
type cardinality int type cardinality int
const ( const (
@ -74,10 +74,10 @@ func cardinalityOf(t reflect.Type) (cardinality, error) {
} }
} }
// isBoolean returns true if the type is a boolean or a pointer to a boolean // isBoolean returns true if the type can be parsed from a single string
func isBoolean(t reflect.Type) bool { func isBoolean(t reflect.Type) bool {
switch { switch {
case isTextUnmarshaler(t): case t.Implements(textUnmarshalerType):
return false return false
case t.Kind() == reflect.Bool: case t.Kind() == reflect.Bool:
return true return true
@ -88,11 +88,6 @@ func isBoolean(t reflect.Type) bool {
} }
} }
// isTextUnmarshaler returns true if the type or its pointer implements encoding.TextUnmarshaler
func isTextUnmarshaler(t reflect.Type) bool {
return t.Implements(textUnmarshalerType) || reflect.PtrTo(t).Implements(textUnmarshalerType)
}
// isExported returns true if the struct field name is exported // isExported returns true if the struct field name is exported
func isExported(field string) bool { func isExported(field string) bool {
r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8 r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8
@ -102,7 +97,7 @@ func isExported(field string) bool {
// isZero returns true if v contains the zero value for its type // isZero returns true if v contains the zero value for its type
func isZero(v reflect.Value) bool { func isZero(v reflect.Value) bool {
t := v.Type() t := v.Type()
if t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice || t.Kind() == reflect.Map || t.Kind() == reflect.Chan || t.Kind() == reflect.Interface { if t.Kind() == reflect.Slice || t.Kind() == reflect.Map {
return v.IsNil() return v.IsNil()
} }
if !t.Comparable() { if !t.Comparable() {

View File

@ -1,7 +1,5 @@
package arg package arg
import "fmt"
// Subcommand returns the user struct for the subcommand selected by // Subcommand returns the user struct for the subcommand selected by
// the command line arguments most recently processed by the parser. // the command line arguments most recently processed by the parser.
// The return value is always a pointer to a struct. If no subcommand // The return value is always a pointer to a struct. If no subcommand
@ -9,35 +7,31 @@ import "fmt"
// no command line arguments have been processed by this parser then it // no command line arguments have been processed by this parser then it
// returns nil. // returns nil.
func (p *Parser) Subcommand() interface{} { func (p *Parser) Subcommand() interface{} {
if len(p.subcommand) == 0 { if p.lastCmd == nil || p.lastCmd.parent == nil {
return nil return nil
} }
cmd, err := p.lookupCommand(p.subcommand...) return p.val(p.lastCmd.dest).Interface()
if err != nil {
return nil
}
return p.val(cmd.dest).Interface()
} }
// SubcommandNames returns the sequence of subcommands specified by the // SubcommandNames returns the sequence of subcommands specified by the
// user. If no subcommands were given then it returns an empty slice. // user. If no subcommands were given then it returns an empty slice.
func (p *Parser) SubcommandNames() []string { func (p *Parser) SubcommandNames() []string {
return p.subcommand if p.lastCmd == nil {
} return nil
// lookupCommand finds a subcommand based on a sequence of subcommand names. The
// first string should be a top-level subcommand, the next should be a child
// subcommand of that subcommand, and so on. If no strings are given then the
// root command is returned. If no such subcommand exists then an error is
// returned.
func (p *Parser) lookupCommand(path ...string) (*command, error) {
cmd := p.cmd
for _, name := range path {
found := findSubcommand(cmd.subcommands, name)
if found == nil {
return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
}
cmd = found
} }
return cmd, nil
// make a list of ancestor commands
var ancestors []string
cur := p.lastCmd
for cur.parent != nil { // we want to exclude the root
ancestors = append(ancestors, cur.name)
cur = cur.parent
}
// reverse the list
out := make([]string, len(ancestors))
for i := 0; i < len(ancestors); i++ {
out[i] = ancestors[len(ancestors)-i-1]
}
return out
} }

View File

@ -83,19 +83,6 @@ func TestNamedSubcommand(t *testing.T) {
assert.Equal(t, []string{"ls"}, p.SubcommandNames()) assert.Equal(t, []string{"ls"}, p.SubcommandNames())
} }
func TestSubcommandAliases(t *testing.T) {
type listCmd struct {
}
var args struct {
List *listCmd `arg:"subcommand:list|ls"`
}
p, err := pparse("ls", &args)
require.NoError(t, err)
assert.NotNil(t, args.List)
assert.Equal(t, args.List, p.Subcommand())
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
}
func TestEmptySubcommand(t *testing.T) { func TestEmptySubcommand(t *testing.T) {
type listCmd struct { type listCmd struct {
} }
@ -126,23 +113,6 @@ func TestTwoSubcommands(t *testing.T) {
assert.Equal(t, []string{"list"}, p.SubcommandNames()) assert.Equal(t, []string{"list"}, p.SubcommandNames())
} }
func TestTwoSubcommandsWithAliases(t *testing.T) {
type getCmd struct {
}
type listCmd struct {
}
var args struct {
Get *getCmd `arg:"subcommand:get|g"`
List *listCmd `arg:"subcommand:list|ls"`
}
p, err := pparse("ls", &args)
require.NoError(t, err)
assert.Nil(t, args.Get)
assert.NotNil(t, args.List)
assert.Equal(t, args.List, p.Subcommand())
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
}
func TestSubcommandsWithOptions(t *testing.T) { func TestSubcommandsWithOptions(t *testing.T) {
type getCmd struct { type getCmd struct {
Name string Name string
@ -305,60 +275,6 @@ func TestNestedSubcommands(t *testing.T) {
} }
} }
func TestNestedSubcommandsWithAliases(t *testing.T) {
type child struct{}
type parent struct {
Child *child `arg:"subcommand:child|ch"`
}
type grandparent struct {
Parent *parent `arg:"subcommand:parent|pa"`
}
type root struct {
Grandparent *grandparent `arg:"subcommand:grandparent|gp"`
}
{
var args root
p, err := pparse("gp parent child", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.NotNil(t, args.Grandparent.Parent)
require.NotNil(t, args.Grandparent.Parent.Child)
assert.Equal(t, args.Grandparent.Parent.Child, p.Subcommand())
assert.Equal(t, []string{"gp", "parent", "child"}, p.SubcommandNames())
}
{
var args root
p, err := pparse("grandparent pa", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.NotNil(t, args.Grandparent.Parent)
require.Nil(t, args.Grandparent.Parent.Child)
assert.Equal(t, args.Grandparent.Parent, p.Subcommand())
assert.Equal(t, []string{"grandparent", "pa"}, p.SubcommandNames())
}
{
var args root
p, err := pparse("grandparent", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.Nil(t, args.Grandparent.Parent)
assert.Equal(t, args.Grandparent, p.Subcommand())
assert.Equal(t, []string{"grandparent"}, p.SubcommandNames())
}
{
var args root
p, err := pparse("", &args)
require.NoError(t, err)
require.Nil(t, args.Grandparent)
assert.Nil(t, p.Subcommand())
assert.Empty(t, p.SubcommandNames())
}
}
func TestSubcommandsWithPositionals(t *testing.T) { func TestSubcommandsWithPositionals(t *testing.T) {
type listCmd struct { type listCmd struct {
Pattern string `arg:"positional"` Pattern string `arg:"positional"`
@ -495,14 +411,3 @@ func TestValForNilStruct(t *testing.T) {
v := p.val(path{fields: []reflect.StructField{subField, subField}}) v := p.val(path{fields: []reflect.StructField{subField, subField}})
assert.False(t, v.IsValid()) assert.False(t, v.IsValid())
} }
func TestSubcommandInvalidInternal(t *testing.T) {
// this situation should never arise in practice but still good to test for it
var cmd struct{}
p, err := NewParser(Config{}, &cmd)
require.NoError(t, err)
p.subcommand = []string{"should", "never", "happen"}
sub := p.Subcommand()
assert.Nil(t, sub)
}

184
usage.go
View File

@ -3,15 +3,23 @@ package arg
import ( import (
"fmt" "fmt"
"io" "io"
"os"
"strings" "strings"
) )
// the width of the left column // the width of the left column
const colWidth = 25 const colWidth = 25
// to allow monkey patching in tests
var (
stdout io.Writer = os.Stdout
stderr io.Writer = os.Stderr
osExit = os.Exit
)
// Fail prints usage information to stderr and exits with non-zero status // Fail prints usage information to stderr and exits with non-zero status
func (p *Parser) Fail(msg string) { func (p *Parser) Fail(msg string) {
p.FailSubcommand(msg) p.failWithSubcommand(msg, p.cmd)
} }
// FailSubcommand prints usage information for a specified subcommand to stderr, // FailSubcommand prints usage information for a specified subcommand to stderr,
@ -21,19 +29,28 @@ func (p *Parser) Fail(msg string) {
// a sequence of subcommand names starting with the top-level subcommand and so // a sequence of subcommand names starting with the top-level subcommand and so
// on down the tree. // on down the tree.
func (p *Parser) FailSubcommand(msg string, subcommand ...string) error { func (p *Parser) FailSubcommand(msg string, subcommand ...string) error {
err := p.WriteUsageForSubcommand(p.config.Out, subcommand...) cmd, err := p.lookupCommand(subcommand...)
if err != nil { if err != nil {
return err return err
} }
p.failWithSubcommand(msg, cmd)
fmt.Fprintln(p.config.Out, "error:", msg)
p.config.Exit(-1)
return nil return nil
} }
// failWithSubcommand prints usage information for the given subcommand to stderr and exits with non-zero status
func (p *Parser) failWithSubcommand(msg string, cmd *command) {
p.writeUsageForSubcommand(stderr, cmd)
fmt.Fprintln(stderr, "error:", msg)
osExit(-1)
}
// WriteUsage writes usage information to the given writer // WriteUsage writes usage information to the given writer
func (p *Parser) WriteUsage(w io.Writer) { func (p *Parser) WriteUsage(w io.Writer) {
p.WriteUsageForSubcommand(w, p.subcommand...) cmd := p.cmd
if p.lastCmd != nil {
cmd = p.lastCmd
}
p.writeUsageForSubcommand(w, cmd)
} }
// WriteUsageForSubcommand writes the usage information for a specified // WriteUsageForSubcommand writes the usage information for a specified
@ -46,7 +63,12 @@ func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) erro
if err != nil { if err != nil {
return err return err
} }
p.writeUsageForSubcommand(w, cmd)
return nil
}
// writeUsageForSubcommand writes usage information for the given subcommand
func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *command) {
var positionals, longOptions, shortOptions []*spec var positionals, longOptions, shortOptions []*spec
for _, spec := range cmd.specs { for _, spec := range cmd.specs {
switch { switch {
@ -63,10 +85,18 @@ func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) erro
fmt.Fprintln(w, p.version) fmt.Fprintln(w, p.version)
} }
// make a list of ancestor commands so that we print with full context
var ancestors []string
ancestor := cmd
for ancestor != nil {
ancestors = append(ancestors, ancestor.name)
ancestor = ancestor.parent
}
// print the beginning of the usage string // print the beginning of the usage string
fmt.Fprintf(w, "Usage: %s", p.cmd.name) fmt.Fprint(w, "Usage:")
for _, s := range subcommand { for i := len(ancestors) - 1; i >= 0; i-- {
fmt.Fprint(w, " "+s) fmt.Fprint(w, " "+ancestors[i])
} }
// write the option component of the usage message // write the option component of the usage message
@ -127,66 +157,47 @@ func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) erro
} }
fmt.Fprint(w, "\n") fmt.Fprint(w, "\n")
return nil
} }
// print prints a line like this: func printTwoCols(w io.Writer, left, help string, defaultVal string, envVal string) {
// lhs := " " + left
// --option FOO A description of the option [default: 123]
//
// If the text on the left is longer than a certain threshold, the description is moved to the next line:
//
// --verylongoptionoption VERY_LONG_VARIABLE
// A description of the option [default: 123]
//
// If multiple "extras" are provided then they are put inside a single set of square brackets:
//
// --option FOO A description of the option [default: 123, env: FOO]
func print(w io.Writer, item, description string, bracketed ...string) {
lhs := " " + item
fmt.Fprint(w, lhs) fmt.Fprint(w, lhs)
if description != "" { if help != "" {
if len(lhs)+2 < colWidth { if len(lhs)+2 < colWidth {
fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs))) fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs)))
} else { } else {
fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth)) fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
} }
fmt.Fprint(w, description) fmt.Fprint(w, help)
} }
var brack string bracketsContent := []string{}
for _, s := range bracketed {
if s != "" { if defaultVal != "" {
if brack != "" { bracketsContent = append(bracketsContent,
brack += ", " fmt.Sprintf("default: %s", defaultVal),
} )
brack += s
}
} }
if brack != "" { if envVal != "" {
fmt.Fprintf(w, " [%s]", brack) bracketsContent = append(bracketsContent,
fmt.Sprintf("env: %s", envVal),
)
}
if len(bracketsContent) > 0 {
fmt.Fprintf(w, " [%s]", strings.Join(bracketsContent, ", "))
} }
fmt.Fprint(w, "\n") fmt.Fprint(w, "\n")
} }
func withDefault(s string) string {
if s == "" {
return ""
}
return "default: " + s
}
func withEnv(env string) string {
if env == "" {
return ""
}
return "env: " + env
}
// WriteHelp writes the usage string followed by the full help string for each option // WriteHelp writes the usage string followed by the full help string for each option
func (p *Parser) WriteHelp(w io.Writer) { func (p *Parser) WriteHelp(w io.Writer) {
p.WriteHelpForSubcommand(w, p.subcommand...) cmd := p.cmd
if p.lastCmd != nil {
cmd = p.lastCmd
}
p.writeHelpForSubcommand(w, cmd)
} }
// WriteHelpForSubcommand writes the usage string followed by the full help // WriteHelpForSubcommand writes the usage string followed by the full help
@ -199,9 +210,13 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
if err != nil { if err != nil {
return err return err
} }
p.writeHelpForSubcommand(w, cmd)
return nil
}
var positionals, longOptions, shortOptions, envOnlyOptions []*spec // writeHelp writes the usage string for the given subcommand
var hasVersionOption bool func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
var positionals, longOptions, shortOptions []*spec
for _, spec := range cmd.specs { for _, spec := range cmd.specs {
switch { switch {
case spec.positional: case spec.positional:
@ -210,21 +225,19 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
longOptions = append(longOptions, spec) longOptions = append(longOptions, spec)
case spec.short != "": case spec.short != "":
shortOptions = append(shortOptions, spec) shortOptions = append(shortOptions, spec)
case spec.short == "" && spec.long == "":
envOnlyOptions = append(envOnlyOptions, spec)
} }
} }
if p.description != "" { if p.description != "" {
fmt.Fprintln(w, p.description) fmt.Fprintln(w, p.description)
} }
p.WriteUsageForSubcommand(w, subcommand...) p.writeUsageForSubcommand(w, cmd)
// write the list of positionals // write the list of positionals
if len(positionals) > 0 { if len(positionals) > 0 {
fmt.Fprint(w, "\nPositional arguments:\n") fmt.Fprint(w, "\nPositional arguments:\n")
for _, spec := range positionals { for _, spec := range positionals {
print(w, spec.placeholder, spec.help) printTwoCols(w, spec.placeholder, spec.help, "", "")
} }
} }
@ -236,9 +249,6 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
} }
for _, spec := range longOptions { for _, spec := range longOptions {
p.printOption(w, spec) p.printOption(w, spec)
if spec.long == "version" {
hasVersionOption = true
}
} }
} }
@ -255,9 +265,6 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
fmt.Fprint(w, "\nGlobal options:\n") fmt.Fprint(w, "\nGlobal options:\n")
for _, spec := range globals { for _, spec := range globals {
p.printOption(w, spec) p.printOption(w, spec)
if spec.long == "version" {
hasVersionOption = true
}
} }
} }
@ -268,7 +275,7 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
short: "h", short: "h",
help: "display this help and exit", help: "display this help and exit",
}) })
if !hasVersionOption && p.version != "" { if p.version != "" {
p.printOption(w, &spec{ p.printOption(w, &spec{
cardinality: zero, cardinality: zero,
long: "version", long: "version",
@ -276,27 +283,13 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
}) })
} }
// write the list of environment only variables
if len(envOnlyOptions) > 0 {
fmt.Fprint(w, "\nEnvironment variables:\n")
for _, spec := range envOnlyOptions {
p.printEnvOnlyVar(w, spec)
}
}
// write the list of subcommands // write the list of subcommands
if len(cmd.subcommands) > 0 { if len(cmd.subcommands) > 0 {
fmt.Fprint(w, "\nCommands:\n") fmt.Fprint(w, "\nCommands:\n")
for _, subcmd := range cmd.subcommands { for _, subcmd := range cmd.subcommands {
names := append([]string{subcmd.name}, subcmd.aliases...) printTwoCols(w, subcmd.name, subcmd.help, "", "")
print(w, strings.Join(names, ", "), subcmd.help)
} }
} }
if p.epilogue != "" {
fmt.Fprintln(w, "\n"+p.epilogue)
}
return nil
} }
func (p *Parser) printOption(w io.Writer, spec *spec) { func (p *Parser) printOption(w io.Writer, spec *spec) {
@ -308,23 +301,30 @@ func (p *Parser) printOption(w io.Writer, spec *spec) {
ways = append(ways, synopsis(spec, "-"+spec.short)) ways = append(ways, synopsis(spec, "-"+spec.short))
} }
if len(ways) > 0 { if len(ways) > 0 {
print(w, strings.Join(ways, ", "), spec.help, withDefault(spec.defaultString), withEnv(spec.env)) printTwoCols(w, strings.Join(ways, ", "), spec.help, spec.defaultVal, spec.env)
} }
} }
func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) { // lookupCommand finds a subcommand based on a sequence of subcommand names. The
ways := make([]string, 0, 2) // first string should be a top-level subcommand, the next should be a child
if spec.required { // subcommand of that subcommand, and so on. If no strings are given then the
ways = append(ways, "Required.") // root command is returned. If no such subcommand exists then an error is
} else { // returned.
ways = append(ways, "Optional.") func (p *Parser) lookupCommand(path ...string) (*command, error) {
cmd := p.cmd
for _, name := range path {
var found *command
for _, child := range cmd.subcommands {
if child.name == name {
found = child
}
}
if found == nil {
return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
}
cmd = found
} }
return cmd, nil
if spec.help != "" {
ways = append(ways, spec.help)
}
print(w, spec.env, strings.Join(ways, " "), withDefault(spec.defaultString))
} }
func synopsis(spec *spec, form string) string { func synopsis(spec *spec, form string) string {

View File

@ -50,16 +50,12 @@ Options:
--optimize OPTIMIZE, -O OPTIMIZE --optimize OPTIMIZE, -O OPTIMIZE
optimization level optimization level
--ids IDS Ids --ids IDS Ids
--values VALUES Values --values VALUES Values [default: [3.14 42 256]]
--workers WORKERS, -w WORKERS --workers WORKERS, -w WORKERS
number of workers to start [default: 10, env: WORKERS] number of workers to start [default: 10, env: WORKERS]
--testenv TESTENV, -a TESTENV [env: TEST_ENV] --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 [default: scratch.txt]
--help, -h display this help and exit --help, -h display this help and exit
Environment variables:
API_KEY Required. Only via env-var for security reasons
TRACE Optional. Record low-level trace
` `
var args struct { var args struct {
@ -74,12 +70,11 @@ Environment variables:
Values []float64 `help:"Values"` Values []float64 `help:"Values"`
Workers int `arg:"-w,env:WORKERS" help:"number of workers to start" default:"10"` Workers int `arg:"-w,env:WORKERS" help:"number of workers to start" default:"10"`
TestEnv string `arg:"-a,env:TEST_ENV"` TestEnv string `arg:"-a,env:TEST_ENV"`
ApiKey string `arg:"required,-,--,env:API_KEY" help:"Only via env-var for security reasons"`
Trace bool `arg:"-,--,env" help:"Record low-level trace"`
File *NameDotName `arg:"-f" help:"File with mandatory extension"` File *NameDotName `arg:"-f" help:"File with mandatory extension"`
} }
args.Name = "Foo Bar" args.Name = "Foo Bar"
args.Value = 42 args.Value = 42
args.Values = []float64{3.14, 42, 256}
args.File = &NameDotName{"scratch", "txt"} args.File = &NameDotName{"scratch", "txt"}
p, err := NewParser(Config{Program: "example"}, &args) p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err) require.NoError(t, err)
@ -290,37 +285,6 @@ 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 {
@ -450,8 +414,6 @@ Global options:
_ = p.Parse([]string{"child", "nested", "value"}) _ = p.Parse([]string{"child", "nested", "value"})
assert.Equal(t, []string{"child", "nested"}, p.SubcommandNames())
var help bytes.Buffer var help bytes.Buffer
p.WriteHelp(&help) p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String()) assert.Equal(t, expectedHelp[1:], help.String())
@ -473,7 +435,7 @@ func TestNonexistentSubcommand(t *testing.T) {
var args struct { var args struct {
sub *struct{} `arg:"subcommand"` sub *struct{} `arg:"subcommand"`
} }
p, err := NewParser(Config{Exit: func(int) {}}, &args) p, err := NewParser(Config{}, &args)
require.NoError(t, err) require.NoError(t, err)
var b bytes.Buffer var b bytes.Buffer
@ -513,7 +475,7 @@ Options:
ShortOnly2 string `arg:"-b,--,required" help:"some help2"` ShortOnly2 string `arg:"-b,--,required" help:"some help2"`
} }
p, err := NewParser(Config{Program: "example"}, &args) p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err) assert.NoError(t, err)
var help bytes.Buffer var help bytes.Buffer
p.WriteHelp(&help) p.WriteHelp(&help)
@ -560,16 +522,10 @@ Usage: example [-s SHORT]
Options: Options:
-s SHORT [env: SHORT] -s SHORT [env: SHORT]
--help, -h display this help and exit --help, -h display this help and exit
Environment variables:
ENVONLY Optional.
ENVONLY2 Optional.
CUSTOM Optional.
` `
var args struct { var args struct {
Short string `arg:"--,-s,env"` Short string `arg:"--,-s,env"`
EnvOnly string `arg:"--,env"` EnvOnly string `arg:"--,env"`
EnvOnly2 string `arg:"--,-,env"`
EnvOnlyOverriden string `arg:"--,env:CUSTOM"` EnvOnlyOverriden string `arg:"--,env:CUSTOM"`
} }
@ -585,39 +541,19 @@ Environment variables:
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String())) assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
} }
func TestEnvOnlyArgs(t *testing.T) {
expectedUsage := "Usage: example [--arg ARG]"
expectedHelp := `
Usage: example [--arg ARG]
Options:
--arg ARG, -a ARG [env: MY_ARG]
--help, -h display this help and exit
Environment variables:
AUTH_KEY Required.
`
var args struct {
ArgParam string `arg:"-a,--arg,env:MY_ARG"`
AuthKey string `arg:"required,--,env:AUTH_KEY"`
}
p, err := NewParser(Config{Program: "example"}, &args)
assert.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 TestFail(t *testing.T) { func TestFail(t *testing.T) {
var stdout bytes.Buffer originalStderr := stderr
originalExit := osExit
defer func() {
stderr = originalStderr
osExit = originalExit
}()
var b bytes.Buffer
stderr = &b
var exitCode int var exitCode int
exit := func(code int) { exitCode = code } osExit = func(code int) { exitCode = code }
expectedStdout := ` expectedStdout := `
Usage: example [--foo FOO] Usage: example [--foo FOO]
@ -627,18 +563,27 @@ error: something went wrong
var args struct { var args struct {
Foo int Foo int
} }
p, err := NewParser(Config{Program: "example", Exit: exit, Out: &stdout}, &args) p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err) require.NoError(t, err)
p.Fail("something went wrong") p.Fail("something went wrong")
assert.Equal(t, expectedStdout[1:], stdout.String()) assert.Equal(t, expectedStdout[1:], b.String())
assert.Equal(t, -1, exitCode) assert.Equal(t, -1, exitCode)
} }
func TestFailSubcommand(t *testing.T) { func TestFailSubcommand(t *testing.T) {
var stdout bytes.Buffer originalStderr := stderr
originalExit := osExit
defer func() {
stderr = originalStderr
osExit = originalExit
}()
var b bytes.Buffer
stderr = &b
var exitCode int var exitCode int
exit := func(code int) { exitCode = code } osExit = func(code int) { exitCode = code }
expectedStdout := ` expectedStdout := `
Usage: example sub Usage: example sub
@ -648,70 +593,12 @@ error: something went wrong
var args struct { var args struct {
Sub *struct{} `arg:"subcommand"` Sub *struct{} `arg:"subcommand"`
} }
p, err := NewParser(Config{Program: "example", Exit: exit, Out: &stdout}, &args) p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err) require.NoError(t, err)
err = p.FailSubcommand("something went wrong", "sub") err = p.FailSubcommand("something went wrong", "sub")
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, expectedStdout[1:], stdout.String()) assert.Equal(t, expectedStdout[1:], b.String())
assert.Equal(t, -1, exitCode) assert.Equal(t, -1, exitCode)
} }
type lengthOf struct {
Length int
}
func (p *lengthOf) UnmarshalText(b []byte) error {
p.Length = len(b)
return nil
}
func TestHelpShowsDefaultValueFromOriginalTag(t *testing.T) {
// check that the usage text prints the original string from the default tag, not
// the serialization of the parsed value
expectedHelp := `
Usage: example [--test TEST]
Options:
--test TEST [default: some_default_value]
--help, -h display this help and exit
`
var args struct {
Test *lengthOf `default:"some_default_value"`
}
p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
}
func TestHelpShowsSubcommandAliases(t *testing.T) {
expectedHelp := `
Usage: example <command> [<args>]
Options:
--help, -h display this help and exit
Commands:
remove, rm, r remove something from somewhere
simple do something simple
halt, stop stop now
`
var args struct {
Remove *struct{} `arg:"subcommand:remove|rm|r" help:"remove something from somewhere"`
Simple *struct{} `arg:"subcommand" help:"do something simple"`
Stop *struct{} `arg:"subcommand:halt|stop" help:"stop now"`
}
p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
}