Compare commits
119 Commits
Author | SHA1 | Date |
---|---|---|
|
8235b935a3 | |
|
b72816f193 | |
|
8ecad2c6af | |
|
8b32c8d316 | |
|
6cc03a124c | |
|
e778224db2 | |
|
7155e7e986 | |
|
a1a65e29cf | |
|
f21878956c | |
|
a7ee83cf5d | |
|
438bbfff1e | |
|
efb1be7122 | |
|
51d9bef113 | |
|
cb7e5c1905 | |
|
9b5c76b1c4 | |
|
b218ad854d | |
|
dcb5577c2b | |
|
d10a064207 | |
|
a5045bbe85 | |
|
3925edf11a | |
|
12fffac1d8 | |
|
b13a62172a | |
|
7cf32414af | |
|
bdb7560b8d | |
|
50166cae2c | |
|
7fd624cf1c | |
|
bf156d17a3 | |
|
3673177bf9 | |
|
3de7278c4f | |
|
b8282df4c4 | |
|
ec0ced7467 | |
|
0cc152dce5 | |
|
67353a8bcf | |
|
af368523db | |
|
b6422dcbc3 | |
|
56ee7c97ac | |
|
177b84441e | |
|
c087d71802 | |
|
c992aa8627 | |
|
bed89eb683 | |
|
4ed4ce751f | |
|
a7c40c36a3 | |
|
bee5cf5d7c | |
|
aa844c7de9 | |
|
dfca71d159 | |
|
188bd31bf6 | |
|
8a917260c3 | |
|
3ddfffdcd3 | |
|
68948b2ac1 | |
|
be792f1f8b | |
|
8e35a4f0d4 | |
|
84ddf1d244 | |
|
582e6d537a | |
|
bf629a16cb | |
|
f02da4cd10 | |
|
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 | |
|
f0f44b65d1 | |
|
5fb236a65d | |
|
d3706100bf | |
|
25d4d1c864 | |
|
3bf2a5e78a | |
|
a87d80089a | |
|
bf32f08247 | |
|
b47d6e3da6 | |
|
a4afd6a849 | |
|
f2f876420c | |
|
66cb696e79 | |
|
0f0b4b5c3f | |
|
b157e8d10a | |
|
ff38a63b36 | |
|
3d59e5e89e | |
|
eb0393e9bc | |
|
fa12c02e81 | |
|
7cc8da61cf | |
|
c9b504edc1 | |
|
679be43af3 | |
|
2e81334206 |
|
@ -0,0 +1 @@
|
||||||
|
github: [alexflint]
|
|
@ -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.20', '1.21', '1.22']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: go
|
- id: go
|
||||||
name: Set up Go
|
name: Set up Go
|
||||||
uses: actions/setup-go@v1
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build -v .
|
run: go build -v .
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
all: goimports vet
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f go.*
|
||||||
|
|
||||||
|
redomod:
|
||||||
|
rm -f go.*
|
||||||
|
GO111MODULE= go mod init
|
||||||
|
GO111MODULE= go mod tidy
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test
|
||||||
|
|
||||||
|
vet:
|
||||||
|
@GO111MODULE=off go vet
|
||||||
|
@echo this go binary package builds okay
|
||||||
|
|
||||||
|
goimports:
|
||||||
|
goimports -w *.go
|
349
README.md
349
README.md
|
@ -64,7 +64,7 @@ fmt.Println("Input:", args.Input)
|
||||||
fmt.Println("Output:", args.Output)
|
fmt.Println("Output:", args.Output)
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```shell
|
||||||
$ ./example src.txt x.out y.out z.out
|
$ ./example src.txt x.out y.out z.out
|
||||||
Input: src.txt
|
Input: src.txt
|
||||||
Output: [x.out y.out z.out]
|
Output: [x.out y.out z.out]
|
||||||
|
@ -80,12 +80,12 @@ arg.MustParse(&args)
|
||||||
fmt.Println("Workers:", args.Workers)
|
fmt.Println("Workers:", args.Workers)
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```shell
|
||||||
$ WORKERS=4 ./example
|
$ WORKERS=4 ./example
|
||||||
Workers: 4
|
Workers: 4
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```shell
|
||||||
$ WORKERS=4 ./example --workers=6
|
$ WORKERS=4 ./example --workers=6
|
||||||
Workers: 6
|
Workers: 6
|
||||||
```
|
```
|
||||||
|
@ -100,12 +100,12 @@ arg.MustParse(&args)
|
||||||
fmt.Println("Workers:", args.Workers)
|
fmt.Println("Workers:", args.Workers)
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```shell
|
||||||
$ NUM_WORKERS=4 ./example
|
$ NUM_WORKERS=4 ./example
|
||||||
Workers: 4
|
Workers: 4
|
||||||
```
|
```
|
||||||
|
|
||||||
You can provide multiple values using the CSV (RFC 4180) format:
|
You can provide multiple values in environment variables using commas:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var args struct {
|
var args struct {
|
||||||
|
@ -115,12 +115,50 @@ arg.MustParse(&args)
|
||||||
fmt.Println("Workers:", args.Workers)
|
fmt.Println("Workers:", args.Workers)
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```shell
|
||||||
$ WORKERS='1,99' ./example
|
$ WORKERS='1,99' ./example
|
||||||
Workers: [1 99]
|
Workers: [1 99]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Command line arguments take precedence over environment variables:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Workers int `arg:"--count,env:NUM_WORKERS"`
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
fmt.Println("Workers:", args.Workers)
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ NUM_WORKERS=6 ./example
|
||||||
|
Workers: 6
|
||||||
|
$ NUM_WORKERS=6 ./example --count 4
|
||||||
|
Workers: 4
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuring a global environment variable name prefix is also possible:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Workers int `arg:"--count,env:NUM_WORKERS"`
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := arg.NewParser(arg.Config{
|
||||||
|
EnvPrefix: "MYAPP_",
|
||||||
|
}, &args)
|
||||||
|
|
||||||
|
p.MustParse(os.Args[1:])
|
||||||
|
fmt.Println("Workers:", args.Workers)
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ MYAPP_NUM_WORKERS=6 ./example
|
||||||
|
Workers: 6
|
||||||
|
```
|
||||||
|
|
||||||
### Usage strings
|
### Usage strings
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var args struct {
|
var args struct {
|
||||||
Input string `arg:"positional"`
|
Input string `arg:"positional"`
|
||||||
|
@ -158,18 +196,32 @@ var args struct {
|
||||||
arg.MustParse(&args)
|
arg.MustParse(&args)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Default values (before v1.2)
|
Command line arguments take precedence over environment variables, which take precedence over default values. This means that we check whether a certain option was provided on the command line, then if not, we check for an environment variable (only if an `env` tag was provided), then if none is found, we check for a `default` tag containing a default value.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo string
|
Test string `arg:"-t,env:TEST" default:"something"`
|
||||||
Bar bool
|
|
||||||
}
|
}
|
||||||
arg.Foo = "abc"
|
|
||||||
arg.MustParse(&args)
|
arg.MustParse(&args)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 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[1:])
|
||||||
|
```
|
||||||
|
|
||||||
### Arguments with multiple values
|
### Arguments with multiple values
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var args struct {
|
var args struct {
|
||||||
Database string
|
Database string
|
||||||
|
@ -185,6 +237,7 @@ Fetching the following IDs from foo: [1 2 3]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arguments that can be specified multiple times, mixed with positionals
|
### Arguments that can be specified multiple times, mixed with positionals
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var args struct {
|
var args struct {
|
||||||
Commands []string `arg:"-c,separate"`
|
Commands []string `arg:"-c,separate"`
|
||||||
|
@ -202,6 +255,7 @@ Databases [db1 db2 db3]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arguments with keys and values
|
### Arguments with keys and values
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var args struct {
|
var args struct {
|
||||||
UserIDs map[string]int
|
UserIDs map[string]int
|
||||||
|
@ -215,24 +269,6 @@ fmt.Println(args.UserIDs)
|
||||||
map[john:123 mary:456]
|
map[john:123 mary:456]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom validation
|
|
||||||
```go
|
|
||||||
var args struct {
|
|
||||||
Foo string
|
|
||||||
Bar string
|
|
||||||
}
|
|
||||||
p := arg.MustParse(&args)
|
|
||||||
if args.Foo == "" && args.Bar == "" {
|
|
||||||
p.Fail("you must provide either --foo or --bar")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```shell
|
|
||||||
./example
|
|
||||||
Usage: samples [--foo FOO] [--bar BAR]
|
|
||||||
error: you must provide either --foo or --bar
|
|
||||||
```
|
|
||||||
|
|
||||||
### Version strings
|
### Version strings
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
@ -255,6 +291,28 @@ $ ./example --version
|
||||||
someprogram 4.3.0
|
someprogram 4.3.0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
> If a `--version` flag is defined in `args` or any subcommand, it overrides the built-in versioning.
|
||||||
|
|
||||||
|
### Custom validation
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Foo string
|
||||||
|
Bar string
|
||||||
|
}
|
||||||
|
p := arg.MustParse(&args)
|
||||||
|
if args.Foo == "" && args.Bar == "" {
|
||||||
|
p.Fail("you must provide either --foo or --bar")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./example
|
||||||
|
Usage: samples [--foo FOO] [--bar BAR]
|
||||||
|
error: you must provide either --foo or --bar
|
||||||
|
```
|
||||||
|
|
||||||
### Overriding option names
|
### Overriding option names
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
@ -279,13 +337,11 @@ Options:
|
||||||
--help, -h display this help and exit
|
--help, -h display this help and exit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Embedded structs
|
### Embedded structs
|
||||||
|
|
||||||
The fields of embedded structs are treated just like regular fields:
|
The fields of embedded structs are treated just like regular fields:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|
||||||
type DatabaseOptions struct {
|
type DatabaseOptions struct {
|
||||||
Host string
|
Host string
|
||||||
Username string
|
Username string
|
||||||
|
@ -308,6 +364,22 @@ func main() {
|
||||||
|
|
||||||
As usual, any field tagged with `arg:"-"` is ignored.
|
As usual, any field tagged with `arg:"-"` is ignored.
|
||||||
|
|
||||||
|
### Supported types
|
||||||
|
|
||||||
|
The following types may be used as arguments:
|
||||||
|
- built-in integer types: `int, int8, int16, int32, int64, byte, rune`
|
||||||
|
- built-in floating point types: `float32, float64`
|
||||||
|
- strings
|
||||||
|
- booleans
|
||||||
|
- URLs represented as `url.URL`
|
||||||
|
- time durations represented as `time.Duration`
|
||||||
|
- email addresses represented as `mail.Address`
|
||||||
|
- MAC addresses represented as `net.HardwareAddr`
|
||||||
|
- pointers to any of the above
|
||||||
|
- slices of any of the above
|
||||||
|
- maps using any of the above as keys and values
|
||||||
|
- any type that implements `encoding.TextUnmarshaler`
|
||||||
|
|
||||||
### Custom parsing
|
### Custom parsing
|
||||||
|
|
||||||
Implement `encoding.TextUnmarshaler` to define your own parsing logic.
|
Implement `encoding.TextUnmarshaler` to define your own parsing logic.
|
||||||
|
@ -337,6 +409,7 @@ func main() {
|
||||||
fmt.Printf("%#v\n", args.Name)
|
fmt.Printf("%#v\n", args.Name)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ ./example --name=foo.bar
|
$ ./example --name=foo.bar
|
||||||
main.NameDotName{Head:"foo", Tail:"bar"}
|
main.NameDotName{Head:"foo", Tail:"bar"}
|
||||||
|
@ -373,6 +446,7 @@ func main() {
|
||||||
fmt.Printf("%#v\n", args.Name)
|
fmt.Printf("%#v\n", args.Name)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ ./example --help
|
$ ./example --help
|
||||||
Usage: test [--name NAME]
|
Usage: test [--name NAME]
|
||||||
|
@ -387,8 +461,6 @@ main.NameDotName{Head:"file", Tail:"txt"}
|
||||||
|
|
||||||
### Custom placeholders
|
### Custom placeholders
|
||||||
|
|
||||||
*Introduced in version 1.3.0*
|
|
||||||
|
|
||||||
Use the `placeholder` tag to control which placeholder text is used in the usage text.
|
Use the `placeholder` tag to control which placeholder text is used in the usage text.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
@ -400,6 +472,7 @@ var args struct {
|
||||||
}
|
}
|
||||||
arg.MustParse(&args)
|
arg.MustParse(&args)
|
||||||
```
|
```
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ ./example -h
|
$ ./example -h
|
||||||
Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
|
Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
|
||||||
|
@ -417,6 +490,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
|
||||||
|
@ -442,9 +518,36 @@ Options:
|
||||||
--help, -h display this help and exit
|
--help, -h display this help and exit
|
||||||
```
|
```
|
||||||
|
|
||||||
### Subcommands
|
Similarly an epilogue can be added at the end of the help text by implementing
|
||||||
|
the `Epilogue` function.
|
||||||
|
|
||||||
*Introduced in version 1.1.0*
|
```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 are commonly used in tools that wish to group multiple functions into a single program. An example is the `git` tool:
|
Subcommands are commonly used in tools that wish to group multiple functions into a single program. An example is the `git` tool:
|
||||||
```shell
|
```shell
|
||||||
|
@ -506,15 +609,187 @@ if p.Subcommand() == nil {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Custom handling of --help and --version
|
||||||
|
|
||||||
|
The following reproduces the internal logic of `MustParse` for the simple case where
|
||||||
|
you are not using subcommands or --version. This allows you to respond
|
||||||
|
programatically to --help, and to any errors that come up.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Something string
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := arg.NewParser(arg.Config{}, &args)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.Parse(os.Args[1:])
|
||||||
|
switch {
|
||||||
|
case err == arg.ErrHelp: // indicates that user wrote "--help" on command line
|
||||||
|
p.WriteHelp(os.Stdout)
|
||||||
|
os.Exit(0)
|
||||||
|
case err != nil:
|
||||||
|
fmt.Printf("error: %v\n", err)
|
||||||
|
p.WriteUsage(os.Stdout)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ go run ./example --help
|
||||||
|
Usage: ./example --something SOMETHING
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--something SOMETHING
|
||||||
|
--help, -h display this help and exit
|
||||||
|
|
||||||
|
$ ./example --wrong
|
||||||
|
error: unknown argument --wrong
|
||||||
|
Usage: ./example --something SOMETHING
|
||||||
|
|
||||||
|
$ ./example
|
||||||
|
error: --something is required
|
||||||
|
Usage: ./example --something SOMETHING
|
||||||
|
```
|
||||||
|
|
||||||
|
To also handle --version programatically, use the following:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type args struct {
|
||||||
|
Something string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (args) Version() string {
|
||||||
|
return "1.2.3"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var args args
|
||||||
|
p, err := arg.NewParser(arg.Config{}, &args)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.Parse(os.Args[1:])
|
||||||
|
switch {
|
||||||
|
case err == arg.ErrHelp: // found "--help" on command line
|
||||||
|
p.WriteHelp(os.Stdout)
|
||||||
|
os.Exit(0)
|
||||||
|
case err == arg.ErrVersion: // found "--version" on command line
|
||||||
|
fmt.Println(args.Version())
|
||||||
|
os.Exit(0)
|
||||||
|
case err != nil:
|
||||||
|
fmt.Printf("error: %v\n", err)
|
||||||
|
p.WriteUsage(os.Stdout)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("got %q\n", args.Something)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./example --version
|
||||||
|
1.2.3
|
||||||
|
|
||||||
|
$ go run ./example --help
|
||||||
|
1.2.3
|
||||||
|
Usage: example --something SOMETHING
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--something SOMETHING
|
||||||
|
--help, -h display this help and exit
|
||||||
|
|
||||||
|
$ ./example --wrong
|
||||||
|
1.2.3
|
||||||
|
error: unknown argument --wrong
|
||||||
|
Usage: example --something SOMETHING
|
||||||
|
|
||||||
|
$ ./example
|
||||||
|
error: --something is required
|
||||||
|
Usage: example --something SOMETHING
|
||||||
|
```
|
||||||
|
|
||||||
|
To generate subcommand-specific help messages, use the following most general version
|
||||||
|
(this also works in absence of subcommands but is a bit more complex):
|
||||||
|
|
||||||
|
```go
|
||||||
|
type fetchCmd struct {
|
||||||
|
Count int
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
Something string
|
||||||
|
Fetch *fetchCmd `arg:"subcommand"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (args) Version() string {
|
||||||
|
return "1.2.3"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var args args
|
||||||
|
p, err := arg.NewParser(arg.Config{}, &args)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.Parse(os.Args[1:])
|
||||||
|
switch {
|
||||||
|
case err == arg.ErrHelp: // found "--help" on command line
|
||||||
|
p.WriteHelpForSubcommand(os.Stdout, p.SubcommandNames()...)
|
||||||
|
os.Exit(0)
|
||||||
|
case err == arg.ErrVersion: // found "--version" on command line
|
||||||
|
fmt.Println(args.Version())
|
||||||
|
os.Exit(0)
|
||||||
|
case err != nil:
|
||||||
|
fmt.Printf("error: %v\n", err)
|
||||||
|
p.WriteUsageForSubcommand(os.Stdout, p.SubcommandNames()...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./example --version
|
||||||
|
1.2.3
|
||||||
|
|
||||||
|
$ ./example --help
|
||||||
|
1.2.3
|
||||||
|
Usage: example [--something SOMETHING] <command> [<args>]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--something SOMETHING
|
||||||
|
--help, -h display this help and exit
|
||||||
|
--version display version and exit
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
fetch
|
||||||
|
|
||||||
|
$ ./example fetch --help
|
||||||
|
1.2.3
|
||||||
|
Usage: example fetch [--count COUNT]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--count COUNT
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
--something SOMETHING
|
||||||
|
--help, -h display this help and exit
|
||||||
|
--version display version and exit
|
||||||
|
```
|
||||||
|
|
||||||
### API Documentation
|
### API Documentation
|
||||||
|
|
||||||
https://godoc.org/github.com/alexflint/go-arg
|
https://pkg.go.dev/github.com/alexflint/go-arg
|
||||||
|
|
||||||
### Rationale
|
### Rationale
|
||||||
|
|
||||||
There are many command line argument parsing libraries for Go, including one in the standard library, so why build another?
|
There are many command line argument parsing libraries for Go, including one in the standard library, so why build another?
|
||||||
|
|
||||||
The `flag` library that ships in the standard library seems awkward to me. Positional arguments must preceed options, so `./prog x --foo=1` does what you expect but `./prog --foo=1 x` does not. It also does not allow arguments to have both long (`--foo`) and short (`-f`) forms.
|
The `flag` library that ships in the standard library seems awkward to me. Positional arguments must precede options, so `./prog x --foo=1` does what you expect but `./prog --foo=1 x` does not. It also does not allow arguments to have both long (`--foo`) and short (`-f`) forms.
|
||||||
|
|
||||||
Many third-party argument parsing libraries are great for writing sophisticated command line interfaces, but feel to me like overkill for a simple script with a few flags.
|
Many third-party argument parsing libraries are great for writing sophisticated command line interfaces, but feel to me like overkill for a simple script with a few flags.
|
||||||
|
|
||||||
|
@ -522,4 +797,4 @@ The idea behind `go-arg` is that Go already has an excellent way to describe dat
|
||||||
|
|
||||||
### Backward compatibility notes
|
### Backward compatibility notes
|
||||||
|
|
||||||
Earlier versions of this library required the help text to be part of the `arg` tag. This is still supported but is now deprecated. Instead, you should use a separate `help` tag, described above, which removes most of the limits on the text you can write. In particular, you will need to use the new `help` tag if your help text includes any commas.
|
Earlier versions of this library required the help text to be part of the `arg` tag. This is still supported but is now deprecated. Instead, you should use a separate `help` tag, described above, which makes it possible to include commas inside help text.
|
||||||
|
|
217
example_test.go
217
example_test.go
|
@ -2,8 +2,12 @@ package arg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/mail"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func split(s string) []string {
|
func split(s string) []string {
|
||||||
|
@ -150,7 +154,7 @@ func Example_helpText() {
|
||||||
os.Args = split("./example --help")
|
os.Args = split("./example --help")
|
||||||
|
|
||||||
var args struct {
|
var args struct {
|
||||||
Input string `arg:"positional"`
|
Input string `arg:"positional,required"`
|
||||||
Output []string `arg:"positional"`
|
Output []string `arg:"positional"`
|
||||||
Verbose bool `arg:"-v" help:"verbosity level"`
|
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||||
Dataset string `help:"dataset to use"`
|
Dataset string `help:"dataset to use"`
|
||||||
|
@ -158,8 +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
|
||||||
osExit = func(int) {}
|
mustParseExit = func(int) {}
|
||||||
stdout = os.Stdout
|
mustParseOut = os.Stdout
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -184,26 +188,25 @@ func Example_helpPlaceholder() {
|
||||||
os.Args = split("./example --help")
|
os.Args = split("./example --help")
|
||||||
|
|
||||||
var args struct {
|
var args struct {
|
||||||
Input string `arg:"positional" placeholder:"SRC"`
|
Input string `arg:"positional,required" placeholder:"SRC"`
|
||||||
Output []string `arg:"positional" placeholder:"DST"`
|
Output []string `arg:"positional" placeholder:"DST"`
|
||||||
Optimize int `arg:"-O" help:"optimization level" placeholder:"LEVEL"`
|
Optimize int `arg:"-O" help:"optimization level" placeholder:"LEVEL"`
|
||||||
MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" placeholder:"N"`
|
MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" placeholder:"N"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
mustParseOut = os.Stdout
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
// output:
|
// output:
|
||||||
|
|
||||||
// Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
|
// Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
|
||||||
|
//
|
||||||
// Positional arguments:
|
// Positional arguments:
|
||||||
// SRC
|
// SRC
|
||||||
// DST
|
// DST
|
||||||
|
//
|
||||||
// Options:
|
// Options:
|
||||||
// --optimize LEVEL, -O LEVEL
|
// --optimize LEVEL, -O LEVEL
|
||||||
// optimization level
|
// optimization level
|
||||||
|
@ -232,8 +235,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
|
||||||
osExit = func(int) {}
|
mustParseExit = func(int) {}
|
||||||
stdout = os.Stdout
|
mustParseOut = os.Stdout
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -250,7 +253,44 @@ func Example_helpTextWithSubcommand() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// This example shows the usage string generated by go-arg when using subcommands
|
// This example shows the usage string generated by go-arg when using subcommands
|
||||||
func Example_helpTextForSubcommand() {
|
func Example_helpTextWhenUsingSubcommand() {
|
||||||
|
// These are the args you would pass in on the command line
|
||||||
|
os.Args = split("./example get --help")
|
||||||
|
|
||||||
|
type getCmd struct {
|
||||||
|
Item string `arg:"positional,required" help:"item to fetch"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type listCmd struct {
|
||||||
|
Format string `help:"output format"`
|
||||||
|
Limit int
|
||||||
|
}
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Verbose bool
|
||||||
|
Get *getCmd `arg:"subcommand" help:"fetch an item and print it"`
|
||||||
|
List *listCmd `arg:"subcommand" help:"list available items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is only necessary when running inside golang's runnable example harness
|
||||||
|
mustParseExit = func(int) {}
|
||||||
|
mustParseOut = os.Stdout
|
||||||
|
|
||||||
|
MustParse(&args)
|
||||||
|
|
||||||
|
// output:
|
||||||
|
// Usage: example get ITEM
|
||||||
|
//
|
||||||
|
// Positional arguments:
|
||||||
|
// ITEM item to fetch
|
||||||
|
//
|
||||||
|
// Global options:
|
||||||
|
// --verbose
|
||||||
|
// --help, -h display this help and exit
|
||||||
|
}
|
||||||
|
|
||||||
|
// This example shows how to print help for an explicit subcommand
|
||||||
|
func Example_writeHelpForSubcommand() {
|
||||||
// These are the args you would pass in on the command line
|
// These are the args you would pass in on the command line
|
||||||
os.Args = split("./example get --help")
|
os.Args = split("./example get --help")
|
||||||
|
|
||||||
|
@ -270,29 +310,83 @@ func Example_helpTextForSubcommand() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
MustParse(&args)
|
p, err := NewParser(Config{Exit: exit}, &args)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.WriteHelpForSubcommand(os.Stdout, "list")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
// output:
|
// output:
|
||||||
// Usage: example get ITEM
|
// Usage: example list [--format FORMAT] [--limit LIMIT]
|
||||||
//
|
//
|
||||||
// Positional arguments:
|
// Options:
|
||||||
// ITEM item to fetch
|
// --format FORMAT output format
|
||||||
|
// --limit LIMIT
|
||||||
//
|
//
|
||||||
// Global options:
|
// Global options:
|
||||||
// --verbose
|
// --verbose
|
||||||
// --help, -h display this help and exit
|
// --help, -h display this help and exit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This example shows how to print help for a subcommand that is nested several levels deep
|
||||||
|
func Example_writeHelpForSubcommandNested() {
|
||||||
|
// These are the args you would pass in on the command line
|
||||||
|
os.Args = split("./example get --help")
|
||||||
|
|
||||||
|
type mostNestedCmd struct {
|
||||||
|
Item string
|
||||||
|
}
|
||||||
|
|
||||||
|
type nestedCmd struct {
|
||||||
|
MostNested *mostNestedCmd `arg:"subcommand"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type topLevelCmd struct {
|
||||||
|
Nested *nestedCmd `arg:"subcommand"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
TopLevel *topLevelCmd `arg:"subcommand"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is only necessary when running inside golang's runnable example harness
|
||||||
|
exit := func(int) {}
|
||||||
|
|
||||||
|
p, err := NewParser(Config{Exit: exit}, &args)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.WriteHelpForSubcommand(os.Stdout, "toplevel", "nested", "mostnested")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// output:
|
||||||
|
// Usage: example toplevel nested mostnested [--item ITEM]
|
||||||
|
//
|
||||||
|
// Options:
|
||||||
|
// --item ITEM
|
||||||
|
// --help, -h display this help and exit
|
||||||
|
}
|
||||||
|
|
||||||
// This example shows the error string generated by go-arg when an invalid option is provided
|
// This example shows the error string generated by go-arg when an invalid option is provided
|
||||||
func Example_errorText() {
|
func Example_errorText() {
|
||||||
// These are the args you would pass in on the command line
|
// These are the args you would pass in on the command line
|
||||||
os.Args = split("./example --optimize INVALID")
|
os.Args = split("./example --optimize INVALID")
|
||||||
|
|
||||||
var args struct {
|
var args struct {
|
||||||
Input string `arg:"positional"`
|
Input string `arg:"positional,required"`
|
||||||
Output []string `arg:"positional"`
|
Output []string `arg:"positional"`
|
||||||
Verbose bool `arg:"-v" help:"verbosity level"`
|
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||||
Dataset string `help:"dataset to use"`
|
Dataset string `help:"dataset to use"`
|
||||||
|
@ -300,8 +394,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
|
||||||
osExit = func(int) {}
|
mustParseExit = func(int) {}
|
||||||
stderr = os.Stdout
|
mustParseOut = os.Stdout
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -324,8 +418,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
|
||||||
osExit = func(int) {}
|
mustParseExit = func(int) {}
|
||||||
stderr = os.Stdout
|
mustParseOut = os.Stdout
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -360,8 +454,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
|
||||||
osExit = func(int) {}
|
mustParseExit = func(int) {}
|
||||||
stderr = os.Stdout
|
mustParseOut = os.Stdout
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -377,3 +471,76 @@ func Example_subcommand() {
|
||||||
// output:
|
// output:
|
||||||
// commit requested with message "what-this-commit-is-about"
|
// commit requested with message "what-this-commit-is-about"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Example_allSupportedTypes() {
|
||||||
|
// These are the args you would pass in on the command line
|
||||||
|
os.Args = []string{}
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Bool bool
|
||||||
|
Byte byte
|
||||||
|
Rune rune
|
||||||
|
Int int
|
||||||
|
Int8 int8
|
||||||
|
Int16 int16
|
||||||
|
Int32 int32
|
||||||
|
Int64 int64
|
||||||
|
Float32 float32
|
||||||
|
Float64 float64
|
||||||
|
String string
|
||||||
|
Duration time.Duration
|
||||||
|
URL url.URL
|
||||||
|
Email mail.Address
|
||||||
|
MAC net.HardwareAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
// go-arg supports each of the types above, as well as pointers to any of
|
||||||
|
// the above and slices of any of the above. It also supports any types that
|
||||||
|
// implements encoding.TextUnmarshaler.
|
||||||
|
|
||||||
|
MustParse(&args)
|
||||||
|
|
||||||
|
// 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/arg
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexflint/go-scalar v1.0.0
|
github.com/alexflint/go-scalar v1.2.0
|
||||||
github.com/stretchr/testify v1.2.2
|
github.com/stretchr/testify v1.7.0
|
||||||
)
|
)
|
||||||
|
|
||||||
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.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
go 1.18
|
||||||
|
|
14
go.sum
14
go.sum
|
@ -1,8 +1,16 @@
|
||||||
github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi+A70=
|
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
||||||
github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw=
|
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/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
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.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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
431
parse.go
431
parse.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
@ -43,23 +44,25 @@ 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
|
||||||
defaultVal string // default value for this option
|
defaultValue reflect.Value // default value for this option
|
||||||
placeholder string // name of the data in help
|
defaultString string // default value for this option, in string form to be displayed in help text
|
||||||
|
placeholder string // placeholder string 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,48 @@ 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 and test code
|
||||||
|
var mustParseExit = os.Exit
|
||||||
|
var mustParseOut io.Writer = os.Stdout
|
||||||
|
|
||||||
|
/*
|
||||||
|
This allows you to have common arg values defined in a GO package
|
||||||
|
|
||||||
|
package 'foo'
|
||||||
|
function init() {
|
||||||
|
args.Register(&argsFoo)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This stores the args sent from the GO packages
|
||||||
|
var register []interface{}
|
||||||
|
|
||||||
|
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, Out: mustParseOut}, register...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mustParse is a helper that facilitates testing
|
||||||
|
func mustParse(config Config, dest ...interface{}) *Parser {
|
||||||
|
p, err := NewParser(config, dest...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(stdout, err)
|
fmt.Fprintln(config.Out, err)
|
||||||
osExit(-1)
|
config.Exit(2)
|
||||||
return nil // just in case osExit was monkey-patched
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
err = p.Parse(flags())
|
|
||||||
switch {
|
|
||||||
case err == ErrHelp:
|
|
||||||
p.writeHelpForCommand(stdout, p.lastCmd)
|
|
||||||
osExit(0)
|
|
||||||
case err == ErrVersion:
|
|
||||||
fmt.Fprintln(stdout, p.version)
|
|
||||||
osExit(0)
|
|
||||||
case err != nil:
|
|
||||||
p.failWithCommand(err.Error(), p.lastCmd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.MustParse(flags())
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,8 +124,24 @@ func Parse(dest ...interface{}) error {
|
||||||
return p.Parse(flags())
|
return p.Parse(flags())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pass in a "pretend" os.Args. Used for bash autocomplete
|
||||||
|
func ParseFlags(flags []string, dest ...interface{}) (*Parser, error) {
|
||||||
|
p, err := NewParser(Config{}, dest...)
|
||||||
|
if err != nil {
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
overrideFlags = append(overrideFlags, flags...)
|
||||||
|
err = p.Parse(flags)
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var overrideFlags []string
|
||||||
|
|
||||||
// flags gets all command line arguments other than the first (program name)
|
// flags gets all command line arguments other than the first (program name)
|
||||||
func flags() []string {
|
func flags() []string {
|
||||||
|
if len(overrideFlags) > 0 {
|
||||||
|
return overrideFlags
|
||||||
|
}
|
||||||
if len(os.Args) == 0 { // os.Args could be empty
|
if len(os.Args) == 0 { // os.Args could be empty
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -121,6 +155,23 @@ 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
|
||||||
|
|
||||||
|
// EnvPrefix instructs the library to use a name prefix when reading environment variables.
|
||||||
|
EnvPrefix string
|
||||||
|
|
||||||
|
// 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 +181,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 +203,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 +234,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 {
|
||||||
|
@ -203,23 +271,36 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
||||||
panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", t))
|
panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", t))
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd, err := cmdFromStruct(name, path{root: i}, t)
|
cmd, err := cmdFromStruct(name, path{root: i}, t, config.EnvPrefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
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 err != nil {
|
// if the value is the "zero value" (e.g. nil pointer, empty struct) then ignore
|
||||||
return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
|
if isZero(v) {
|
||||||
}
|
continue
|
||||||
spec.defaultVal = string(str)
|
}
|
||||||
} else {
|
|
||||||
spec.defaultVal = fmt.Sprintf("%v", v)
|
// 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 {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,12 +313,22 @@ 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the parent of the subcommands to be the top-level command
|
||||||
|
// to make sure that global options work when there is more than one
|
||||||
|
// dest supplied.
|
||||||
|
for _, subcommand := range p.cmd.subcommands {
|
||||||
|
subcommand.parent = p.cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
return &p, nil
|
return &p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*command, error) {
|
||||||
// commands can only be created from pointers to structs
|
// commands can only be created from pointers to structs
|
||||||
if t.Kind() != reflect.Ptr {
|
if t.Kind() != reflect.Ptr {
|
||||||
return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a %s",
|
return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a %s",
|
||||||
|
@ -257,17 +348,24 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
||||||
|
|
||||||
var errs []string
|
var errs []string
|
||||||
walkFields(t, func(field reflect.StructField, t reflect.Type) bool {
|
walkFields(t, func(field reflect.StructField, t reflect.Type) bool {
|
||||||
// Check for the ignore switch in the tag
|
// check for the ignore switch in the tag
|
||||||
tag := field.Tag.Get("arg")
|
tag := field.Tag.Get("arg")
|
||||||
if tag == "-" || !isExported(field.Name) {
|
if tag == "-" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is an embedded struct then recurse into its fields
|
// if this is an embedded struct then recurse into its fields, even if
|
||||||
|
// it is unexported, because exported fields on unexported embedded
|
||||||
|
// structs are still writable
|
||||||
if field.Anonymous && field.Type.Kind() == reflect.Struct {
|
if field.Anonymous && field.Type.Kind() == reflect.Struct {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore any other unexported field
|
||||||
|
if !isExported(field.Name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// duplicate the entire path to avoid slice overwrites
|
// duplicate the entire path to avoid slice overwrites
|
||||||
subdest := dest.Child(field)
|
subdest := dest.Child(field)
|
||||||
spec := spec{
|
spec := spec{
|
||||||
|
@ -281,13 +379,8 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
||||||
spec.help = help
|
spec.help = help
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultVal, hasDefault := field.Tag.Lookup("default")
|
// process each comma-separated part of the tag
|
||||||
if hasDefault {
|
var isSubcommand bool
|
||||||
spec.defaultVal = defaultVal
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look at the tag
|
|
||||||
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
|
||||||
|
@ -305,18 +398,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
|
||||||
|
@ -327,24 +415,30 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
||||||
case key == "env":
|
case key == "env":
|
||||||
// Use override name if provided
|
// Use override name if provided
|
||||||
if value != "" {
|
if value != "" {
|
||||||
spec.env = value
|
spec.env = envPrefix + value
|
||||||
} else {
|
} else {
|
||||||
spec.env = strings.ToUpper(field.Name)
|
spec.env = envPrefix + strings.ToUpper(field.Name)
|
||||||
}
|
}
|
||||||
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, envPrefix)
|
||||||
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")
|
||||||
|
|
||||||
|
@ -356,6 +450,7 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// placeholder is the string used in the help text like this: "--somearg PLACEHOLDER"
|
||||||
placeholder, hasPlaceholder := field.Tag.Lookup("placeholder")
|
placeholder, hasPlaceholder := field.Tag.Lookup("placeholder")
|
||||||
if hasPlaceholder {
|
if hasPlaceholder {
|
||||||
spec.placeholder = placeholder
|
spec.placeholder = placeholder
|
||||||
|
@ -365,27 +460,60 @@ 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 {
|
var err error
|
||||||
cmd.specs = append(cmd.specs, &spec)
|
spec.cardinality, err = cardinalityOf(field.Type)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported",
|
||||||
|
t.Name(), field.Name, field.Type.String()))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
defaultString, hasDefault := field.Tag.Lookup("default")
|
||||||
spec.cardinality, err = cardinalityOf(field.Type)
|
if hasDefault {
|
||||||
if err != nil {
|
// we do not support default values for maps and slices
|
||||||
errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported",
|
if spec.cardinality == multiple {
|
||||||
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
|
||||||
})
|
})
|
||||||
|
@ -408,8 +536,15 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
||||||
return &cmd, nil
|
return &cmd, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse processes the given command line option, storing the results in the field
|
// Parse processes the given command line option, storing the results in the fields
|
||||||
// of the structs from which NewParser was constructed
|
// of the structs from which NewParser was constructed.
|
||||||
|
//
|
||||||
|
// It returns ErrHelp if "--help" is one of the command line args and ErrVersion if
|
||||||
|
// "--version" is one of the command line args (the latter only applies if the
|
||||||
|
// destination struct passed to NewParser implements Versioned.)
|
||||||
|
//
|
||||||
|
// To respond to --help and --version in the way that MustParse does, see examples
|
||||||
|
// in the README under "Custom handling of --help and --version".
|
||||||
func (p *Parser) Parse(args []string) error {
|
func (p *Parser) Parse(args []string) error {
|
||||||
err := p.process(args)
|
err := p.process(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -426,6 +561,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 {
|
||||||
|
@ -441,13 +590,17 @@ func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error
|
||||||
if spec.cardinality == multiple {
|
if spec.cardinality == multiple {
|
||||||
// expect a CSV string in an environment
|
// expect a CSV string in an environment
|
||||||
// variable in the case of multiple values
|
// variable in the case of multiple values
|
||||||
values, err := csv.NewReader(strings.NewReader(value)).Read()
|
var values []string
|
||||||
if err != nil {
|
var err error
|
||||||
return fmt.Errorf(
|
if len(strings.TrimSpace(value)) > 0 {
|
||||||
"error reading a CSV string from environment variable %s with multiple values: %v",
|
values, err = csv.NewReader(strings.NewReader(value)).Read()
|
||||||
spec.env,
|
if err != nil {
|
||||||
err,
|
return fmt.Errorf(
|
||||||
)
|
"error reading a CSV string from environment variable %s with multiple values: %v",
|
||||||
|
spec.env,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err = setSliceOrMap(p.val(spec.dest), values, !spec.separate); err != nil {
|
if err = setSliceOrMap(p.val(spec.dest), values, !spec.separate); err != nil {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
|
@ -475,7 +628,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))
|
||||||
|
@ -489,6 +642,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
|
||||||
|
@ -496,7 +658,7 @@ func (p *Parser) process(args []string) error {
|
||||||
// must use explicit for loop, not range, because we manipulate i inside the loop
|
// must use explicit for loop, not range, because we manipulate i inside the loop
|
||||||
for i := 0; i < len(args); i++ {
|
for i := 0; i < len(args); i++ {
|
||||||
arg := args[i]
|
arg := args[i]
|
||||||
if arg == "--" {
|
if arg == "--" && !allpositional {
|
||||||
allpositional = true
|
allpositional = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -516,10 +678,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)
|
||||||
v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers
|
if v.IsNil() {
|
||||||
|
v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers
|
||||||
|
}
|
||||||
|
|
||||||
// add the new options to the set of allowed options
|
// add the new options to the set of allowed options
|
||||||
specs = append(specs, subcmd.specs...)
|
if p.config.StrictSubcommands {
|
||||||
|
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 {
|
||||||
|
@ -530,7 +699,7 @@ func (p *Parser) process(args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
curCmd = subcmd
|
curCmd = subcmd
|
||||||
p.lastCmd = curCmd
|
p.subcommand = append(p.subcommand, arg)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -539,7 +708,9 @@ func (p *Parser) process(args []string) error {
|
||||||
case "-h", "--help":
|
case "-h", "--help":
|
||||||
return ErrHelp
|
return ErrHelp
|
||||||
case "--version":
|
case "--version":
|
||||||
return ErrVersion
|
if !hasVersionOption && p.version != "" {
|
||||||
|
return ErrVersion
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for an equals sign, as in "--foo=bar"
|
// check for an equals sign, as in "--foo=bar"
|
||||||
|
@ -553,7 +724,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
|
||||||
|
@ -562,7 +733,7 @@ func (p *Parser) process(args []string) error {
|
||||||
if spec.cardinality == multiple {
|
if spec.cardinality == multiple {
|
||||||
var values []string
|
var values []string
|
||||||
if value == "" {
|
if value == "" {
|
||||||
for i+1 < len(args) && !isFlag(args[i+1]) && args[i+1] != "--" {
|
for i+1 < len(args) && isValue(args[i+1], spec.field.Type, specs) && args[i+1] != "--" {
|
||||||
values = append(values, args[i+1])
|
values = append(values, args[i+1])
|
||||||
i++
|
i++
|
||||||
if spec.separate {
|
if spec.separate {
|
||||||
|
@ -590,7 +761,7 @@ func (p *Parser) process(args []string) error {
|
||||||
if i+1 == len(args) {
|
if i+1 == len(args) {
|
||||||
return fmt.Errorf("missing value for %s", arg)
|
return fmt.Errorf("missing value for %s", arg)
|
||||||
}
|
}
|
||||||
if !nextIsNumeric(spec.field.Type, args[i+1]) && isFlag(args[i+1]) {
|
if !isValue(args[i+1], spec.field.Type, specs) {
|
||||||
return fmt.Errorf("missing value for %s", arg)
|
return fmt.Errorf("missing value for %s", arg)
|
||||||
}
|
}
|
||||||
value = args[i+1]
|
value = args[i+1]
|
||||||
|
@ -615,13 +786,13 @@ func (p *Parser) process(args []string) error {
|
||||||
if spec.cardinality == multiple {
|
if spec.cardinality == multiple {
|
||||||
err := setSliceOrMap(p.val(spec.dest), positionals, true)
|
err := setSliceOrMap(p.val(spec.dest), positionals, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error processing %s: %v", spec.field.Name, err)
|
return fmt.Errorf("error processing %s: %v", spec.placeholder, err)
|
||||||
}
|
}
|
||||||
positionals = nil
|
positionals = nil
|
||||||
} else {
|
} else {
|
||||||
err := scalar.ParseValue(p.val(spec.dest), positionals[0])
|
err := scalar.ParseValue(p.val(spec.dest), positionals[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error processing %s: %v", spec.field.Name, err)
|
return fmt.Errorf("error processing %s: %v", spec.placeholder, err)
|
||||||
}
|
}
|
||||||
positionals = positionals[1:]
|
positionals = positionals[1:]
|
||||||
}
|
}
|
||||||
|
@ -636,43 +807,58 @@ func (p *Parser) process(args []string) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
name := strings.ToLower(spec.field.Name)
|
if spec.required {
|
||||||
if spec.long != "" && !spec.positional {
|
if spec.short == "" && spec.long == "" {
|
||||||
name = "--" + spec.long
|
msg := fmt.Sprintf("environment variable %s is required", spec.env)
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := fmt.Sprintf("%s is required", spec.placeholder)
|
||||||
|
if spec.env != "" {
|
||||||
|
msg += " (or environment variable " + spec.env + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
if spec.required {
|
if spec.defaultValue.IsValid() && !p.config.IgnoreDefault {
|
||||||
return fmt.Errorf("%s is required", name)
|
// One issue here is that if the user now modifies the value then
|
||||||
}
|
// the default value stored in the spec will be corrupted. There
|
||||||
if spec.defaultVal != "" {
|
// is no general way to "deep-copy" values in Go, and we still
|
||||||
err := scalar.ParseValue(p.val(spec.dest), spec.defaultVal)
|
// support the old-style method for specifying defaults as
|
||||||
if err != nil {
|
// Go values assigned directly to the struct field, so we are stuck.
|
||||||
return fmt.Errorf("error processing default value for %s: %v", name, err)
|
p.val(spec.dest).Set(spec.defaultValue)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func nextIsNumeric(t reflect.Type, s string) bool {
|
|
||||||
switch t.Kind() {
|
|
||||||
case reflect.Ptr:
|
|
||||||
return nextIsNumeric(t.Elem(), s)
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
|
||||||
v := reflect.New(t)
|
|
||||||
err := scalar.ParseValue(v, s)
|
|
||||||
return err == nil
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// isFlag returns true if a token is a flag such as "-v" or "--user" but not "-" or "--"
|
// isFlag returns true if a token is a flag such as "-v" or "--user" but not "-" or "--"
|
||||||
func isFlag(s string) bool {
|
func isFlag(s string) bool {
|
||||||
return strings.HasPrefix(s, "-") && strings.TrimLeft(s, "-") != ""
|
return strings.HasPrefix(s, "-") && strings.TrimLeft(s, "-") != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isValue returns true if a token should be consumed as a value for a flag of type t. This
|
||||||
|
// is almost always the inverse of isFlag. The one exception is for negative numbers, in which
|
||||||
|
// case we check the list of active options and return true if its not present there.
|
||||||
|
func isValue(s string, t reflect.Type, specs []*spec) bool {
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Ptr, reflect.Slice:
|
||||||
|
return isValue(s, t.Elem(), specs)
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
v := reflect.New(t)
|
||||||
|
err := scalar.ParseValue(v, s)
|
||||||
|
// if value can be parsed and is not an explicit option declared elsewhere, then use it as a value
|
||||||
|
if err == nil && (!strings.HasPrefix(s, "-") || findOption(specs, strings.TrimPrefix(s, "-")) == nil) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// default case that is used in all cases other than negative numbers: inverse of isFlag
|
||||||
|
return !isFlag(s)
|
||||||
|
}
|
||||||
|
|
||||||
// val returns a reflect.Value corresponding to the current value for the
|
// val returns a reflect.Value corresponding to the current value for the
|
||||||
// given path
|
// given path
|
||||||
func (p *Parser) val(dest path) reflect.Value {
|
func (p *Parser) val(dest path) reflect.Value {
|
||||||
|
@ -709,6 +895,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
|
||||||
}
|
}
|
||||||
|
|
608
parse_test.go
608
parse_test.go
|
@ -2,9 +2,11 @@ package arg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -26,11 +28,11 @@ func parse(cmdline string, dest interface{}) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func pparse(cmdline string, dest interface{}) (*Parser, error) {
|
func pparse(cmdline string, dest interface{}) (*Parser, error) {
|
||||||
return parseWithEnv(cmdline, nil, dest)
|
return parseWithEnv(Config{}, cmdline, nil, dest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseWithEnv(cmdline string, env []string, dest interface{}) (*Parser, error) {
|
func parseWithEnv(config Config, cmdline string, env []string, dest interface{}) (*Parser, error) {
|
||||||
p, err := NewParser(Config{}, dest)
|
p, err := NewParser(config, dest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -94,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
|
||||||
|
@ -103,17 +120,91 @@ func TestNegativeInt(t *testing.T) {
|
||||||
assert.EqualValues(t, args.Foo, -100)
|
assert.EqualValues(t, args.Foo, -100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNegativeFloat(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
Foo float64
|
||||||
|
}
|
||||||
|
err := parse("-foo -99", &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.EqualValues(t, args.Foo, -99)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNumericFlag(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
UseIPv6 bool `arg:"-6"`
|
||||||
|
Foo int
|
||||||
|
}
|
||||||
|
err := parse("-6", &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.EqualValues(t, args.UseIPv6, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNumericFlagTakesPrecedence(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
UseIPv6 bool `arg:"-6"`
|
||||||
|
Foo int
|
||||||
|
}
|
||||||
|
err := parse("-foo -6", &args)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepeatedNegativeInts(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
Ints []int `arg:"--numbers"`
|
||||||
|
}
|
||||||
|
err := parse("--numbers -1 -2 -6", &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.EqualValues(t, args.Ints, []int{-1, -2, -6})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepeatedNegativeFloats(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
Floats []float32 `arg:"--numbers"`
|
||||||
|
}
|
||||||
|
err := parse("--numbers -1 -2 -6", &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.EqualValues(t, args.Floats, []float32{-1, -2, -6})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepeatedNegativeFloatsThenNumericFlag(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
Floats []float32 `arg:"--numbers"`
|
||||||
|
UseIPv6 bool `arg:"-6"`
|
||||||
|
}
|
||||||
|
err := parse("--numbers -1 -2 -6", &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.EqualValues(t, args.Floats, []float32{-1, -2})
|
||||||
|
assert.True(t, args.UseIPv6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepeatedNegativeFloatsThenNonexistentFlag(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
Floats []float32 `arg:"--numbers"`
|
||||||
|
UseIPv6 bool `arg:"-6"`
|
||||||
|
}
|
||||||
|
err := parse("--numbers -1 -2 -n", &args)
|
||||||
|
require.Error(t, err, "unknown argument -n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepeatedNegativeIntsThenFloat(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
Ints []int `arg:"--numbers"`
|
||||||
|
}
|
||||||
|
err := parse("--numbers -1 -2 -0.1", &args)
|
||||||
|
require.Error(t, err, "unknown argument -0.1")
|
||||||
|
}
|
||||||
|
|
||||||
func TestNegativeIntAndFloatAndTricks(t *testing.T) {
|
func TestNegativeIntAndFloatAndTricks(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo int
|
Foo int
|
||||||
Bar float64
|
Bar float64
|
||||||
N int `arg:"--100"`
|
N int `arg:"--100"`
|
||||||
}
|
}
|
||||||
err := parse("-foo -100 -bar -60.14 -100 -100", &args)
|
err := parse("-foo -99 -bar -60.14 -100 -101", &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.EqualValues(t, args.Foo, -100)
|
assert.EqualValues(t, args.Foo, -99)
|
||||||
assert.EqualValues(t, args.Bar, -60.14)
|
assert.EqualValues(t, args.Bar, -60.14)
|
||||||
assert.EqualValues(t, args.N, -100)
|
assert.EqualValues(t, args.N, -101)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUint(t *testing.T) {
|
func TestUint(t *testing.T) {
|
||||||
|
@ -202,6 +293,22 @@ func TestRequired(t *testing.T) {
|
||||||
require.Error(t, err, "--foo is required")
|
require.Error(t, err, "--foo is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRequiredWithEnv(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
Foo string `arg:"required,env:FOO"`
|
||||||
|
}
|
||||||
|
err := parse("", &args)
|
||||||
|
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(Config{}, "", []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"`
|
||||||
|
@ -492,15 +599,6 @@ func TestMissingValueInMiddle(t *testing.T) {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNegativeValue(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
Foo int
|
|
||||||
}
|
|
||||||
err := parse("--foo -123", &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, -123, args.Foo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvalidInt(t *testing.T) {
|
func TestInvalidInt(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo int
|
Foo int
|
||||||
|
@ -576,6 +674,15 @@ func TestNoMoreOptionsBeforeHelp(t *testing.T) {
|
||||||
assert.NotEqual(t, ErrHelp, err)
|
assert.NotEqual(t, ErrHelp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNoMoreOptionsTwice(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
X []string `arg:"positional"`
|
||||||
|
}
|
||||||
|
err := parse("-- --", &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []string{"--"}, args.X)
|
||||||
|
}
|
||||||
|
|
||||||
func TestHelpFlag(t *testing.T) {
|
func TestHelpFlag(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo string
|
Foo string
|
||||||
|
@ -659,11 +766,26 @@ func TestMustParse(t *testing.T) {
|
||||||
assert.NotNil(t, parser)
|
assert.NotNil(t, parser)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMustParseError(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
Foo []string `default:""`
|
||||||
|
}
|
||||||
|
var exitCode int
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
mustParseExit = func(code int) { exitCode = code }
|
||||||
|
mustParseOut = &stdout
|
||||||
|
os.Args = []string{"example"}
|
||||||
|
parser := MustParse(&args)
|
||||||
|
assert.Nil(t, parser)
|
||||||
|
assert.Equal(t, 2, exitCode)
|
||||||
|
assert.Contains(t, stdout.String(), "default values are not supported for slice or map fields")
|
||||||
|
}
|
||||||
|
|
||||||
func TestEnvironmentVariable(t *testing.T) {
|
func TestEnvironmentVariable(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo string `arg:"env"`
|
Foo string `arg:"env"`
|
||||||
}
|
}
|
||||||
_, err := parseWithEnv("", []string{"FOO=bar"}, &args)
|
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "bar", args.Foo)
|
assert.Equal(t, "bar", args.Foo)
|
||||||
}
|
}
|
||||||
|
@ -672,7 +794,7 @@ func TestEnvironmentVariableNotPresent(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
NotPresent string `arg:"env"`
|
NotPresent string `arg:"env"`
|
||||||
}
|
}
|
||||||
_, err := parseWithEnv("", nil, &args)
|
_, err := parseWithEnv(Config{}, "", nil, &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "", args.NotPresent)
|
assert.Equal(t, "", args.NotPresent)
|
||||||
}
|
}
|
||||||
|
@ -681,7 +803,7 @@ func TestEnvironmentVariableOverrideName(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo string `arg:"env:BAZ"`
|
Foo string `arg:"env:BAZ"`
|
||||||
}
|
}
|
||||||
_, err := parseWithEnv("", []string{"BAZ=bar"}, &args)
|
_, err := parseWithEnv(Config{}, "", []string{"BAZ=bar"}, &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "bar", args.Foo)
|
assert.Equal(t, "bar", args.Foo)
|
||||||
}
|
}
|
||||||
|
@ -690,7 +812,7 @@ func TestEnvironmentVariableOverrideArgument(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo string `arg:"env"`
|
Foo string `arg:"env"`
|
||||||
}
|
}
|
||||||
_, err := parseWithEnv("--foo zzz", []string{"FOO=bar"}, &args)
|
_, err := parseWithEnv(Config{}, "--foo zzz", []string{"FOO=bar"}, &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "zzz", args.Foo)
|
assert.Equal(t, "zzz", args.Foo)
|
||||||
}
|
}
|
||||||
|
@ -699,7 +821,7 @@ func TestEnvironmentVariableError(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo int `arg:"env"`
|
Foo int `arg:"env"`
|
||||||
}
|
}
|
||||||
_, err := parseWithEnv("", []string{"FOO=bar"}, &args)
|
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -707,7 +829,7 @@ func TestEnvironmentVariableRequired(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo string `arg:"env,required"`
|
Foo string `arg:"env,required"`
|
||||||
}
|
}
|
||||||
_, err := parseWithEnv("", []string{"FOO=bar"}, &args)
|
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "bar", args.Foo)
|
assert.Equal(t, "bar", args.Foo)
|
||||||
}
|
}
|
||||||
|
@ -716,16 +838,25 @@ func TestEnvironmentVariableSliceArgumentString(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo []string `arg:"env"`
|
Foo []string `arg:"env"`
|
||||||
}
|
}
|
||||||
_, err := parseWithEnv("", []string{`FOO=bar,"baz, qux"`}, &args)
|
_, err := parseWithEnv(Config{}, "", []string{`FOO=bar,"baz, qux"`}, &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []string{"bar", "baz, qux"}, args.Foo)
|
assert.Equal(t, []string{"bar", "baz, qux"}, args.Foo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnvironmentVariableSliceEmpty(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
Foo []string `arg:"env"`
|
||||||
|
}
|
||||||
|
_, err := parseWithEnv(Config{}, "", []string{`FOO=`}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, args.Foo, 0)
|
||||||
|
}
|
||||||
|
|
||||||
func TestEnvironmentVariableSliceArgumentInteger(t *testing.T) {
|
func TestEnvironmentVariableSliceArgumentInteger(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo []int `arg:"env"`
|
Foo []int `arg:"env"`
|
||||||
}
|
}
|
||||||
_, err := parseWithEnv("", []string{`FOO=1,99`}, &args)
|
_, err := parseWithEnv(Config{}, "", []string{`FOO=1,99`}, &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []int{1, 99}, args.Foo)
|
assert.Equal(t, []int{1, 99}, args.Foo)
|
||||||
}
|
}
|
||||||
|
@ -734,7 +865,7 @@ func TestEnvironmentVariableSliceArgumentFloat(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo []float32 `arg:"env"`
|
Foo []float32 `arg:"env"`
|
||||||
}
|
}
|
||||||
_, err := parseWithEnv("", []string{`FOO=1.1,99.9`}, &args)
|
_, err := parseWithEnv(Config{}, "", []string{`FOO=1.1,99.9`}, &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []float32{1.1, 99.9}, args.Foo)
|
assert.Equal(t, []float32{1.1, 99.9}, args.Foo)
|
||||||
}
|
}
|
||||||
|
@ -743,7 +874,7 @@ func TestEnvironmentVariableSliceArgumentBool(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo []bool `arg:"env"`
|
Foo []bool `arg:"env"`
|
||||||
}
|
}
|
||||||
_, err := parseWithEnv("", []string{`FOO=true,false,0,1`}, &args)
|
_, err := parseWithEnv(Config{}, "", []string{`FOO=true,false,0,1`}, &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []bool{true, false, false, true}, args.Foo)
|
assert.Equal(t, []bool{true, false, false, true}, args.Foo)
|
||||||
}
|
}
|
||||||
|
@ -752,7 +883,7 @@ func TestEnvironmentVariableSliceArgumentWrongCsv(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo []int `arg:"env"`
|
Foo []int `arg:"env"`
|
||||||
}
|
}
|
||||||
_, err := parseWithEnv("", []string{`FOO=1,99\"`}, &args)
|
_, err := parseWithEnv(Config{}, "", []string{`FOO=1,99\"`}, &args)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -760,7 +891,7 @@ func TestEnvironmentVariableSliceArgumentWrongType(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo []bool `arg:"env"`
|
Foo []bool `arg:"env"`
|
||||||
}
|
}
|
||||||
_, err := parseWithEnv("", []string{`FOO=one,two`}, &args)
|
_, err := parseWithEnv(Config{}, "", []string{`FOO=one,two`}, &args)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -768,13 +899,32 @@ func TestEnvironmentVariableMap(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo map[int]string `arg:"env"`
|
Foo map[int]string `arg:"env"`
|
||||||
}
|
}
|
||||||
_, err := parseWithEnv("", []string{`FOO=1=one,99=ninetynine`}, &args)
|
_, err := parseWithEnv(Config{}, "", []string{`FOO=1=one,99=ninetynine`}, &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, args.Foo, 2)
|
assert.Len(t, args.Foo, 2)
|
||||||
assert.Equal(t, "one", args.Foo[1])
|
assert.Equal(t, "one", args.Foo[1])
|
||||||
assert.Equal(t, "ninetynine", args.Foo[99])
|
assert.Equal(t, "ninetynine", args.Foo[99])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnvironmentVariableEmptyMap(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
Foo map[int]string `arg:"env"`
|
||||||
|
}
|
||||||
|
_, err := parseWithEnv(Config{}, "", []string{`FOO=`}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, args.Foo, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvironmentVariableWithPrefix(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
Foo string `arg:"env"`
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := parseWithEnv(Config{EnvPrefix: "MYAPP_"}, "", []string{"MYAPP_FOO=bar"}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "bar", args.Foo)
|
||||||
|
}
|
||||||
|
|
||||||
func TestEnvironmentVariableIgnored(t *testing.T) {
|
func TestEnvironmentVariableIgnored(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo string `arg:"env"`
|
Foo string `arg:"env"`
|
||||||
|
@ -789,6 +939,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(Config{}, "", []string{""}, &args)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptionalEnvironmentOnlyVariable(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
Foo string `arg:"env:FOO"`
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := parseWithEnv(Config{}, "", []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 {
|
||||||
|
@ -801,10 +982,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: 2, 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
|
||||||
}
|
}
|
||||||
|
@ -943,6 +1165,24 @@ func TestPtrToIP(t *testing.T) {
|
||||||
assert.Equal(t, "192.168.0.1", args.Host.String())
|
assert.Equal(t, "192.168.0.1", args.Host.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestURL(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
URL url.URL
|
||||||
|
}
|
||||||
|
err := parse("--url https://example.com/get?item=xyz", &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://example.com/get?item=xyz", args.URL.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPtrToURL(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
URL *url.URL
|
||||||
|
}
|
||||||
|
err := parse("--url http://example.com/#xyz", &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "http://example.com/#xyz", args.URL.String())
|
||||||
|
}
|
||||||
|
|
||||||
func TestIPSlice(t *testing.T) {
|
func TestIPSlice(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Host []net.IP
|
Host []net.IP
|
||||||
|
@ -1077,6 +1317,29 @@ func TestEmbeddedWithDuplicateField2(t *testing.T) {
|
||||||
assert.Equal(t, "", args.U.A)
|
assert.Equal(t, "", args.U.A)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnexportedEmbedded(t *testing.T) {
|
||||||
|
type embeddedArgs struct {
|
||||||
|
Foo string
|
||||||
|
}
|
||||||
|
var args struct {
|
||||||
|
embeddedArgs
|
||||||
|
}
|
||||||
|
err := parse("--foo bar", &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "bar", args.Foo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIgnoredEmbedded(t *testing.T) {
|
||||||
|
type embeddedArgs struct {
|
||||||
|
Foo string
|
||||||
|
}
|
||||||
|
var args struct {
|
||||||
|
embeddedArgs `arg:"-"`
|
||||||
|
}
|
||||||
|
err := parse("--foo bar", &args)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestEmptyArgs(t *testing.T) {
|
func TestEmptyArgs(t *testing.T) {
|
||||||
origArgs := os.Args
|
origArgs := os.Args
|
||||||
|
|
||||||
|
@ -1217,11 +1480,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) {
|
||||||
|
@ -1252,13 +1559,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)
|
||||||
assert.Equal(t, 123, *args.B)
|
if assert.NotNil(t, args.B) {
|
||||||
|
assert.Equal(t, 123, *args.B)
|
||||||
|
}
|
||||||
assert.Equal(t, "xyz", args.C)
|
assert.Equal(t, "xyz", args.C)
|
||||||
assert.Equal(t, "abc", *args.D)
|
if assert.NotNil(t, args.D) {
|
||||||
|
assert.Equal(t, "abc", *args.D)
|
||||||
|
}
|
||||||
assert.Equal(t, 4.56, args.E)
|
assert.Equal(t, 4.56, args.E)
|
||||||
assert.Equal(t, 1.23, *args.F)
|
if assert.NotNil(t, args.F) {
|
||||||
assert.True(t, args.G)
|
assert.Equal(t, 1.23, *args.F)
|
||||||
|
}
|
||||||
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) {
|
||||||
|
@ -1267,7 +1582,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) {
|
||||||
|
@ -1286,13 +1601,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)
|
||||||
assert.Equal(t, 789, *args.B)
|
if assert.NotNil(t, args.B) {
|
||||||
|
assert.Equal(t, 789, *args.B)
|
||||||
|
}
|
||||||
assert.Equal(t, "abc", args.C)
|
assert.Equal(t, "abc", args.C)
|
||||||
assert.Equal(t, "abc", *args.D)
|
if assert.NotNil(t, args.D) {
|
||||||
|
assert.Equal(t, "abc", *args.D)
|
||||||
|
}
|
||||||
assert.Equal(t, 1.23, args.E)
|
assert.Equal(t, 1.23, args.E)
|
||||||
assert.Equal(t, 1.23, *args.F)
|
if assert.NotNil(t, args.F) {
|
||||||
assert.True(t, args.G)
|
assert.Equal(t, 1.23, *args.F)
|
||||||
|
}
|
||||||
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) {
|
||||||
|
@ -1306,7 +1629,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)
|
||||||
|
@ -1323,68 +1646,201 @@ 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, 2, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExitFunctionAndOutStreamGetFilledIn(t *testing.T) {
|
||||||
|
var args struct{}
|
||||||
|
p, err := NewParser(Config{}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, p.config.Exit) // go prohibits function pointer comparison
|
||||||
|
assert.Equal(t, p.config.Out, os.Stdout)
|
||||||
}
|
}
|
||||||
|
|
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 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)
|
||||||
|
}
|
||||||
|
|
255
usage.go
255
usage.go
|
@ -3,43 +3,50 @@ 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
|
// Fail prints usage information to p.Config.Out and exits with status code 2.
|
||||||
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
|
|
||||||
func (p *Parser) Fail(msg string) {
|
func (p *Parser) Fail(msg string) {
|
||||||
p.failWithCommand(msg, p.cmd)
|
p.FailSubcommand(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// failWithCommand prints usage information for the given subcommand to stderr and exits with non-zero status
|
// FailSubcommand prints usage information for a specified subcommand to p.Config.Out,
|
||||||
func (p *Parser) failWithCommand(msg string, cmd *command) {
|
// then exits with status code 2. To write usage information for a top-level
|
||||||
p.writeUsageForCommand(stderr, cmd)
|
// subcommand, provide just the name of that subcommand. To write usage
|
||||||
fmt.Fprintln(stderr, "error:", msg)
|
// information for a subcommand that is nested under another subcommand, provide
|
||||||
osExit(-1)
|
// a sequence of subcommand names starting with the top-level subcommand and so
|
||||||
|
// on down the tree.
|
||||||
|
func (p *Parser) FailSubcommand(msg string, subcommand ...string) error {
|
||||||
|
err := p.WriteUsageForSubcommand(p.config.Out, subcommand...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(p.config.Out, "error:", msg)
|
||||||
|
p.config.Exit(2)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.writeUsageForCommand(w, cmd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeUsageForCommand writes usage information for the given subcommand
|
// WriteUsageForSubcommand writes the usage information for a specified
|
||||||
func (p *Parser) writeUsageForCommand(w io.Writer, cmd *command) {
|
// subcommand. To write usage information for a top-level subcommand, provide
|
||||||
|
// just the name of that subcommand. To write usage information for a subcommand
|
||||||
|
// that is nested under another subcommand, provide a sequence of subcommand
|
||||||
|
// names starting with the top-level subcommand and so on down the tree.
|
||||||
|
func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) error {
|
||||||
|
cmd, err := p.lookupCommand(subcommand...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var positionals, longOptions, shortOptions []*spec
|
var positionals, longOptions, shortOptions []*spec
|
||||||
for _, spec := range cmd.specs {
|
for _, spec := range cmd.specs {
|
||||||
switch {
|
switch {
|
||||||
|
@ -52,22 +59,10 @@ func (p *Parser) writeUsageForCommand(w io.Writer, cmd *command) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if 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
|
||||||
|
@ -95,22 +90,32 @@ func (p *Parser) writeUsageForCommand(w io.Writer, cmd *command) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// write the positional component of the usage message
|
// When we parse positionals, we check that:
|
||||||
|
// 1. required positionals come before non-required positionals
|
||||||
|
// 2. there is at most one multiple-value positional
|
||||||
|
// 3. if there is a multiple-value positional then it comes after all other positionals
|
||||||
|
// Here we merely print the usage string, so we do not explicitly re-enforce those rules
|
||||||
|
|
||||||
|
// write the positionals in following form:
|
||||||
|
// REQUIRED1 REQUIRED2
|
||||||
|
// REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]
|
||||||
|
// REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]
|
||||||
|
// REQUIRED1 REQUIRED2 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]
|
||||||
|
// REQUIRED1 REQUIRED2 [OPTIONAL1 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]]
|
||||||
|
var closeBrackets int
|
||||||
for _, spec := range positionals {
|
for _, spec := range positionals {
|
||||||
// prefix with a space
|
|
||||||
fmt.Fprint(w, " ")
|
fmt.Fprint(w, " ")
|
||||||
|
if !spec.required {
|
||||||
|
fmt.Fprint(w, "[")
|
||||||
|
closeBrackets += 1
|
||||||
|
}
|
||||||
if spec.cardinality == multiple {
|
if spec.cardinality == multiple {
|
||||||
if !spec.required {
|
|
||||||
fmt.Fprint(w, "[")
|
|
||||||
}
|
|
||||||
fmt.Fprintf(w, "%s [%s ...]", spec.placeholder, spec.placeholder)
|
fmt.Fprintf(w, "%s [%s ...]", spec.placeholder, spec.placeholder)
|
||||||
if !spec.required {
|
|
||||||
fmt.Fprint(w, "]")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprint(w, spec.placeholder)
|
fmt.Fprint(w, spec.placeholder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fmt.Fprint(w, strings.Repeat("]", closeBrackets))
|
||||||
|
|
||||||
// if the program supports subcommands, give a hint to the user about their existence
|
// if the program supports subcommands, give a hint to the user about their existence
|
||||||
if len(cmd.subcommands) > 0 {
|
if len(cmd.subcommands) > 0 {
|
||||||
|
@ -118,73 +123,127 @@ func (p *Parser) writeUsageForCommand(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")
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteHelp writes the usage string followed by the full help string for each option
|
func withDefault(s string) string {
|
||||||
func (p *Parser) WriteHelp(w io.Writer) {
|
if s == "" {
|
||||||
cmd := p.cmd
|
return ""
|
||||||
if p.lastCmd != nil {
|
|
||||||
cmd = p.lastCmd
|
|
||||||
}
|
}
|
||||||
p.writeHelpForCommand(w, cmd)
|
return "default: " + s
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeHelp writes the usage string for the given subcommand
|
func withEnv(env string) string {
|
||||||
func (p *Parser) writeHelpForCommand(w io.Writer, cmd *command) {
|
if env == "" {
|
||||||
var positionals, longOptions, shortOptions []*spec
|
return ""
|
||||||
|
}
|
||||||
|
return "env: " + env
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHelp writes the usage string followed by the full help string for each option
|
||||||
|
func (p *Parser) WriteHelp(w io.Writer) {
|
||||||
|
p.WriteHelpForSubcommand(w, p.subcommand...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHelpForSubcommand writes the usage string followed by the full help
|
||||||
|
// string for a specified subcommand. To write help for a top-level subcommand,
|
||||||
|
// provide just the name of that subcommand. To write help for a subcommand that
|
||||||
|
// is nested under another subcommand, provide a sequence of subcommand names
|
||||||
|
// starting with the top-level subcommand and so on down the tree.
|
||||||
|
func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error {
|
||||||
|
cmd, err := p.lookupCommand(subcommand...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var positionals, longOptions, shortOptions, envOnlyOptions []*spec
|
||||||
|
var hasVersionOption bool
|
||||||
for _, spec := range cmd.specs {
|
for _, spec := range cmd.specs {
|
||||||
switch {
|
switch {
|
||||||
case spec.positional:
|
case spec.positional:
|
||||||
positionals = append(positionals, spec)
|
positionals = append(positionals, spec)
|
||||||
case spec.long != "":
|
case spec.long != "":
|
||||||
longOptions = append(longOptions, spec)
|
longOptions = append(longOptions, spec)
|
||||||
|
if spec.long == "version" {
|
||||||
|
hasVersionOption = true
|
||||||
|
}
|
||||||
case spec.short != "":
|
case spec.short != "":
|
||||||
shortOptions = append(shortOptions, spec)
|
shortOptions = append(shortOptions, spec)
|
||||||
|
case spec.short == "" && spec.long == "":
|
||||||
|
envOnlyOptions = append(envOnlyOptions, spec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// obtain a flattened list of options from all ancestors
|
||||||
|
// also determine if any ancestor has a version option spec
|
||||||
|
var globals []*spec
|
||||||
|
ancestor := cmd.parent
|
||||||
|
for ancestor != nil {
|
||||||
|
for _, spec := range ancestor.specs {
|
||||||
|
if spec.long == "version" {
|
||||||
|
hasVersionOption = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globals = append(globals, ancestor.specs...)
|
||||||
|
ancestor = ancestor.parent
|
||||||
|
}
|
||||||
|
|
||||||
if p.description != "" {
|
if p.description != "" {
|
||||||
fmt.Fprintln(w, p.description)
|
fmt.Fprintln(w, p.description)
|
||||||
}
|
}
|
||||||
p.writeUsageForCommand(w, cmd)
|
|
||||||
|
if !hasVersionOption && p.version != "" {
|
||||||
|
fmt.Fprintln(w, p.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, withDefault(spec.defaultString), withEnv(spec.env))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,14 +258,6 @@ func (p *Parser) writeHelpForCommand(w io.Writer, cmd *command) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// obtain a flattened list of options from all ancestors
|
|
||||||
var globals []*spec
|
|
||||||
ancestor := cmd.parent
|
|
||||||
for ancestor != nil {
|
|
||||||
globals = append(globals, ancestor.specs...)
|
|
||||||
ancestor = ancestor.parent
|
|
||||||
}
|
|
||||||
|
|
||||||
// write the list of global options
|
// write the list of global options
|
||||||
if len(globals) > 0 {
|
if len(globals) > 0 {
|
||||||
fmt.Fprint(w, "\nGlobal options:\n")
|
fmt.Fprint(w, "\nGlobal options:\n")
|
||||||
|
@ -222,7 +273,7 @@ func (p *Parser) writeHelpForCommand(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",
|
||||||
|
@ -230,13 +281,27 @@ func (p *Parser) writeHelpForCommand(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) {
|
||||||
|
@ -248,12 +313,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 {
|
||||||
printTwoCols(w, strings.Join(ways, ", "), spec.help, spec.defaultVal, spec.env)
|
print(w, strings.Join(ways, ", "), spec.help, withDefault(spec.defaultString), withEnv(spec.env))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) {
|
||||||
|
ways := make([]string, 0, 2)
|
||||||
|
if spec.required {
|
||||||
|
ways = append(ways, "Required.")
|
||||||
|
} else {
|
||||||
|
ways = append(ways, "Optional.")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
if spec.cardinality == zero {
|
// if the user omits the placeholder tag then we pick one automatically,
|
||||||
|
// but if the user explicitly specifies an empty placeholder then we
|
||||||
|
// leave out the placeholder in the help message
|
||||||
|
if spec.cardinality == zero || spec.placeholder == "" {
|
||||||
return form
|
return form
|
||||||
}
|
}
|
||||||
return form + " " + spec.placeholder
|
return form + " " + spec.placeholder
|
||||||
|
|
666
usage_test.go
666
usage_test.go
|
@ -50,16 +50,20 @@ 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 {
|
||||||
Input string `arg:"positional"`
|
Input string `arg:"positional,required"`
|
||||||
Output []string `arg:"positional" help:"list of outputs"`
|
Output []string `arg:"positional" help:"list of outputs"`
|
||||||
Name string `help:"name to use"`
|
Name string `help:"name to use"`
|
||||||
Value int `help:"secret value"`
|
Value int `help:"secret value"`
|
||||||
|
@ -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)
|
||||||
|
@ -141,10 +146,10 @@ func TestUsageCannotMarshalToString(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUsageLongPositionalWithHelp_legacyForm(t *testing.T) {
|
func TestUsageLongPositionalWithHelp_legacyForm(t *testing.T) {
|
||||||
expectedUsage := "Usage: example VERYLONGPOSITIONALWITHHELP"
|
expectedUsage := "Usage: example [VERYLONGPOSITIONALWITHHELP]"
|
||||||
|
|
||||||
expectedHelp := `
|
expectedHelp := `
|
||||||
Usage: example VERYLONGPOSITIONALWITHHELP
|
Usage: example [VERYLONGPOSITIONALWITHHELP]
|
||||||
|
|
||||||
Positional arguments:
|
Positional arguments:
|
||||||
VERYLONGPOSITIONALWITHHELP
|
VERYLONGPOSITIONALWITHHELP
|
||||||
|
@ -170,10 +175,10 @@ Options:
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUsageLongPositionalWithHelp_newForm(t *testing.T) {
|
func TestUsageLongPositionalWithHelp_newForm(t *testing.T) {
|
||||||
expectedUsage := "Usage: example VERYLONGPOSITIONALWITHHELP"
|
expectedUsage := "Usage: example [VERYLONGPOSITIONALWITHHELP]"
|
||||||
|
|
||||||
expectedHelp := `
|
expectedHelp := `
|
||||||
Usage: example VERYLONGPOSITIONALWITHHELP
|
Usage: example [VERYLONGPOSITIONALWITHHELP]
|
||||||
|
|
||||||
Positional arguments:
|
Positional arguments:
|
||||||
VERYLONGPOSITIONALWITHHELP
|
VERYLONGPOSITIONALWITHHELP
|
||||||
|
@ -232,7 +237,7 @@ func (versioned) Version() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUsageWithVersion(t *testing.T) {
|
func TestUsageWithVersion(t *testing.T) {
|
||||||
expectedUsage := "example 3.2.1\nUsage: example"
|
expectedUsage := "Usage: example"
|
||||||
|
|
||||||
expectedHelp := `
|
expectedHelp := `
|
||||||
example 3.2.1
|
example 3.2.1
|
||||||
|
@ -255,6 +260,233 @@ Options:
|
||||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUsageWithUserDefinedVersionFlag(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example [--version]"
|
||||||
|
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example [--version]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--version this is a user-defined version flag
|
||||||
|
--help, -h display this help and exit
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Args[0] = "example"
|
||||||
|
p, err := NewParser(Config{}, &args)
|
||||||
|
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 TestUsageWithVersionAndUserDefinedVersionFlag(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example [--version]"
|
||||||
|
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example [--version]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--version this is a user-defined version flag
|
||||||
|
--help, -h display this help and exit
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
versioned
|
||||||
|
ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Args[0] = "example"
|
||||||
|
p, err := NewParser(Config{}, &args)
|
||||||
|
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()))
|
||||||
|
}
|
||||||
|
|
||||||
|
type subcommand struct {
|
||||||
|
Number int `arg:"-n,--number" help:"compute something on the given number"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageWithVersionAndSubcommand(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example <command> [<args>]"
|
||||||
|
|
||||||
|
expectedHelp := `
|
||||||
|
example 3.2.1
|
||||||
|
Usage: example <command> [<args>]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--help, -h display this help and exit
|
||||||
|
--version display version and exit
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
cmd
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
versioned
|
||||||
|
Cmd *subcommand `arg:"subcommand"`
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Args[0] = "example"
|
||||||
|
p, err := NewParser(Config{}, &args)
|
||||||
|
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()))
|
||||||
|
|
||||||
|
expectedUsage = "Usage: example cmd [--number NUMBER]"
|
||||||
|
|
||||||
|
expectedHelp = `
|
||||||
|
example 3.2.1
|
||||||
|
Usage: example cmd [--number NUMBER]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--number NUMBER, -n NUMBER
|
||||||
|
compute something on the given number
|
||||||
|
--help, -h display this help and exit
|
||||||
|
--version display version and exit
|
||||||
|
`
|
||||||
|
_ = p.Parse([]string{"cmd"})
|
||||||
|
|
||||||
|
help = bytes.Buffer{}
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
usage = bytes.Buffer{}
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageWithUserDefinedVersionFlagAndSubcommand(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example [--version] <command> [<args>]"
|
||||||
|
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example [--version] <command> [<args>]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--version this is a user-defined version flag
|
||||||
|
--help, -h display this help and exit
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
cmd
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Cmd *subcommand `arg:"subcommand"`
|
||||||
|
ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Args[0] = "example"
|
||||||
|
p, err := NewParser(Config{}, &args)
|
||||||
|
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()))
|
||||||
|
|
||||||
|
expectedUsage = "Usage: example cmd [--number NUMBER]"
|
||||||
|
|
||||||
|
expectedHelp = `
|
||||||
|
Usage: example cmd [--number NUMBER]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--number NUMBER, -n NUMBER
|
||||||
|
compute something on the given number
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
--version this is a user-defined version flag
|
||||||
|
--help, -h display this help and exit
|
||||||
|
`
|
||||||
|
_ = p.Parse([]string{"cmd"})
|
||||||
|
|
||||||
|
help = bytes.Buffer{}
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
usage = bytes.Buffer{}
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageWithVersionAndUserDefinedVersionFlagAndSubcommand(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example [--version] <command> [<args>]"
|
||||||
|
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example [--version] <command> [<args>]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--version this is a user-defined version flag
|
||||||
|
--help, -h display this help and exit
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
cmd
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
versioned
|
||||||
|
Cmd *subcommand `arg:"subcommand"`
|
||||||
|
ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Args[0] = "example"
|
||||||
|
p, err := NewParser(Config{}, &args)
|
||||||
|
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()))
|
||||||
|
|
||||||
|
expectedUsage = "Usage: example cmd [--number NUMBER]"
|
||||||
|
|
||||||
|
expectedHelp = `
|
||||||
|
Usage: example cmd [--number NUMBER]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--number NUMBER, -n NUMBER
|
||||||
|
compute something on the given number
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
--version this is a user-defined version flag
|
||||||
|
--help, -h display this help and exit
|
||||||
|
`
|
||||||
|
_ = p.Parse([]string{"cmd"})
|
||||||
|
|
||||||
|
help = bytes.Buffer{}
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
usage = bytes.Buffer{}
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
}
|
||||||
|
|
||||||
type described struct{}
|
type described struct{}
|
||||||
|
|
||||||
// Described returns the description for this program
|
// Described returns the description for this program
|
||||||
|
@ -285,8 +517,105 @@ 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) {
|
||||||
|
expectedUsage := "Usage: example REQUIRED1 REQUIRED2\n"
|
||||||
|
var args struct {
|
||||||
|
Required1 string `arg:"positional,required"`
|
||||||
|
Required2 string `arg:"positional,required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, usage.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageForMixedPositionals(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]\n"
|
||||||
|
var args struct {
|
||||||
|
Required1 string `arg:"positional,required"`
|
||||||
|
Required2 string `arg:"positional,required"`
|
||||||
|
Optional1 string `arg:"positional"`
|
||||||
|
Optional2 string `arg:"positional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, usage.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageForRepeatedPositionals(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]\n"
|
||||||
|
var args struct {
|
||||||
|
Required1 string `arg:"positional,required"`
|
||||||
|
Required2 string `arg:"positional,required"`
|
||||||
|
Repeated []string `arg:"positional,required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, usage.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageForMixedAndRepeatedPositionals(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2 [REPEATED [REPEATED ...]]]]\n"
|
||||||
|
var args struct {
|
||||||
|
Required1 string `arg:"positional,required"`
|
||||||
|
Required2 string `arg:"positional,required"`
|
||||||
|
Optional1 string `arg:"positional"`
|
||||||
|
Optional2 string `arg:"positional"`
|
||||||
|
Repeated []string `arg:"positional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, usage.String())
|
||||||
|
}
|
||||||
|
|
||||||
func TestRequiredMultiplePositionals(t *testing.T) {
|
func TestRequiredMultiplePositionals(t *testing.T) {
|
||||||
expectedUsage := "Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]"
|
expectedUsage := "Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]\n"
|
||||||
|
|
||||||
expectedHelp := `
|
expectedHelp := `
|
||||||
Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]
|
Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]
|
||||||
|
@ -301,16 +630,60 @@ Options:
|
||||||
RequiredMultiple []string `arg:"positional,required" help:"required multiple positional"`
|
RequiredMultiple []string `arg:"positional,required" help:"required multiple positional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
p, err := NewParser(Config{}, &args)
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
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())
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, usage.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageWithSubcommands(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example child [--values VALUES]"
|
||||||
|
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example child [--values VALUES]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--values VALUES Values
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
--verbose, -v verbosity level
|
||||||
|
--help, -h display this help and exit
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||||
|
Child *struct {
|
||||||
|
Values []float64 `help:"Values"`
|
||||||
|
} `arg:"subcommand:child"`
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Args[0] = "example"
|
||||||
|
p, err := NewParser(Config{}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_ = p.Parse([]string{"child"})
|
||||||
|
|
||||||
|
var help bytes.Buffer
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
var help2 bytes.Buffer
|
||||||
|
p.WriteHelpForSubcommand(&help2, "child")
|
||||||
|
assert.Equal(t, expectedHelp[1:], help2.String())
|
||||||
|
|
||||||
var usage bytes.Buffer
|
var usage bytes.Buffer
|
||||||
p.WriteUsage(&usage)
|
p.WriteUsage(&usage)
|
||||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
|
||||||
|
var usage2 bytes.Buffer
|
||||||
|
p.WriteUsageForSubcommand(&usage2, "child")
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUsageWithNestedSubcommands(t *testing.T) {
|
func TestUsageWithNestedSubcommands(t *testing.T) {
|
||||||
|
@ -348,13 +721,51 @@ 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())
|
||||||
|
|
||||||
|
var help2 bytes.Buffer
|
||||||
|
p.WriteHelpForSubcommand(&help2, "child", "nested")
|
||||||
|
assert.Equal(t, expectedHelp[1:], help2.String())
|
||||||
|
|
||||||
var usage bytes.Buffer
|
var usage bytes.Buffer
|
||||||
p.WriteUsage(&usage)
|
p.WriteUsage(&usage)
|
||||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
|
||||||
|
var usage2 bytes.Buffer
|
||||||
|
p.WriteUsageForSubcommand(&usage2, "child", "nested")
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonexistentSubcommand(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
sub *struct{} `arg:"subcommand"`
|
||||||
|
}
|
||||||
|
p, err := NewParser(Config{Exit: func(int) {}}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
|
||||||
|
err = p.WriteUsageForSubcommand(&b, "does_not_exist")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
err = p.WriteHelpForSubcommand(&b, "does_not_exist")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
err = p.FailSubcommand("something went wrong", "does_not_exist")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
err = p.WriteUsageForSubcommand(&b, "sub", "does_not_exist")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
err = p.WriteHelpForSubcommand(&b, "sub", "does_not_exist")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
err = p.FailSubcommand("something went wrong", "sub", "does_not_exist")
|
||||||
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUsageWithoutLongNames(t *testing.T) {
|
func TestUsageWithoutLongNames(t *testing.T) {
|
||||||
|
@ -373,7 +784,36 @@ 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
|
||||||
|
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 TestUsageWithEmptyPlaceholder(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example [-a] [--b] [--c]"
|
||||||
|
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example [-a] [--b] [--c]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-a some help for a
|
||||||
|
--b some help for b
|
||||||
|
--c, -c some help for c
|
||||||
|
--help, -h display this help and exit
|
||||||
|
`
|
||||||
|
var args struct {
|
||||||
|
ShortOnly string `arg:"-a,--" placeholder:"" help:"some help for a"`
|
||||||
|
LongOnly string `arg:"--b" placeholder:"" help:"some help for b"`
|
||||||
|
Both string `arg:"-c,--c" placeholder:"" help:"some help for c"`
|
||||||
|
}
|
||||||
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
var help bytes.Buffer
|
var help bytes.Buffer
|
||||||
p.WriteHelp(&help)
|
p.WriteHelp(&help)
|
||||||
|
@ -420,10 +860,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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,19 +885,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]
|
||||||
|
@ -461,10 +927,160 @@ 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, 2, exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFailSubcommand(t *testing.T) {
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
var exitCode int
|
||||||
|
exit := func(code int) { exitCode = code }
|
||||||
|
|
||||||
|
expectedStdout := `
|
||||||
|
Usage: example sub
|
||||||
|
error: something went wrong
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Sub *struct{} `arg:"subcommand"`
|
||||||
|
}
|
||||||
|
p, err := NewParser(Config{Program: "example", Exit: exit, Out: &stdout}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = p.FailSubcommand("something went wrong", "sub")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, expectedStdout[1:], stdout.String())
|
||||||
|
assert.Equal(t, 2, 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelpShowsPositionalWithDefault(t *testing.T) {
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example [FOO]
|
||||||
|
|
||||||
|
Positional arguments:
|
||||||
|
FOO this is a positional with a default [default: bar]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--help, -h display this help and exit
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Foo string `arg:"positional" default:"bar" help:"this is a positional with a default"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 TestHelpShowsPositionalWithEnv(t *testing.T) {
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example [FOO]
|
||||||
|
|
||||||
|
Positional arguments:
|
||||||
|
FOO this is a positional with an env variable [env: FOO]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--help, -h display this help and exit
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Foo string `arg:"positional,env:FOO" help:"this is a positional with an env variable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 TestHelpShowsPositionalWithDefaultAndEnv(t *testing.T) {
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example [FOO]
|
||||||
|
|
||||||
|
Positional arguments:
|
||||||
|
FOO this is a positional with a default and an env variable [default: bar, env: FOO]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--help, -h display this help and exit
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Foo string `arg:"positional,env:FOO" default:"bar" help:"this is a positional with a default and an env variable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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