Compare commits
51 Commits
Author | SHA1 | Date |
---|---|---|
|
119b334118 | |
|
b698745ef7 | |
|
625ef41f20 | |
|
2f59cd70aa | |
|
f9208bdffb | |
|
42d240c6c1 | |
|
bf629a16cb | |
|
e7a4f77ed0 | |
|
960d38c3ce | |
|
0142b0b842 | |
|
5ec29ce755 | |
|
8e9f60aafc | |
|
660b9045e1 | |
|
c73f38cd54 | |
|
463902ef7d | |
|
259c83fd5a | |
|
18623d869b | |
|
b928a1839a | |
|
ccf62e0ffc | |
|
5f10667949 | |
|
c3cac76438 | |
|
0280e6e591 | |
|
e25b4707a7 | |
|
df28e7154b | |
|
5dbdd5d0c5 | |
|
efae1938fd | |
|
c0a8e20a0a | |
|
5036dce2d6 | |
|
cef66fd2f6 | |
|
727f8533ac | |
|
3489ea5b2e | |
|
763072452f | |
|
3d95a706a6 | |
|
d949871b67 | |
|
9d5e97ac8a | |
|
67f7183b85 | |
|
522dbbcea8 | |
|
27c832b934 | |
|
197e226c77 | |
|
dbc2ba5d0c | |
|
4fc9666f79 | |
|
11f9b624a9 | |
|
7f4979a06e | |
|
0c21f821f8 | |
|
ea0f540c40 | |
|
74af96c6cc | |
|
c8b9567d1b | |
|
ebd7a68a06 | |
|
23b2b67fe2 | |
|
b48371a62f | |
|
a87d80089a |
|
@ -15,17 +15,17 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
go: ['1.13', '1.14', '1.15', '1.16']
|
go: ['1.17', '1.18', '1.19']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: go
|
- id: go
|
||||||
name: Set up Go
|
name: Set up Go
|
||||||
uses: actions/setup-go@v1
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build -v .
|
run: go build -v .
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# 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
|
50
README.md
50
README.md
|
@ -180,6 +180,24 @@ var args struct {
|
||||||
arg.MustParse(&args)
|
arg.MustParse(&args)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Ignoring environment variables and/or default values
|
||||||
|
|
||||||
|
The values in an existing structure can be kept in-tact by ignoring environment
|
||||||
|
variables and/or default values.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Test string `arg:"-t,env:TEST" default:"something"`
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := arg.NewParser(arg.Config{
|
||||||
|
IgnoreEnv: true,
|
||||||
|
IgnoreDefault: true,
|
||||||
|
}, &args)
|
||||||
|
|
||||||
|
err = p.Parse(os.Args)
|
||||||
|
```
|
||||||
|
|
||||||
### Arguments with multiple values
|
### Arguments with multiple values
|
||||||
```go
|
```go
|
||||||
var args struct {
|
var args struct {
|
||||||
|
@ -444,6 +462,9 @@ Options:
|
||||||
|
|
||||||
### Description strings
|
### Description strings
|
||||||
|
|
||||||
|
A descriptive message can be added at the top of the help text by implementing
|
||||||
|
a `Description` function that returns a string.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type args struct {
|
type args struct {
|
||||||
Foo string
|
Foo string
|
||||||
|
@ -469,6 +490,35 @@ Options:
|
||||||
--help, -h display this help and exit
|
--help, -h display this help and exit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Similarly an epilogue can be added at the end of the help text by implementing
|
||||||
|
the `Epilogue` function.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type args struct {
|
||||||
|
Foo string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (args) Epilogue() string {
|
||||||
|
return "For more information visit github.com/alexflint/go-arg"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var args args
|
||||||
|
arg.MustParse(&args)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./example -h
|
||||||
|
Usage: example [--foo FOO]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--foo FOO
|
||||||
|
--help, -h display this help and exit
|
||||||
|
|
||||||
|
For more information visit github.com/alexflint/go-arg
|
||||||
|
```
|
||||||
|
|
||||||
### Subcommands
|
### Subcommands
|
||||||
|
|
||||||
*Introduced in version 1.1.0*
|
*Introduced in version 1.1.0*
|
||||||
|
|
|
@ -162,8 +162,7 @@ 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
|
||||||
osExit = func(int) {}
|
mustParseExit = func(int) {}
|
||||||
stdout = os.Stdout
|
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -195,8 +194,7 @@ 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
|
||||||
osExit = func(int) {}
|
mustParseExit = func(int) {}
|
||||||
stdout = os.Stdout
|
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -236,8 +234,7 @@ 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
|
||||||
osExit = func(int) {}
|
mustParseExit = func(int) {}
|
||||||
stdout = os.Stdout
|
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -274,8 +271,7 @@ 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
|
||||||
osExit = func(int) {}
|
mustParseExit = func(int) {}
|
||||||
stdout = os.Stdout
|
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -311,10 +307,9 @@ 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
|
||||||
osExit = func(int) {}
|
exit := func(int) {}
|
||||||
stdout = os.Stdout
|
|
||||||
|
|
||||||
p, err := NewParser(Config{}, &args)
|
p, err := NewParser(Config{Exit: exit}, &args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -360,10 +355,9 @@ 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
|
||||||
osExit = func(int) {}
|
exit := func(int) {}
|
||||||
stdout = os.Stdout
|
|
||||||
|
|
||||||
p, err := NewParser(Config{}, &args)
|
p, err := NewParser(Config{Exit: exit}, &args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -397,8 +391,7 @@ 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
|
||||||
osExit = func(int) {}
|
mustParseExit = func(int) {}
|
||||||
stderr = os.Stdout
|
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -421,8 +414,7 @@ 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
|
||||||
osExit = func(int) {}
|
mustParseExit = func(int) {}
|
||||||
stderr = os.Stdout
|
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -457,8 +449,7 @@ 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
|
||||||
osExit = func(int) {}
|
mustParseExit = func(int) {}
|
||||||
stderr = os.Stdout
|
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -505,3 +496,45 @@ 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,8 +1,14 @@
|
||||||
module github.com/alexflint/go-arg
|
module go.wit.com/dev/alexflint/go-arg
|
||||||
|
|
||||||
|
go 1.21.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexflint/go-scalar v1.1.0
|
github.com/alexflint/go-scalar v1.2.0
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.8.4
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.13
|
require (
|
||||||
|
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,15 +1,13 @@
|
||||||
github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM=
|
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
||||||
github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 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.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
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.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
258
parse.go
258
parse.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
@ -53,13 +54,15 @@ type spec struct {
|
||||||
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
|
||||||
defaultVal string // default value for this option
|
defaultValue reflect.Value // 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
|
||||||
|
@ -67,33 +70,55 @@ type command struct {
|
||||||
parent *command
|
parent *command
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrHelp indicates that -h or --help were provided
|
// ErrHelp indicates that the builtin -h or --help were provided
|
||||||
var ErrHelp = errors.New("help requested by user")
|
var ErrHelp = errors.New("help requested by user")
|
||||||
|
|
||||||
// ErrVersion indicates that --version was provided
|
// ErrVersion indicates that the builtin --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 {
|
||||||
p, err := NewParser(Config{}, dest...)
|
register = append(register, 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(stdout, err)
|
fmt.Fprintln(config.Out, err)
|
||||||
osExit(-1)
|
config.Exit(-1)
|
||||||
return nil // just in case osExit was monkey-patched
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,6 +146,20 @@ 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
|
||||||
|
@ -130,9 +169,10 @@ type Parser struct {
|
||||||
config Config
|
config Config
|
||||||
version string
|
version string
|
||||||
description string
|
description string
|
||||||
|
epilogue string
|
||||||
|
|
||||||
// the following field changes during processing of command line arguments
|
// the following field changes during processing of command line arguments
|
||||||
lastCmd *command
|
subcommand []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Versioned is the interface that the destination struct should implement to
|
// Versioned is the interface that the destination struct should implement to
|
||||||
|
@ -151,6 +191,14 @@ type Described interface {
|
||||||
Description() string
|
Description() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Epilogued is the interface that the destination struct should implement to
|
||||||
|
// add an epilogue string at the bottom of the help message.
|
||||||
|
type Epilogued interface {
|
||||||
|
// Epilogue returns the string that will be printed on a line by itself
|
||||||
|
// at the end of the help message.
|
||||||
|
Epilogue() string
|
||||||
|
}
|
||||||
|
|
||||||
// walkFields calls a function for each field of a struct, recursively expanding struct fields.
|
// walkFields calls a function for each field of a struct, recursively expanding struct fields.
|
||||||
func walkFields(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool) {
|
func walkFields(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool) {
|
||||||
walkFieldsImpl(t, visit, nil)
|
walkFieldsImpl(t, visit, nil)
|
||||||
|
@ -174,6 +222,14 @@ 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 {
|
||||||
|
@ -208,18 +264,31 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// add nonzero field values as defaults
|
// for backwards compatibility, 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 {
|
||||||
if v := p.val(spec.dest); v.IsValid() && !isZero(v) {
|
// get the value
|
||||||
if defaultVal, ok := v.Interface().(encoding.TextMarshaler); ok {
|
v := p.val(spec.dest)
|
||||||
str, err := defaultVal.MarshalText()
|
|
||||||
|
// if the value is the "zero value" (e.g. nil pointer, empty struct) then ignore
|
||||||
|
if isZero(v) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// store as a default
|
||||||
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
|
return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
|
||||||
}
|
}
|
||||||
spec.defaultVal = string(str)
|
spec.defaultString = string(s)
|
||||||
} else {
|
} else {
|
||||||
spec.defaultVal = fmt.Sprintf("%v", v)
|
spec.defaultString = fmt.Sprintf("%v", v)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,6 +301,9 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
||||||
if dest, ok := dest.(Described); ok {
|
if dest, ok := dest.(Described); ok {
|
||||||
p.description = dest.Description()
|
p.description = dest.Description()
|
||||||
}
|
}
|
||||||
|
if dest, ok := dest.(Epilogued); ok {
|
||||||
|
p.epilogue = dest.Epilogue()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &p, nil
|
return &p, nil
|
||||||
|
@ -288,13 +360,9 @@ 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
|
||||||
|
@ -312,18 +380,13 @@ 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
|
||||||
|
@ -340,18 +403,24 @@ 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
|
||||||
cmdname := value
|
var cmdnames []string
|
||||||
if cmdname == "" {
|
if value == "" {
|
||||||
cmdname = strings.ToLower(field.Name)
|
cmdnames = []string{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(cmdname, subdest, field.Type)
|
subcmd, err := cmdFromStruct(cmdnames[0], 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")
|
||||||
|
|
||||||
|
@ -372,13 +441,15 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether this field is supported. It's good to do this here rather than
|
// if this is a subcommand then we've done everything we need to do
|
||||||
|
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.
|
||||||
if !isSubcommand {
|
|
||||||
cmd.specs = append(cmd.specs, &spec)
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
spec.cardinality, err = cardinalityOf(field.Type)
|
spec.cardinality, err = cardinalityOf(field.Type)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -386,13 +457,44 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
||||||
t.Name(), field.Name, field.Type.String()))
|
t.Name(), field.Name, field.Type.String()))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if spec.cardinality == multiple && hasDefault {
|
|
||||||
|
defaultString, hasDefault := field.Tag.Lookup("default")
|
||||||
|
if hasDefault {
|
||||||
|
// we do not support default values for maps and slices
|
||||||
|
if spec.cardinality == multiple {
|
||||||
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
|
||||||
})
|
})
|
||||||
|
@ -433,6 +535,20 @@ func (p *Parser) Parse(args []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Parser) MustParse(args []string) {
|
||||||
|
err := p.Parse(args)
|
||||||
|
switch {
|
||||||
|
case err == ErrHelp:
|
||||||
|
p.WriteHelpForSubcommand(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 {
|
||||||
|
@ -486,7 +602,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.lastCmd = curCmd
|
p.subcommand = nil
|
||||||
|
|
||||||
// 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))
|
||||||
|
@ -500,6 +616,15 @@ 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
|
||||||
|
@ -527,10 +652,17 @@ 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 = make([]*spec, len(subcmd.specs))
|
||||||
|
copy(specs, subcmd.specs)
|
||||||
|
} else {
|
||||||
specs = append(specs, subcmd.specs...)
|
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 {
|
||||||
|
@ -541,7 +673,7 @@ func (p *Parser) process(args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
curCmd = subcmd
|
curCmd = subcmd
|
||||||
p.lastCmd = curCmd
|
p.subcommand = append(p.subcommand, arg)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -550,8 +682,10 @@ 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"
|
||||||
var value string
|
var value string
|
||||||
|
@ -564,7 +698,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 {
|
if spec == nil || opt == "" {
|
||||||
return fmt.Errorf("unknown argument %s", arg)
|
return fmt.Errorf("unknown argument %s", arg)
|
||||||
}
|
}
|
||||||
wasPresent[spec] = true
|
wasPresent[spec] = true
|
||||||
|
@ -653,17 +787,26 @@ 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 != "" {
|
|
||||||
err := scalar.ParseValue(p.val(spec.dest), spec.defaultVal)
|
if spec.defaultValue.IsValid() && !p.config.IgnoreDefault {
|
||||||
if err != nil {
|
// One issue here is that if the user now modifies the value then
|
||||||
return fmt.Errorf("error processing default value for %s: %v", name, err)
|
// the default value stored in the spec will be corrupted. There
|
||||||
}
|
// is no general way to "deep-copy" values in Go, and we still
|
||||||
|
// support the old-style method for specifying defaults as
|
||||||
|
// Go values assigned directly to the struct field, so we are stuck.
|
||||||
|
p.val(spec.dest).Set(spec.defaultValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -724,6 +867,11 @@ 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
|
||||||
}
|
}
|
||||||
|
|
363
parse_test.go
363
parse_test.go
|
@ -2,6 +2,7 @@ package arg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
@ -95,6 +96,21 @@ func TestInt(t *testing.T) {
|
||||||
assert.EqualValues(t, 8, *args.Ptr)
|
assert.EqualValues(t, 8, *args.Ptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHexOctBin(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
Hex int
|
||||||
|
Oct int
|
||||||
|
Bin int
|
||||||
|
Underscored int
|
||||||
|
}
|
||||||
|
err := parse("--hex 0xA --oct 0o10 --bin 0b101 --underscored 123_456", &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.EqualValues(t, 10, args.Hex)
|
||||||
|
assert.EqualValues(t, 8, args.Oct)
|
||||||
|
assert.EqualValues(t, 5, args.Bin)
|
||||||
|
assert.EqualValues(t, 123456, args.Underscored)
|
||||||
|
}
|
||||||
|
|
||||||
func TestNegativeInt(t *testing.T) {
|
func TestNegativeInt(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo int
|
Foo int
|
||||||
|
@ -211,6 +227,14 @@ 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"`
|
||||||
|
@ -816,6 +840,37 @@ 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 {
|
||||||
|
@ -828,10 +883,51 @@ func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = p.Parse([]string{"sub"})
|
err = p.Parse([]string{"sub"})
|
||||||
assert.NoError(t, err)
|
require.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
|
||||||
}
|
}
|
||||||
|
@ -1285,11 +1381,55 @@ func TestReuseParser(t *testing.T) {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVersion(t *testing.T) {
|
func TestNoVersion(t *testing.T) {
|
||||||
var args struct{}
|
var args struct{}
|
||||||
err := parse("--version", &args)
|
|
||||||
assert.Equal(t, ErrVersion, err)
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
@ -1320,13 +1460,21 @@ 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) {
|
||||||
|
@ -1335,7 +1483,7 @@ func TestDefaultUnparseable(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err := parse("", &args)
|
err := parse("", &args)
|
||||||
assert.EqualError(t, err, `error processing default value for --a: strconv.ParseInt: parsing "x": invalid syntax`)
|
assert.EqualError(t, err, `.A: error processing default value: strconv.ParseInt: parsing "x": invalid syntax`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultPositionalValues(t *testing.T) {
|
func TestDefaultPositionalValues(t *testing.T) {
|
||||||
|
@ -1354,13 +1502,21 @@ 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) {
|
||||||
|
@ -1374,7 +1530,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:"123"` // required not allowed with default!
|
A []int `default:"invalid"` // default values not allowed with slices
|
||||||
}
|
}
|
||||||
|
|
||||||
err := parse("", &args)
|
err := parse("", &args)
|
||||||
|
@ -1391,68 +1547,193 @@ 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
|
||||||
osExit = func(code int) { exitCode = code }
|
var stdout bytes.Buffer
|
||||||
stdout = &bytes.Buffer{}
|
exit := func(code int) { exitCode = code }
|
||||||
|
|
||||||
var args struct {
|
var args struct {
|
||||||
CannotParse struct{}
|
CannotParse struct{}
|
||||||
}
|
}
|
||||||
parser := MustParse(&args)
|
parser := mustParse(Config{Out: &stdout, Exit: exit}, &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(&args)
|
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
|
||||||
assert.NotNil(t, parser)
|
assert.NotNil(t, parser)
|
||||||
require.NotNil(t, exitCode)
|
assert.Equal(t, 0, 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
|
||||||
osExit = func(code int) { exitCode = &code }
|
var stdout bytes.Buffer
|
||||||
|
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(&args)
|
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
|
||||||
require.NotNil(t, parser)
|
require.NotNil(t, parser)
|
||||||
require.NotNil(t, exitCode)
|
assert.Equal(t, 0, exitCode)
|
||||||
assert.Equal(t, 0, *exitCode)
|
assert.Equal(t, "example 3.2.1\n", stdout.String())
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
11
reflect.go
11
reflect.go
|
@ -74,10 +74,10 @@ func cardinalityOf(t reflect.Type) (cardinality, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// isBoolean returns true if the type can be parsed from a single string
|
// isBoolean returns true if the type is a boolean or a pointer to a boolean
|
||||||
func isBoolean(t reflect.Type) bool {
|
func isBoolean(t reflect.Type) bool {
|
||||||
switch {
|
switch {
|
||||||
case t.Implements(textUnmarshalerType):
|
case isTextUnmarshaler(t):
|
||||||
return false
|
return false
|
||||||
case t.Kind() == reflect.Bool:
|
case t.Kind() == reflect.Bool:
|
||||||
return true
|
return true
|
||||||
|
@ -88,6 +88,11 @@ 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
|
||||||
|
@ -97,7 +102,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.Slice || t.Kind() == reflect.Map {
|
if t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice || t.Kind() == reflect.Map || t.Kind() == reflect.Chan || t.Kind() == reflect.Interface {
|
||||||
return v.IsNil()
|
return v.IsNil()
|
||||||
}
|
}
|
||||||
if !t.Comparable() {
|
if !t.Comparable() {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
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
|
||||||
|
@ -7,31 +9,35 @@ package arg
|
||||||
// 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 p.lastCmd == nil || p.lastCmd.parent == nil {
|
if len(p.subcommand) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return p.val(p.lastCmd.dest).Interface()
|
cmd, err := p.lookupCommand(p.subcommand...)
|
||||||
|
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 {
|
||||||
if p.lastCmd == nil {
|
return p.subcommand
|
||||||
return nil
|
}
|
||||||
}
|
|
||||||
|
// lookupCommand finds a subcommand based on a sequence of subcommand names. The
|
||||||
// make a list of ancestor commands
|
// first string should be a top-level subcommand, the next should be a child
|
||||||
var ancestors []string
|
// subcommand of that subcommand, and so on. If no strings are given then the
|
||||||
cur := p.lastCmd
|
// root command is returned. If no such subcommand exists then an error is
|
||||||
for cur.parent != nil { // we want to exclude the root
|
// returned.
|
||||||
ancestors = append(ancestors, cur.name)
|
func (p *Parser) lookupCommand(path ...string) (*command, error) {
|
||||||
cur = cur.parent
|
cmd := p.cmd
|
||||||
}
|
for _, name := range path {
|
||||||
|
found := findSubcommand(cmd.subcommands, name)
|
||||||
// reverse the list
|
if found == nil {
|
||||||
out := make([]string, len(ancestors))
|
return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
|
||||||
for i := 0; i < len(ancestors); i++ {
|
}
|
||||||
out[i] = ancestors[len(ancestors)-i-1]
|
cmd = found
|
||||||
}
|
}
|
||||||
return out
|
return cmd, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,6 +83,19 @@ 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 {
|
||||||
}
|
}
|
||||||
|
@ -113,6 +126,23 @@ 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
|
||||||
|
@ -275,6 +305,60 @@ 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"`
|
||||||
|
@ -411,3 +495,14 @@ 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)
|
||||||
|
}
|
||||||
|
|
182
usage.go
182
usage.go
|
@ -3,23 +3,15 @@ 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.failWithSubcommand(msg, p.cmd)
|
p.FailSubcommand(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FailSubcommand prints usage information for a specified subcommand to stderr,
|
// FailSubcommand prints usage information for a specified subcommand to stderr,
|
||||||
|
@ -29,28 +21,19 @@ 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 {
|
||||||
cmd, err := p.lookupCommand(subcommand...)
|
err := p.WriteUsageForSubcommand(p.config.Out, subcommand...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.failWithSubcommand(msg, cmd)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// failWithSubcommand prints usage information for the given subcommand to stderr and exits with non-zero status
|
fmt.Fprintln(p.config.Out, "error:", msg)
|
||||||
func (p *Parser) failWithSubcommand(msg string, cmd *command) {
|
p.config.Exit(-1)
|
||||||
p.writeUsageForSubcommand(stderr, cmd)
|
return nil
|
||||||
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) {
|
||||||
cmd := p.cmd
|
p.WriteUsageForSubcommand(w, p.subcommand...)
|
||||||
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
|
||||||
|
@ -63,12 +46,7 @@ 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 {
|
||||||
|
@ -85,18 +63,10 @@ func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *command) {
|
||||||
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.Fprint(w, "Usage:")
|
fmt.Fprintf(w, "Usage: %s", p.cmd.name)
|
||||||
for i := len(ancestors) - 1; i >= 0; i-- {
|
for _, s := range subcommand {
|
||||||
fmt.Fprint(w, " "+ancestors[i])
|
fmt.Fprint(w, " "+s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// write the option component of the usage message
|
// write the option component of the usage message
|
||||||
|
@ -157,47 +127,66 @@ func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *command) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprint(w, "\n")
|
fmt.Fprint(w, "\n")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func printTwoCols(w io.Writer, left, help string, defaultVal string, envVal string) {
|
// print prints a line like this:
|
||||||
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 help != "" {
|
if description != "" {
|
||||||
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, help)
|
fmt.Fprint(w, description)
|
||||||
}
|
}
|
||||||
|
|
||||||
bracketsContent := []string{}
|
var brack string
|
||||||
|
for _, s := range bracketed {
|
||||||
if defaultVal != "" {
|
if s != "" {
|
||||||
bracketsContent = append(bracketsContent,
|
if brack != "" {
|
||||||
fmt.Sprintf("default: %s", defaultVal),
|
brack += ", "
|
||||||
)
|
}
|
||||||
|
brack += s
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if envVal != "" {
|
if brack != "" {
|
||||||
bracketsContent = append(bracketsContent,
|
fmt.Fprintf(w, " [%s]", brack)
|
||||||
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) {
|
||||||
cmd := p.cmd
|
p.WriteHelpForSubcommand(w, p.subcommand...)
|
||||||
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
|
||||||
|
@ -210,13 +199,9 @@ 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeHelp writes the usage string for the given subcommand
|
var positionals, longOptions, shortOptions, envOnlyOptions []*spec
|
||||||
func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
|
var hasVersionOption bool
|
||||||
var positionals, longOptions, shortOptions []*spec
|
|
||||||
for _, spec := range cmd.specs {
|
for _, spec := range cmd.specs {
|
||||||
switch {
|
switch {
|
||||||
case spec.positional:
|
case spec.positional:
|
||||||
|
@ -225,19 +210,21 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
|
||||||
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, cmd)
|
p.WriteUsageForSubcommand(w, subcommand...)
|
||||||
|
|
||||||
// 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 {
|
||||||
printTwoCols(w, spec.placeholder, spec.help, "", "")
|
print(w, spec.placeholder, spec.help)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,6 +236,9 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
|
||||||
}
|
}
|
||||||
for _, spec := range longOptions {
|
for _, spec := range longOptions {
|
||||||
p.printOption(w, spec)
|
p.printOption(w, spec)
|
||||||
|
if spec.long == "version" {
|
||||||
|
hasVersionOption = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,6 +255,9 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
|
||||||
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,7 +268,7 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
|
||||||
short: "h",
|
short: "h",
|
||||||
help: "display this help and exit",
|
help: "display this help and exit",
|
||||||
})
|
})
|
||||||
if p.version != "" {
|
if !hasVersionOption && p.version != "" {
|
||||||
p.printOption(w, &spec{
|
p.printOption(w, &spec{
|
||||||
cardinality: zero,
|
cardinality: zero,
|
||||||
long: "version",
|
long: "version",
|
||||||
|
@ -283,13 +276,27 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
printTwoCols(w, subcmd.name, subcmd.help, "", "")
|
names := append([]string{subcmd.name}, subcmd.aliases...)
|
||||||
|
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) {
|
||||||
|
@ -301,30 +308,23 @@ 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 {
|
||||||
printTwoCols(w, strings.Join(ways, ", "), spec.help, spec.defaultVal, spec.env)
|
print(w, strings.Join(ways, ", "), spec.help, withDefault(spec.defaultString), withEnv(spec.env))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookupCommand finds a subcommand based on a sequence of subcommand names. The
|
func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) {
|
||||||
// first string should be a top-level subcommand, the next should be a child
|
ways := make([]string, 0, 2)
|
||||||
// subcommand of that subcommand, and so on. If no strings are given then the
|
if spec.required {
|
||||||
// root command is returned. If no such subcommand exists then an error is
|
ways = append(ways, "Required.")
|
||||||
// returned.
|
} else {
|
||||||
func (p *Parser) lookupCommand(path ...string) (*command, error) {
|
ways = append(ways, "Optional.")
|
||||||
cmd := p.cmd
|
|
||||||
for _, name := range path {
|
|
||||||
var found *command
|
|
||||||
for _, child := range cmd.subcommands {
|
|
||||||
if child.name == name {
|
|
||||||
found = child
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if spec.help != "" {
|
||||||
|
ways = append(ways, spec.help)
|
||||||
}
|
}
|
||||||
if found == nil {
|
|
||||||
return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
|
print(w, spec.env, strings.Join(ways, " "), withDefault(spec.defaultString))
|
||||||
}
|
|
||||||
cmd = found
|
|
||||||
}
|
|
||||||
return cmd, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func synopsis(spec *spec, form string) string {
|
func synopsis(spec *spec, form string) string {
|
||||||
|
|
173
usage_test.go
173
usage_test.go
|
@ -50,12 +50,16 @@ Options:
|
||||||
--optimize OPTIMIZE, -O OPTIMIZE
|
--optimize OPTIMIZE, -O OPTIMIZE
|
||||||
optimization level
|
optimization level
|
||||||
--ids IDS Ids
|
--ids IDS Ids
|
||||||
--values VALUES Values [default: [3.14 42 256]]
|
--values VALUES Values
|
||||||
--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 {
|
||||||
|
@ -70,11 +74,12 @@ Options:
|
||||||
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)
|
||||||
|
@ -285,6 +290,37 @@ Options:
|
||||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type epilogued struct{}
|
||||||
|
|
||||||
|
// Epilogued returns the epilogue for this program
|
||||||
|
func (epilogued) Epilogue() string {
|
||||||
|
return "For more information visit github.com/alexflint/go-arg"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageWithEpilogue(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example"
|
||||||
|
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--help, -h display this help and exit
|
||||||
|
|
||||||
|
For more information visit github.com/alexflint/go-arg
|
||||||
|
`
|
||||||
|
os.Args[0] = "example"
|
||||||
|
p, err := NewParser(Config{}, &epilogued{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var help bytes.Buffer
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
}
|
||||||
|
|
||||||
func TestUsageForRequiredPositionals(t *testing.T) {
|
func TestUsageForRequiredPositionals(t *testing.T) {
|
||||||
expectedUsage := "Usage: example REQUIRED1 REQUIRED2\n"
|
expectedUsage := "Usage: example REQUIRED1 REQUIRED2\n"
|
||||||
var args struct {
|
var args struct {
|
||||||
|
@ -414,6 +450,8 @@ 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())
|
||||||
|
@ -435,7 +473,7 @@ func TestNonexistentSubcommand(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
sub *struct{} `arg:"subcommand"`
|
sub *struct{} `arg:"subcommand"`
|
||||||
}
|
}
|
||||||
p, err := NewParser(Config{}, &args)
|
p, err := NewParser(Config{Exit: func(int) {}}, &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
|
@ -475,7 +513,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)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var help bytes.Buffer
|
var help bytes.Buffer
|
||||||
p.WriteHelp(&help)
|
p.WriteHelp(&help)
|
||||||
|
@ -522,10 +560,16 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -541,19 +585,39 @@ Options:
|
||||||
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) {
|
||||||
originalStderr := stderr
|
var stdout bytes.Buffer
|
||||||
originalExit := osExit
|
|
||||||
defer func() {
|
|
||||||
stderr = originalStderr
|
|
||||||
osExit = originalExit
|
|
||||||
}()
|
|
||||||
|
|
||||||
var b bytes.Buffer
|
|
||||||
stderr = &b
|
|
||||||
|
|
||||||
var exitCode int
|
var exitCode int
|
||||||
osExit = func(code int) { exitCode = code }
|
exit := func(code int) { exitCode = code }
|
||||||
|
|
||||||
expectedStdout := `
|
expectedStdout := `
|
||||||
Usage: example [--foo FOO]
|
Usage: example [--foo FOO]
|
||||||
|
@ -563,27 +627,18 @@ error: something went wrong
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo int
|
Foo int
|
||||||
}
|
}
|
||||||
p, err := NewParser(Config{Program: "example"}, &args)
|
p, err := NewParser(Config{Program: "example", Exit: exit, Out: &stdout}, &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
p.Fail("something went wrong")
|
p.Fail("something went wrong")
|
||||||
|
|
||||||
assert.Equal(t, expectedStdout[1:], b.String())
|
assert.Equal(t, expectedStdout[1:], stdout.String())
|
||||||
assert.Equal(t, -1, exitCode)
|
assert.Equal(t, -1, exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFailSubcommand(t *testing.T) {
|
func TestFailSubcommand(t *testing.T) {
|
||||||
originalStderr := stderr
|
var stdout bytes.Buffer
|
||||||
originalExit := osExit
|
|
||||||
defer func() {
|
|
||||||
stderr = originalStderr
|
|
||||||
osExit = originalExit
|
|
||||||
}()
|
|
||||||
|
|
||||||
var b bytes.Buffer
|
|
||||||
stderr = &b
|
|
||||||
|
|
||||||
var exitCode int
|
var exitCode int
|
||||||
osExit = func(code int) { exitCode = code }
|
exit := func(code int) { exitCode = code }
|
||||||
|
|
||||||
expectedStdout := `
|
expectedStdout := `
|
||||||
Usage: example sub
|
Usage: example sub
|
||||||
|
@ -593,12 +648,70 @@ error: something went wrong
|
||||||
var args struct {
|
var args struct {
|
||||||
Sub *struct{} `arg:"subcommand"`
|
Sub *struct{} `arg:"subcommand"`
|
||||||
}
|
}
|
||||||
p, err := NewParser(Config{Program: "example"}, &args)
|
p, err := NewParser(Config{Program: "example", Exit: exit, Out: &stdout}, &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:], b.String())
|
assert.Equal(t, expectedStdout[1:], stdout.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