Compare commits
No commits in common. "register" and "v1.4.3" have entirely different histories.
|
@ -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 .
|
||||||
|
|
23
Makefile
23
Makefile
|
@ -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
|
|
54
README.md
54
README.md
|
@ -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*
|
||||||
|
|
|
@ -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
14
go.mod
|
@ -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
14
go.sum
|
@ -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
304
parse.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
373
parse_test.go
373
parse_test.go
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
17
reflect.go
17
reflect.go
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
184
usage.go
|
@ -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 {
|
||||||
|
|
173
usage_test.go
173
usage_test.go
|
@ -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())
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue