Compare commits
151 Commits
ignore-une
...
main
Author | SHA1 | Date |
---|---|---|
|
5716f9af0d | |
|
3d847431ab | |
|
f92d210ca7 | |
|
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 | |
|
6b16520795 | |
|
0af6f25365 | |
|
530fcb84d4 | |
|
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 | |
|
9d937ba6c9 | |
|
1e81bb6866 | |
|
4354574615 | |
|
473453d8c8 | |
|
a84487a43a | |
|
a0937d1b58 | |
|
01a9fab8d7 | |
|
fe4a138ac8 | |
|
6a01a15f75 | |
|
d4b9b2a008 | |
|
a80336128c | |
|
57f610284f | |
|
91214e01ea | |
|
0100c0a411 | |
|
bb4e7fd4b0 | |
|
ccf882dca7 | |
|
9949860eb3 | |
|
23b96d7aac | |
|
1dfefdc43e | |
|
f4eb7f3a58 | |
|
113aef7114 | |
|
cec6b0d378 | |
|
172800ff9a | |
|
cf2205c84d | |
|
4f2ab5c009 | |
|
ec285c8ec4 | |
|
bd6844a20d | |
|
efe5cdf4da | |
|
b099bc916b | |
|
bfa189218e | |
|
2a91531140 | |
|
788c166025 | |
|
2a23168641 | |
|
438a91dba1 | |
|
04c3fdbd80 | |
|
faebd3e0f2 |
|
@ -0,0 +1 @@
|
|||
github: [alexflint]
|
|
@ -15,17 +15,17 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go: ['1.11', '1.12', '1.13', '1.14']
|
||||
go: ['1.20', '1.21', '1.22']
|
||||
|
||||
steps:
|
||||
- id: go
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build
|
||||
run: go build -v .
|
||||
|
|
|
@ -22,3 +22,5 @@ _testmain.go
|
|||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
go.*
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
all:
|
||||
@echo
|
||||
@echo
|
||||
|
||||
clean:
|
||||
rm -f go.*
|
||||
|
||||
redomod:
|
||||
rm -f go.*
|
||||
GO111MODULE= go mod init
|
||||
GO111MODULE= go mod tidy
|
362
README.md
362
README.md
|
@ -64,7 +64,7 @@ fmt.Println("Input:", args.Input)
|
|||
fmt.Println("Output:", args.Output)
|
||||
```
|
||||
|
||||
```
|
||||
```shell
|
||||
$ ./example src.txt x.out y.out z.out
|
||||
Input: src.txt
|
||||
Output: [x.out y.out z.out]
|
||||
|
@ -80,12 +80,12 @@ arg.MustParse(&args)
|
|||
fmt.Println("Workers:", args.Workers)
|
||||
```
|
||||
|
||||
```
|
||||
```shell
|
||||
$ WORKERS=4 ./example
|
||||
Workers: 4
|
||||
```
|
||||
|
||||
```
|
||||
```shell
|
||||
$ WORKERS=4 ./example --workers=6
|
||||
Workers: 6
|
||||
```
|
||||
|
@ -100,12 +100,12 @@ arg.MustParse(&args)
|
|||
fmt.Println("Workers:", args.Workers)
|
||||
```
|
||||
|
||||
```
|
||||
```shell
|
||||
$ NUM_WORKERS=4 ./example
|
||||
Workers: 4
|
||||
```
|
||||
|
||||
You can provide multiple values using the CSV (RFC 4180) format:
|
||||
You can provide multiple values in environment variables using commas:
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
|
@ -115,12 +115,50 @@ arg.MustParse(&args)
|
|||
fmt.Println("Workers:", args.Workers)
|
||||
```
|
||||
|
||||
```
|
||||
```shell
|
||||
$ WORKERS='1,99' ./example
|
||||
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
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Input string `arg:"positional"`
|
||||
|
@ -134,10 +172,10 @@ arg.MustParse(&args)
|
|||
|
||||
```shell
|
||||
$ ./example -h
|
||||
Usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]]
|
||||
Usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]]
|
||||
|
||||
Positional arguments:
|
||||
INPUT
|
||||
INPUT
|
||||
OUTPUT
|
||||
|
||||
Options:
|
||||
|
@ -158,18 +196,32 @@ var args struct {
|
|||
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
|
||||
var args struct {
|
||||
Foo string
|
||||
Bar bool
|
||||
Test string `arg:"-t,env:TEST" default:"something"`
|
||||
}
|
||||
arg.Foo = "abc"
|
||||
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
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Database string
|
||||
|
@ -185,12 +237,14 @@ Fetching the following IDs from foo: [1 2 3]
|
|||
```
|
||||
|
||||
### Arguments that can be specified multiple times, mixed with positionals
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Commands []string `arg:"-c,separate"`
|
||||
Files []string `arg:"-f,separate"`
|
||||
Databases []string `arg:"positional"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
```
|
||||
|
||||
```shell
|
||||
|
@ -200,22 +254,19 @@ Files [file1 file2 file3]
|
|||
Databases [db1 db2 db3]
|
||||
```
|
||||
|
||||
### Custom validation
|
||||
### Arguments with keys and values
|
||||
|
||||
```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")
|
||||
UserIDs map[string]int
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
fmt.Println(args.UserIDs)
|
||||
```
|
||||
|
||||
```shell
|
||||
./example
|
||||
Usage: samples [--foo FOO] [--bar BAR]
|
||||
error: you must provide either --foo or --bar
|
||||
./example --userids john=123 mary=456
|
||||
map[john:123 mary:456]
|
||||
```
|
||||
|
||||
### Version strings
|
||||
|
@ -240,35 +291,57 @@ $ ./example --version
|
|||
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
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Short string `arg:"-s"`
|
||||
Long string `arg:"--custom-long-option"`
|
||||
ShortAndLong string `arg:"-x,--my-option"`
|
||||
Short string `arg:"-s"`
|
||||
Long string `arg:"--custom-long-option"`
|
||||
ShortAndLong string `arg:"-x,--my-option"`
|
||||
OnlyShort string `arg:"-o,--"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example --help
|
||||
Usage: [--short SHORT] [--custom-long-option CUSTOM-LONG-OPTION] [--my-option MY-OPTION]
|
||||
Usage: example [-o ONLYSHORT] [--short SHORT] [--custom-long-option CUSTOM-LONG-OPTION] [--my-option MY-OPTION]
|
||||
|
||||
Options:
|
||||
--short SHORT, -s SHORT
|
||||
--custom-long-option CUSTOM-LONG-OPTION
|
||||
--my-option MY-OPTION, -x MY-OPTION
|
||||
-o ONLYSHORT
|
||||
--help, -h display this help and exit
|
||||
```
|
||||
|
||||
|
||||
### Embedded structs
|
||||
|
||||
The fields of embedded structs are treated just like regular fields:
|
||||
|
||||
```go
|
||||
|
||||
type DatabaseOptions struct {
|
||||
Host string
|
||||
Username string
|
||||
|
@ -291,6 +364,22 @@ func main() {
|
|||
|
||||
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
|
||||
|
||||
Implement `encoding.TextUnmarshaler` to define your own parsing logic.
|
||||
|
@ -320,6 +409,7 @@ func main() {
|
|||
fmt.Printf("%#v\n", args.Name)
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example --name=foo.bar
|
||||
main.NameDotName{Head:"foo", Tail:"bar"}
|
||||
|
@ -356,6 +446,7 @@ func main() {
|
|||
fmt.Printf("%#v\n", args.Name)
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example --help
|
||||
Usage: test [--name NAME]
|
||||
|
@ -370,8 +461,6 @@ main.NameDotName{Head:"file", Tail:"txt"}
|
|||
|
||||
### Custom placeholders
|
||||
|
||||
*Introduced in version 1.3.0*
|
||||
|
||||
Use the `placeholder` tag to control which placeholder text is used in the usage text.
|
||||
|
||||
```go
|
||||
|
@ -383,6 +472,7 @@ var args struct {
|
|||
}
|
||||
arg.MustParse(&args)
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example -h
|
||||
Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
|
||||
|
@ -400,6 +490,9 @@ Options:
|
|||
|
||||
### 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
|
||||
type args struct {
|
||||
Foo string
|
||||
|
@ -425,9 +518,36 @@ Options:
|
|||
--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:
|
||||
```shell
|
||||
|
@ -489,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
|
||||
|
||||
https://godoc.org/github.com/alexflint/go-arg
|
||||
https://pkg.go.dev/github.com/alexflint/go-arg
|
||||
|
||||
### Rationale
|
||||
|
||||
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.
|
||||
|
||||
|
@ -505,4 +797,4 @@ The idea behind `go-arg` is that Go already has an excellent way to describe dat
|
|||
|
||||
### 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.
|
||||
|
|
8
go.mod
8
go.mod
|
@ -1,8 +0,0 @@
|
|||
module github.com/alexflint/go-arg
|
||||
|
||||
require (
|
||||
github.com/alexflint/go-scalar v1.0.0
|
||||
github.com/stretchr/testify v1.2.2
|
||||
)
|
||||
|
||||
go 1.13
|
8
go.sum
8
go.sum
|
@ -1,8 +0,0 @@
|
|||
github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi+A70=
|
||||
github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw=
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
458
parse.go
458
parse.go
|
@ -5,17 +5,15 @@ import (
|
|||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
scalar "github.com/alexflint/go-scalar"
|
||||
"go.wit.com/dev/alexflint/scalar"
|
||||
)
|
||||
|
||||
// to enable monkey-patching during tests
|
||||
var osExit = os.Exit
|
||||
|
||||
// path represents a sequence of steps to find the output location for an
|
||||
// argument or subcommand in the final destination struct
|
||||
type path struct {
|
||||
|
@ -46,24 +44,25 @@ func (p path) Child(f reflect.StructField) path {
|
|||
|
||||
// spec represents a command line option
|
||||
type spec struct {
|
||||
dest path
|
||||
typ reflect.Type
|
||||
long string
|
||||
short string
|
||||
multiple bool
|
||||
required bool
|
||||
positional bool
|
||||
separate bool
|
||||
help string
|
||||
env string
|
||||
boolean bool
|
||||
defaultVal string // default value for this option
|
||||
placeholder string // name of the data in help
|
||||
dest path
|
||||
field reflect.StructField // the struct field from which this option was created
|
||||
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
|
||||
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
|
||||
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
|
||||
help string // the help text for this option
|
||||
env string // the name of the environment variable for this option, or empty for none
|
||||
defaultValue reflect.Value // default value for this option
|
||||
defaultString string // default value for this option, in string form to be displayed in help text
|
||||
placeholder string // placeholder string in help
|
||||
}
|
||||
|
||||
// command represents a named subcommand, or the top-level command
|
||||
type command struct {
|
||||
name string
|
||||
aliases []string
|
||||
help string
|
||||
dest path
|
||||
specs []*spec
|
||||
|
@ -71,33 +70,49 @@ type command struct {
|
|||
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")
|
||||
|
||||
// ErrVersion indicates that --version was provided
|
||||
// ErrVersion indicates that the builtin --version was provided
|
||||
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 stores the args sent from modules
|
||||
var register []interface{}
|
||||
|
||||
/*
|
||||
Use this in your packages to register
|
||||
variables with go-arg. Then add this to your init()
|
||||
|
||||
package 'foo'
|
||||
function init() {
|
||||
args.Register(&argsFoo)
|
||||
}
|
||||
*/
|
||||
func Register(dest ...interface{}) {
|
||||
register = append(register, dest...)
|
||||
}
|
||||
|
||||
// MustParse processes command line arguments and exits upon failure
|
||||
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 {
|
||||
fmt.Println(err)
|
||||
osExit(-1)
|
||||
return nil // just in case osExit was monkey-patched
|
||||
}
|
||||
|
||||
err = p.Parse(flags())
|
||||
switch {
|
||||
case err == ErrHelp:
|
||||
p.writeHelpForCommand(os.Stdout, p.lastCmd)
|
||||
osExit(0)
|
||||
case err == ErrVersion:
|
||||
fmt.Println(p.version)
|
||||
osExit(0)
|
||||
case err != nil:
|
||||
p.failWithCommand(err.Error(), p.lastCmd)
|
||||
fmt.Fprintln(config.Out, err)
|
||||
config.Exit(2)
|
||||
return nil
|
||||
}
|
||||
|
||||
p.MustParse(flags())
|
||||
return p
|
||||
}
|
||||
|
||||
|
@ -125,6 +140,23 @@ type Config struct {
|
|||
|
||||
// IgnoreEnv instructs the library not to read environment variables
|
||||
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
|
||||
|
@ -134,9 +166,10 @@ type Parser struct {
|
|||
config Config
|
||||
version string
|
||||
description string
|
||||
epilogue string
|
||||
|
||||
// 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
|
||||
|
@ -155,6 +188,14 @@ type Described interface {
|
|||
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.
|
||||
func walkFields(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool) {
|
||||
walkFieldsImpl(t, visit, nil)
|
||||
|
@ -178,6 +219,14 @@ func walkFieldsImpl(t reflect.Type, visit func(field reflect.StructField, owner
|
|||
|
||||
// NewParser constructs a parser from a list of destination structs
|
||||
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
|
||||
var name string
|
||||
switch {
|
||||
|
@ -207,23 +256,36 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
|||
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 {
|
||||
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 {
|
||||
if v := p.val(spec.dest); v.IsValid() && !isZero(v) {
|
||||
if defaultVal, ok := v.Interface().(encoding.TextMarshaler); ok {
|
||||
str, err := defaultVal.MarshalText()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
|
||||
}
|
||||
spec.defaultVal = string(str)
|
||||
} else {
|
||||
spec.defaultVal = fmt.Sprintf("%v", v)
|
||||
// get the value
|
||||
v := p.val(spec.dest)
|
||||
|
||||
// if the value is the "zero value" (e.g. nil pointer, empty struct) then ignore
|
||||
if isZero(v) {
|
||||
continue
|
||||
}
|
||||
|
||||
// store as a default
|
||||
spec.defaultValue = v
|
||||
|
||||
// we need a string to display in help text
|
||||
// if MarshalText is implemented then use that
|
||||
if m, ok := v.Interface().(encoding.TextMarshaler); ok {
|
||||
s, err := m.MarshalText()
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,12 +298,15 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
|||
if dest, ok := dest.(Described); ok {
|
||||
p.description = dest.Description()
|
||||
}
|
||||
if dest, ok := dest.(Epilogued); ok {
|
||||
p.epilogue = dest.Epilogue()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if t.Kind() != reflect.Ptr {
|
||||
return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a %s",
|
||||
|
@ -261,23 +326,30 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
|||
|
||||
var errs []string
|
||||
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")
|
||||
if tag == "-" || !isExported(field.Name) {
|
||||
if tag == "-" {
|
||||
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 {
|
||||
return true
|
||||
}
|
||||
|
||||
// ignore any other unexported field
|
||||
if !isExported(field.Name) {
|
||||
return false
|
||||
}
|
||||
|
||||
// duplicate the entire path to avoid slice overwrites
|
||||
subdest := dest.Child(field)
|
||||
spec := spec{
|
||||
dest: subdest,
|
||||
long: strings.ToLower(field.Name),
|
||||
typ: field.Type,
|
||||
dest: subdest,
|
||||
field: field,
|
||||
long: strings.ToLower(field.Name),
|
||||
}
|
||||
|
||||
help, exists := field.Tag.Lookup("help")
|
||||
|
@ -285,13 +357,8 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
|||
spec.help = help
|
||||
}
|
||||
|
||||
defaultVal, hasDefault := field.Tag.Lookup("default")
|
||||
if hasDefault {
|
||||
spec.defaultVal = defaultVal
|
||||
}
|
||||
|
||||
// Look at the tag
|
||||
var isSubcommand bool // tracks whether this field is a subcommand
|
||||
// process each comma-separated part of the tag
|
||||
var isSubcommand bool
|
||||
for _, key := range strings.Split(tag, ",") {
|
||||
if key == "" {
|
||||
continue
|
||||
|
@ -309,18 +376,13 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
|||
case strings.HasPrefix(key, "--"):
|
||||
spec.long = key[2:]
|
||||
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",
|
||||
t.Name(), field.Name))
|
||||
return false
|
||||
}
|
||||
spec.short = key[1:]
|
||||
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
|
||||
case key == "positional":
|
||||
spec.positional = true
|
||||
|
@ -331,24 +393,30 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
|||
case key == "env":
|
||||
// Use override name if provided
|
||||
if value != "" {
|
||||
spec.env = value
|
||||
spec.env = envPrefix + value
|
||||
} else {
|
||||
spec.env = strings.ToUpper(field.Name)
|
||||
spec.env = envPrefix + strings.ToUpper(field.Name)
|
||||
}
|
||||
case key == "subcommand":
|
||||
// decide on a name for the subcommand
|
||||
cmdname := value
|
||||
if cmdname == "" {
|
||||
cmdname = strings.ToLower(field.Name)
|
||||
var cmdnames []string
|
||||
if value == "" {
|
||||
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
|
||||
subcmd, err := cmdFromStruct(cmdname, subdest, field.Type)
|
||||
subcmd, err := cmdFromStruct(cmdnames[0], subdest, field.Type, envPrefix)
|
||||
if err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
subcmd.aliases = cmdnames[1:]
|
||||
subcmd.parent = &cmd
|
||||
subcmd.help = field.Tag.Get("help")
|
||||
|
||||
|
@ -360,34 +428,70 @@ 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")
|
||||
if hasPlaceholder {
|
||||
spec.placeholder = placeholder
|
||||
} else {
|
||||
} else if spec.long != "" {
|
||||
spec.placeholder = strings.ToUpper(spec.long)
|
||||
} else {
|
||||
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
|
||||
// fields will always fail regardless of whether the arguments it received
|
||||
// exercised those fields.
|
||||
if !isSubcommand {
|
||||
cmd.specs = append(cmd.specs, &spec)
|
||||
var err error
|
||||
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 parseable bool
|
||||
parseable, spec.boolean, spec.multiple = canParse(field.Type)
|
||||
if !parseable {
|
||||
errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported",
|
||||
t.Name(), field.Name, field.Type.String()))
|
||||
return false
|
||||
}
|
||||
if spec.multiple && hasDefault {
|
||||
errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice fields",
|
||||
defaultString, hasDefault := field.Tag.Lookup("default")
|
||||
if hasDefault {
|
||||
// we do not support default values for maps and slices
|
||||
if spec.cardinality == multiple {
|
||||
errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields",
|
||||
t.Name(), field.Name))
|
||||
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
|
||||
return false
|
||||
})
|
||||
|
@ -410,8 +514,15 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
|||
return &cmd, nil
|
||||
}
|
||||
|
||||
// Parse processes the given command line option, storing the results in the field
|
||||
// of the structs from which NewParser was constructed
|
||||
// Parse processes the given command line option, storing the results in the fields
|
||||
// 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 {
|
||||
err := p.process(args)
|
||||
if err != nil {
|
||||
|
@ -428,6 +539,20 @@ func (p *Parser) Parse(args []string) error {
|
|||
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
|
||||
func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error {
|
||||
for _, spec := range specs {
|
||||
|
@ -440,18 +565,22 @@ func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error
|
|||
continue
|
||||
}
|
||||
|
||||
if spec.multiple {
|
||||
if spec.cardinality == multiple {
|
||||
// expect a CSV string in an environment
|
||||
// variable in the case of multiple values
|
||||
values, err := csv.NewReader(strings.NewReader(value)).Read()
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"error reading a CSV string from environment variable %s with multiple values: %v",
|
||||
spec.env,
|
||||
err,
|
||||
)
|
||||
var values []string
|
||||
var err error
|
||||
if len(strings.TrimSpace(value)) > 0 {
|
||||
values, err = csv.NewReader(strings.NewReader(value)).Read()
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"error reading a CSV string from environment variable %s with multiple values: %v",
|
||||
spec.env,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
if err = setSlice(p.val(spec.dest), values, !spec.separate); err != nil {
|
||||
if err = setSliceOrMap(p.val(spec.dest), values, !spec.separate); err != nil {
|
||||
return fmt.Errorf(
|
||||
"error processing environment variable %s with multiple values: %v",
|
||||
spec.env,
|
||||
|
@ -477,7 +606,7 @@ func (p *Parser) process(args []string) error {
|
|||
|
||||
// union of specs for the chain of subcommands encountered so far
|
||||
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
|
||||
specs := make([]*spec, len(curCmd.specs))
|
||||
|
@ -491,6 +620,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
|
||||
var allpositional bool
|
||||
var positionals []string
|
||||
|
@ -498,7 +636,7 @@ func (p *Parser) process(args []string) error {
|
|||
// must use explicit for loop, not range, because we manipulate i inside the loop
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
if arg == "--" {
|
||||
if arg == "--" && !allpositional {
|
||||
allpositional = true
|
||||
continue
|
||||
}
|
||||
|
@ -518,10 +656,17 @@ func (p *Parser) process(args []string) error {
|
|||
|
||||
// instantiate the field to point to a new struct
|
||||
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
|
||||
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
|
||||
if !p.config.IgnoreEnv {
|
||||
|
@ -532,7 +677,7 @@ func (p *Parser) process(args []string) error {
|
|||
}
|
||||
|
||||
curCmd = subcmd
|
||||
p.lastCmd = curCmd
|
||||
p.subcommand = append(p.subcommand, arg)
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -541,7 +686,9 @@ func (p *Parser) process(args []string) error {
|
|||
case "-h", "--help":
|
||||
return ErrHelp
|
||||
case "--version":
|
||||
return ErrVersion
|
||||
if !hasVersionOption && p.version != "" {
|
||||
return ErrVersion
|
||||
}
|
||||
}
|
||||
|
||||
// check for an equals sign, as in "--foo=bar"
|
||||
|
@ -555,13 +702,13 @@ func (p *Parser) process(args []string) error {
|
|||
// 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)
|
||||
spec := findOption(specs, opt)
|
||||
if spec == nil {
|
||||
if spec == nil || opt == "" {
|
||||
return fmt.Errorf("unknown argument %s", arg)
|
||||
}
|
||||
wasPresent[spec] = true
|
||||
|
||||
// deal with the case of multiple values
|
||||
if spec.multiple {
|
||||
if spec.cardinality == multiple {
|
||||
var values []string
|
||||
if value == "" {
|
||||
for i+1 < len(args) && !isFlag(args[i+1]) && args[i+1] != "--" {
|
||||
|
@ -574,7 +721,7 @@ func (p *Parser) process(args []string) error {
|
|||
} else {
|
||||
values = append(values, value)
|
||||
}
|
||||
err := setSlice(p.val(spec.dest), values, !spec.separate)
|
||||
err := setSliceOrMap(p.val(spec.dest), values, !spec.separate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing %s: %v", arg, err)
|
||||
}
|
||||
|
@ -583,7 +730,7 @@ func (p *Parser) process(args []string) error {
|
|||
|
||||
// if it's a flag and it has no value then set the value to true
|
||||
// use boolean because this takes account of TextUnmarshaler
|
||||
if spec.boolean && value == "" {
|
||||
if spec.cardinality == zero && value == "" {
|
||||
value = "true"
|
||||
}
|
||||
|
||||
|
@ -592,7 +739,7 @@ func (p *Parser) process(args []string) error {
|
|||
if i+1 == len(args) {
|
||||
return fmt.Errorf("missing value for %s", arg)
|
||||
}
|
||||
if !nextIsNumeric(spec.typ, args[i+1]) && isFlag(args[i+1]) {
|
||||
if !nextIsNumeric(spec.field.Type, args[i+1]) && isFlag(args[i+1]) {
|
||||
return fmt.Errorf("missing value for %s", arg)
|
||||
}
|
||||
value = args[i+1]
|
||||
|
@ -614,16 +761,16 @@ func (p *Parser) process(args []string) error {
|
|||
break
|
||||
}
|
||||
wasPresent[spec] = true
|
||||
if spec.multiple {
|
||||
err := setSlice(p.val(spec.dest), positionals, true)
|
||||
if spec.cardinality == multiple {
|
||||
err := setSliceOrMap(p.val(spec.dest), positionals, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing %s: %v", spec.long, err)
|
||||
return fmt.Errorf("error processing %s: %v", spec.field.Name, err)
|
||||
}
|
||||
positionals = nil
|
||||
} else {
|
||||
err := scalar.ParseValue(p.val(spec.dest), positionals[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing %s: %v", spec.long, err)
|
||||
return fmt.Errorf("error processing %s: %v", spec.field.Name, err)
|
||||
}
|
||||
positionals = positionals[1:]
|
||||
}
|
||||
|
@ -638,19 +785,32 @@ func (p *Parser) process(args []string) error {
|
|||
continue
|
||||
}
|
||||
|
||||
name := spec.long
|
||||
if !spec.positional {
|
||||
name := strings.ToLower(spec.field.Name)
|
||||
if spec.long != "" && !spec.positional {
|
||||
name = "--" + spec.long
|
||||
}
|
||||
|
||||
if spec.required {
|
||||
return fmt.Errorf("%s is required", name)
|
||||
}
|
||||
if spec.defaultVal != "" {
|
||||
err := scalar.ParseValue(p.val(spec.dest), spec.defaultVal)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing default value for %s: %v", name, err)
|
||||
if spec.short == "" && spec.long == "" {
|
||||
msg := fmt.Sprintf("environment variable %s is required", spec.env)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("%s is required", name)
|
||||
if spec.env != "" {
|
||||
msg += " (or environment variable " + spec.env + ")"
|
||||
}
|
||||
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
if spec.defaultValue.IsValid() && !p.config.IgnoreDefault {
|
||||
// One issue here is that if the user now modifies the value then
|
||||
// the default value stored in the spec will be corrupted. There
|
||||
// is no general way to "deep-copy" values in Go, and we still
|
||||
// support the old-style method for specifying defaults as
|
||||
// Go values assigned directly to the struct field, so we are stuck.
|
||||
p.val(spec.dest).Set(spec.defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -687,50 +847,11 @@ func (p *Parser) val(dest path) reflect.Value {
|
|||
v = v.Elem()
|
||||
}
|
||||
|
||||
next := v.FieldByIndex(field.Index)
|
||||
if !next.IsValid() {
|
||||
// it is appropriate to panic here because this can only happen due to
|
||||
// an internal bug in this library (since we construct the path ourselves
|
||||
// by reflecting on the same struct)
|
||||
panic(fmt.Errorf("error resolving path %v: %v has no field named %v",
|
||||
dest.fields, v.Type(), field))
|
||||
}
|
||||
v = next
|
||||
v = v.FieldByIndex(field.Index)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// parse a value as the appropriate type and store it in the struct
|
||||
func setSlice(dest reflect.Value, values []string, trunc bool) error {
|
||||
if !dest.CanSet() {
|
||||
return fmt.Errorf("field is not writable")
|
||||
}
|
||||
|
||||
var ptr bool
|
||||
elem := dest.Type().Elem()
|
||||
if elem.Kind() == reflect.Ptr && !elem.Implements(textUnmarshalerType) {
|
||||
ptr = true
|
||||
elem = elem.Elem()
|
||||
}
|
||||
|
||||
// Truncate the dest slice in case default values exist
|
||||
if trunc && !dest.IsNil() {
|
||||
dest.SetLen(0)
|
||||
}
|
||||
|
||||
for _, s := range values {
|
||||
v := reflect.New(elem)
|
||||
if err := scalar.ParseValue(v.Elem(), s); err != nil {
|
||||
return err
|
||||
}
|
||||
if !ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
dest.Set(reflect.Append(dest, v))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findOption finds an option from its name, or returns null if no spec is found
|
||||
func findOption(specs []*spec, name string) *spec {
|
||||
for _, spec := range specs {
|
||||
|
@ -750,18 +871,11 @@ func findSubcommand(cmds []*command, name string) *command {
|
|||
if cmd.name == name {
|
||||
return cmd
|
||||
}
|
||||
for _, alias := range cmd.aliases {
|
||||
if alias == name {
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isZero returns true if v contains the zero value for its type
|
||||
func isZero(v reflect.Value) bool {
|
||||
t := v.Type()
|
||||
if t.Kind() == reflect.Slice {
|
||||
return v.IsNil()
|
||||
}
|
||||
if !t.Comparable() {
|
||||
return false
|
||||
}
|
||||
return v.Interface() == reflect.Zero(t).Interface()
|
||||
}
|
||||
|
|
114
reflect.go
114
reflect.go
|
@ -2,57 +2,82 @@ package arg
|
|||
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
scalar "github.com/alexflint/go-scalar"
|
||||
"go.wit.com/dev/alexflint/scalar"
|
||||
)
|
||||
|
||||
var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()
|
||||
|
||||
// canParse returns true if the type can be parsed from a string
|
||||
func canParse(t reflect.Type) (parseable, boolean, multiple bool) {
|
||||
parseable = scalar.CanParse(t)
|
||||
boolean = isBoolean(t)
|
||||
if parseable {
|
||||
return
|
||||
}
|
||||
// cardinality tracks how many tokens are expected for a given spec
|
||||
// - zero is a boolean, which does to expect any value
|
||||
// - 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
|
||||
type cardinality int
|
||||
|
||||
// Look inside pointer types
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
// Look inside slice types
|
||||
if t.Kind() == reflect.Slice {
|
||||
multiple = true
|
||||
t = t.Elem()
|
||||
}
|
||||
const (
|
||||
zero cardinality = iota
|
||||
one
|
||||
multiple
|
||||
unsupported
|
||||
)
|
||||
|
||||
parseable = scalar.CanParse(t)
|
||||
boolean = isBoolean(t)
|
||||
if parseable {
|
||||
return
|
||||
func (k cardinality) String() string {
|
||||
switch k {
|
||||
case zero:
|
||||
return "zero"
|
||||
case one:
|
||||
return "one"
|
||||
case multiple:
|
||||
return "multiple"
|
||||
case unsupported:
|
||||
return "unsupported"
|
||||
default:
|
||||
return fmt.Sprintf("unknown(%d)", int(k))
|
||||
}
|
||||
|
||||
// Look inside pointer types (again, in case of []*Type)
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
parseable = scalar.CanParse(t)
|
||||
boolean = isBoolean(t)
|
||||
if parseable {
|
||||
return
|
||||
}
|
||||
|
||||
return false, false, false
|
||||
}
|
||||
|
||||
// isBoolean returns true if the type can be parsed from a single string
|
||||
// cardinalityOf returns true if the type can be parsed from a string
|
||||
func cardinalityOf(t reflect.Type) (cardinality, error) {
|
||||
if scalar.CanParse(t) {
|
||||
if isBoolean(t) {
|
||||
return zero, nil
|
||||
}
|
||||
return one, nil
|
||||
}
|
||||
|
||||
// look inside pointer types
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
// look inside slice and map types
|
||||
switch t.Kind() {
|
||||
case reflect.Slice:
|
||||
if !scalar.CanParse(t.Elem()) {
|
||||
return unsupported, fmt.Errorf("cannot parse into %v because %v not supported", t, t.Elem())
|
||||
}
|
||||
return multiple, nil
|
||||
case reflect.Map:
|
||||
if !scalar.CanParse(t.Key()) {
|
||||
return unsupported, fmt.Errorf("cannot parse into %v because key type %v not supported", t, t.Elem())
|
||||
}
|
||||
if !scalar.CanParse(t.Elem()) {
|
||||
return unsupported, fmt.Errorf("cannot parse into %v because value type %v not supported", t, t.Elem())
|
||||
}
|
||||
return multiple, nil
|
||||
default:
|
||||
return unsupported, fmt.Errorf("cannot parse into %v", t)
|
||||
}
|
||||
}
|
||||
|
||||
// isBoolean returns true if the type is a boolean or a pointer to a boolean
|
||||
func isBoolean(t reflect.Type) bool {
|
||||
switch {
|
||||
case t.Implements(textUnmarshalerType):
|
||||
case isTextUnmarshaler(t):
|
||||
return false
|
||||
case t.Kind() == reflect.Bool:
|
||||
return true
|
||||
|
@ -63,8 +88,25 @@ 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
|
||||
func isExported(field string) bool {
|
||||
r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8
|
||||
return unicode.IsLetter(r) && unicode.IsUpper(r)
|
||||
}
|
||||
|
||||
// isZero returns true if v contains the zero value for its type
|
||||
func isZero(v reflect.Value) bool {
|
||||
t := v.Type()
|
||||
if t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice || t.Kind() == reflect.Map || t.Kind() == reflect.Chan || t.Kind() == reflect.Interface {
|
||||
return v.IsNil()
|
||||
}
|
||||
if !t.Comparable() {
|
||||
return false
|
||||
}
|
||||
return v.Interface() == reflect.Zero(t).Interface()
|
||||
}
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func assertCanParse(t *testing.T, typ reflect.Type, parseable, boolean, multiple bool) {
|
||||
p, b, m := canParse(typ)
|
||||
assert.Equal(t, parseable, p, "expected %v to have parseable=%v but was %v", typ, parseable, p)
|
||||
assert.Equal(t, boolean, b, "expected %v to have boolean=%v but was %v", typ, boolean, b)
|
||||
assert.Equal(t, multiple, m, "expected %v to have multiple=%v but was %v", typ, multiple, m)
|
||||
}
|
||||
|
||||
func TestCanParse(t *testing.T) {
|
||||
var b bool
|
||||
var i int
|
||||
var s string
|
||||
var f float64
|
||||
var bs []bool
|
||||
var is []int
|
||||
|
||||
assertCanParse(t, reflect.TypeOf(b), true, true, false)
|
||||
assertCanParse(t, reflect.TypeOf(i), true, false, false)
|
||||
assertCanParse(t, reflect.TypeOf(s), true, false, false)
|
||||
assertCanParse(t, reflect.TypeOf(f), true, false, false)
|
||||
|
||||
assertCanParse(t, reflect.TypeOf(&b), true, true, false)
|
||||
assertCanParse(t, reflect.TypeOf(&s), true, false, false)
|
||||
assertCanParse(t, reflect.TypeOf(&i), true, false, false)
|
||||
assertCanParse(t, reflect.TypeOf(&f), true, false, false)
|
||||
|
||||
assertCanParse(t, reflect.TypeOf(bs), true, true, true)
|
||||
assertCanParse(t, reflect.TypeOf(&bs), true, true, true)
|
||||
|
||||
assertCanParse(t, reflect.TypeOf(is), true, false, true)
|
||||
assertCanParse(t, reflect.TypeOf(&is), true, false, true)
|
||||
}
|
||||
|
||||
type implementsTextUnmarshaler struct{}
|
||||
|
||||
func (*implementsTextUnmarshaler) UnmarshalText(text []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCanParseTextUnmarshaler(t *testing.T) {
|
||||
var u implementsTextUnmarshaler
|
||||
var su []implementsTextUnmarshaler
|
||||
assertCanParse(t, reflect.TypeOf(u), true, false, false)
|
||||
assertCanParse(t, reflect.TypeOf(&u), true, false, false)
|
||||
assertCanParse(t, reflect.TypeOf(su), true, false, true)
|
||||
assertCanParse(t, reflect.TypeOf(&su), true, false, true)
|
||||
}
|
||||
|
||||
func TestIsExported(t *testing.T) {
|
||||
assert.True(t, isExported("Exported"))
|
||||
assert.False(t, isExported("notExported"))
|
||||
assert.False(t, isExported(""))
|
||||
assert.False(t, isExported(string([]byte{255})))
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"go.wit.com/dev/alexflint/scalar"
|
||||
)
|
||||
|
||||
// setSliceOrMap parses a sequence of strings into a slice or map. If clear is
|
||||
// true then any values already in the slice or map are first removed.
|
||||
func setSliceOrMap(dest reflect.Value, values []string, clear bool) error {
|
||||
if !dest.CanSet() {
|
||||
return fmt.Errorf("field is not writable")
|
||||
}
|
||||
|
||||
t := dest.Type()
|
||||
if t.Kind() == reflect.Ptr {
|
||||
dest = dest.Elem()
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
switch t.Kind() {
|
||||
case reflect.Slice:
|
||||
return setSlice(dest, values, clear)
|
||||
case reflect.Map:
|
||||
return setMap(dest, values, clear)
|
||||
default:
|
||||
return fmt.Errorf("setSliceOrMap cannot insert values into a %v", t)
|
||||
}
|
||||
}
|
||||
|
||||
// setSlice parses a sequence of strings and inserts them into a slice. If clear
|
||||
// is true then any values already in the slice are removed.
|
||||
func setSlice(dest reflect.Value, values []string, clear bool) error {
|
||||
var ptr bool
|
||||
elem := dest.Type().Elem()
|
||||
if elem.Kind() == reflect.Ptr && !elem.Implements(textUnmarshalerType) {
|
||||
ptr = true
|
||||
elem = elem.Elem()
|
||||
}
|
||||
|
||||
// clear the slice in case default values exist
|
||||
if clear && !dest.IsNil() {
|
||||
dest.SetLen(0)
|
||||
}
|
||||
|
||||
// parse the values one-by-one
|
||||
for _, s := range values {
|
||||
v := reflect.New(elem)
|
||||
if err := scalar.ParseValue(v.Elem(), s); err != nil {
|
||||
return err
|
||||
}
|
||||
if !ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
dest.Set(reflect.Append(dest, v))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setMap parses a sequence of name=value strings and inserts them into a map.
|
||||
// If clear is true then any values already in the map are removed.
|
||||
func setMap(dest reflect.Value, values []string, clear bool) error {
|
||||
// determine the key and value type
|
||||
var keyIsPtr bool
|
||||
keyType := dest.Type().Key()
|
||||
if keyType.Kind() == reflect.Ptr && !keyType.Implements(textUnmarshalerType) {
|
||||
keyIsPtr = true
|
||||
keyType = keyType.Elem()
|
||||
}
|
||||
|
||||
var valIsPtr bool
|
||||
valType := dest.Type().Elem()
|
||||
if valType.Kind() == reflect.Ptr && !valType.Implements(textUnmarshalerType) {
|
||||
valIsPtr = true
|
||||
valType = valType.Elem()
|
||||
}
|
||||
|
||||
// clear the slice in case default values exist
|
||||
if clear && !dest.IsNil() {
|
||||
for _, k := range dest.MapKeys() {
|
||||
dest.SetMapIndex(k, reflect.Value{})
|
||||
}
|
||||
}
|
||||
|
||||
// allocate the map if it is not allocated
|
||||
if dest.IsNil() {
|
||||
dest.Set(reflect.MakeMap(dest.Type()))
|
||||
}
|
||||
|
||||
// parse the values one-by-one
|
||||
for _, s := range values {
|
||||
// split at the first equals sign
|
||||
pos := strings.Index(s, "=")
|
||||
if pos == -1 {
|
||||
return fmt.Errorf("cannot parse %q into a map, expected format key=value", s)
|
||||
}
|
||||
|
||||
// parse the key
|
||||
k := reflect.New(keyType)
|
||||
if err := scalar.ParseValue(k.Elem(), s[:pos]); err != nil {
|
||||
return err
|
||||
}
|
||||
if !keyIsPtr {
|
||||
k = k.Elem()
|
||||
}
|
||||
|
||||
// parse the value
|
||||
v := reflect.New(valType)
|
||||
if err := scalar.ParseValue(v.Elem(), s[pos+1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if !valIsPtr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
// add it to the map
|
||||
dest.SetMapIndex(k, v)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package arg
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Subcommand returns the user struct for the subcommand selected by
|
||||
// the command line arguments most recently processed by the parser.
|
||||
// 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
|
||||
// returns nil.
|
||||
func (p *Parser) Subcommand() interface{} {
|
||||
if p.lastCmd == nil || p.lastCmd.parent == nil {
|
||||
if len(p.subcommand) == 0 {
|
||||
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
|
||||
// user. If no subcommands were given then it returns an empty slice.
|
||||
func (p *Parser) SubcommandNames() []string {
|
||||
if p.lastCmd == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// make a list of ancestor commands
|
||||
var ancestors []string
|
||||
cur := p.lastCmd
|
||||
for cur.parent != nil { // we want to exclude the root
|
||||
ancestors = append(ancestors, cur.name)
|
||||
cur = cur.parent
|
||||
}
|
||||
|
||||
// reverse the list
|
||||
out := make([]string, len(ancestors))
|
||||
for i := 0; i < len(ancestors); i++ {
|
||||
out[i] = ancestors[len(ancestors)-i-1]
|
||||
}
|
||||
return out
|
||||
return p.subcommand
|
||||
}
|
||||
|
||||
// lookupCommand finds a subcommand based on a sequence of subcommand names. The
|
||||
// first string should be a top-level subcommand, the next should be a child
|
||||
// subcommand of that subcommand, and so on. If no strings are given then the
|
||||
// root command is returned. If no such subcommand exists then an error is
|
||||
// returned.
|
||||
func (p *Parser) lookupCommand(path ...string) (*command, error) {
|
||||
cmd := p.cmd
|
||||
for _, name := range path {
|
||||
found := findSubcommand(cmd.subcommands, name)
|
||||
if found == nil {
|
||||
return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
|
||||
}
|
||||
cmd = found
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
all:
|
||||
@echo
|
||||
@echo
|
||||
|
||||
test:
|
||||
|
||||
redomod:
|
||||
rm -f go.*
|
||||
GO111MODULE= go mod init
|
||||
GO111MODULE= go mod tidy
|
|
@ -2,8 +2,12 @@ package arg
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func split(s string) []string {
|
||||
|
@ -82,6 +86,48 @@ func Example_multipleValues() {
|
|||
// output: Fetching the following IDs from localhost: [1 2 3]
|
||||
}
|
||||
|
||||
// This example demonstrates arguments with keys and values
|
||||
func Example_mappings() {
|
||||
// The args you would pass in on the command line
|
||||
os.Args = split("./example --userids john=123 mary=456")
|
||||
|
||||
var args struct {
|
||||
UserIDs map[string]int
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println(args.UserIDs)
|
||||
// output: map[john:123 mary:456]
|
||||
}
|
||||
|
||||
type commaSeparated struct {
|
||||
M map[string]string
|
||||
}
|
||||
|
||||
func (c *commaSeparated) UnmarshalText(b []byte) error {
|
||||
c.M = make(map[string]string)
|
||||
for _, part := range strings.Split(string(b), ",") {
|
||||
pos := strings.Index(part, "=")
|
||||
if pos == -1 {
|
||||
return fmt.Errorf("error parsing %q, expected format key=value", part)
|
||||
}
|
||||
c.M[part[:pos]] = part[pos+1:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// This example demonstrates arguments with keys and values separated by commas
|
||||
func Example_mappingWithCommas() {
|
||||
// The args you would pass in on the command line
|
||||
os.Args = split("./example --values one=two,three=four")
|
||||
|
||||
var args struct {
|
||||
Values commaSeparated
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println(args.Values.M)
|
||||
// output: map[one:two three:four]
|
||||
}
|
||||
|
||||
// This eample demonstrates multiple value arguments that can be mixed with
|
||||
// other arguments.
|
||||
func Example_multipleMixed() {
|
||||
|
@ -108,7 +154,7 @@ func Example_helpText() {
|
|||
os.Args = split("./example --help")
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional"`
|
||||
Input string `arg:"positional,required"`
|
||||
Output []string `arg:"positional"`
|
||||
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||
Dataset string `help:"dataset to use"`
|
||||
|
@ -116,7 +162,8 @@ func Example_helpText() {
|
|||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
|
@ -141,25 +188,25 @@ func Example_helpPlaceholder() {
|
|||
os.Args = split("./example --help")
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional" placeholder:"SRC"`
|
||||
Input string `arg:"positional,required" placeholder:"SRC"`
|
||||
Output []string `arg:"positional" placeholder:"DST"`
|
||||
Optimize int `arg:"-O" help:"optimization level" placeholder:"LEVEL"`
|
||||
MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" placeholder:"N"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
|
||||
// Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
|
||||
|
||||
//
|
||||
// Positional arguments:
|
||||
// SRC
|
||||
// DST
|
||||
|
||||
//
|
||||
// Options:
|
||||
// --optimize LEVEL, -O LEVEL
|
||||
// optimization level
|
||||
|
@ -188,7 +235,8 @@ func Example_helpTextWithSubcommand() {
|
|||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
|
@ -205,7 +253,44 @@ func Example_helpTextWithSubcommand() {
|
|||
}
|
||||
|
||||
// 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
|
||||
os.Args = split("./example get --help")
|
||||
|
||||
|
@ -225,28 +310,83 @@ func Example_helpTextForSubcommand() {
|
|||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
exit := func(int) {}
|
||||
|
||||
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:
|
||||
// Usage: example get ITEM
|
||||
// Usage: example list [--format FORMAT] [--limit LIMIT]
|
||||
//
|
||||
// Positional arguments:
|
||||
// ITEM item to fetch
|
||||
// Options:
|
||||
// --format FORMAT output format
|
||||
// --limit LIMIT
|
||||
//
|
||||
// Global options:
|
||||
// --verbose
|
||||
// --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
|
||||
func Example_errorText() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --optimize INVALID")
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional"`
|
||||
Input string `arg:"positional,required"`
|
||||
Output []string `arg:"positional"`
|
||||
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||
Dataset string `help:"dataset to use"`
|
||||
|
@ -254,8 +394,8 @@ func Example_errorText() {
|
|||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
stderr = os.Stdout
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
|
@ -278,8 +418,8 @@ func Example_errorTextForSubcommand() {
|
|||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
stderr = os.Stdout
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
|
@ -314,8 +454,8 @@ func Example_subcommand() {
|
|||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
stderr = os.Stdout
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
|
@ -331,3 +471,76 @@ func Example_subcommand() {
|
|||
// output:
|
||||
// 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
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
module go.wit.com/dev/alexflint/arg/test
|
||||
|
||||
go 1.21.4
|
||||
|
||||
require github.com/stretchr/testify v1.8.4
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
|
@ -0,0 +1,10 @@
|
|||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -1,8 +1,12 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -24,14 +28,34 @@ func parse(cmdline string, dest interface{}) error {
|
|||
}
|
||||
|
||||
func pparse(cmdline string, dest interface{}) (*Parser, error) {
|
||||
p, err := NewParser(Config{}, dest)
|
||||
return parseWithEnv(Config{}, cmdline, nil, dest)
|
||||
}
|
||||
|
||||
func parseWithEnv(config Config, cmdline string, env []string, dest interface{}) (*Parser, error) {
|
||||
p, err := NewParser(config, dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// split the command line
|
||||
var parts []string
|
||||
if len(cmdline) > 0 {
|
||||
parts = strings.Split(cmdline, " ")
|
||||
}
|
||||
|
||||
// split the environment vars
|
||||
for _, s := range env {
|
||||
pos := strings.Index(s, "=")
|
||||
if pos == -1 {
|
||||
return nil, fmt.Errorf("missing equals sign in %q", s)
|
||||
}
|
||||
err := os.Setenv(s[:pos], s[pos+1:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// execute the parser
|
||||
return p, p.Parse(parts)
|
||||
}
|
||||
|
||||
|
@ -72,6 +96,21 @@ func TestInt(t *testing.T) {
|
|||
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) {
|
||||
var args struct {
|
||||
Foo int
|
||||
|
@ -180,6 +219,22 @@ func TestRequired(t *testing.T) {
|
|||
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) {
|
||||
var args struct {
|
||||
Foo string `arg:"-f"`
|
||||
|
@ -220,6 +275,60 @@ func TestLongFlag(t *testing.T) {
|
|||
assert.Equal(t, "xyz", args.Foo)
|
||||
}
|
||||
|
||||
func TestSlice(t *testing.T) {
|
||||
var args struct {
|
||||
Strings []string
|
||||
}
|
||||
err := parse("--strings a b c", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"a", "b", "c"}, args.Strings)
|
||||
}
|
||||
func TestSliceOfBools(t *testing.T) {
|
||||
var args struct {
|
||||
B []bool
|
||||
}
|
||||
|
||||
err := parse("--b true false true", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []bool{true, false, true}, args.B)
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
var args struct {
|
||||
Values map[string]int
|
||||
}
|
||||
err := parse("--values a=1 b=2 c=3", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, args.Values, 3)
|
||||
assert.Equal(t, 1, args.Values["a"])
|
||||
assert.Equal(t, 2, args.Values["b"])
|
||||
assert.Equal(t, 3, args.Values["c"])
|
||||
}
|
||||
|
||||
func TestMapPositional(t *testing.T) {
|
||||
var args struct {
|
||||
Values map[string]int `arg:"positional"`
|
||||
}
|
||||
err := parse("a=1 b=2 c=3", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, args.Values, 3)
|
||||
assert.Equal(t, 1, args.Values["a"])
|
||||
assert.Equal(t, 2, args.Values["b"])
|
||||
assert.Equal(t, 3, args.Values["c"])
|
||||
}
|
||||
|
||||
func TestMapWithSeparate(t *testing.T) {
|
||||
var args struct {
|
||||
Values map[string]int `arg:"separate"`
|
||||
}
|
||||
err := parse("--values a=1 --values b=2 --values c=3", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, args.Values, 3)
|
||||
assert.Equal(t, 1, args.Values["a"])
|
||||
assert.Equal(t, 2, args.Values["b"])
|
||||
assert.Equal(t, 3, args.Values["c"])
|
||||
}
|
||||
|
||||
func TestPlaceholder(t *testing.T) {
|
||||
var args struct {
|
||||
Input string `arg:"positional" placeholder:"SRC"`
|
||||
|
@ -231,6 +340,18 @@ func TestPlaceholder(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNoLongName(t *testing.T) {
|
||||
var args struct {
|
||||
ShortOnly string `arg:"-s,--"`
|
||||
EnvOnly string `arg:"--,env"`
|
||||
}
|
||||
setenv(t, "ENVONLY", "TestVal")
|
||||
err := parse("-s TestVal2", &args)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "TestVal", args.EnvOnly)
|
||||
assert.Equal(t, "TestVal2", args.ShortOnly)
|
||||
}
|
||||
|
||||
func TestCaseSensitive(t *testing.T) {
|
||||
var args struct {
|
||||
Lower bool `arg:"-v"`
|
||||
|
@ -395,7 +516,7 @@ func TestMissingValueAtEnd(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMissingValueInMIddle(t *testing.T) {
|
||||
func TestMissingValueInMiddle(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string
|
||||
Bar string
|
||||
|
@ -480,6 +601,23 @@ func TestNoMoreOptions(t *testing.T) {
|
|||
assert.Equal(t, []string{"abc", "--foo", "xyz"}, args.Bar)
|
||||
}
|
||||
|
||||
func TestNoMoreOptionsBeforeHelp(t *testing.T) {
|
||||
var args struct {
|
||||
Foo int
|
||||
}
|
||||
err := parse("not_an_integer -- --help", &args)
|
||||
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) {
|
||||
var args struct {
|
||||
Foo string
|
||||
|
@ -563,13 +701,27 @@ func TestMustParse(t *testing.T) {
|
|||
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) {
|
||||
var args struct {
|
||||
Foo string `arg:"env"`
|
||||
}
|
||||
setenv(t, "FOO", "bar")
|
||||
os.Args = []string{"example"}
|
||||
MustParse(&args)
|
||||
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "bar", args.Foo)
|
||||
}
|
||||
|
||||
|
@ -577,8 +729,8 @@ func TestEnvironmentVariableNotPresent(t *testing.T) {
|
|||
var args struct {
|
||||
NotPresent string `arg:"env"`
|
||||
}
|
||||
os.Args = []string{"example"}
|
||||
MustParse(&args)
|
||||
_, err := parseWithEnv(Config{}, "", nil, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", args.NotPresent)
|
||||
}
|
||||
|
||||
|
@ -586,9 +738,8 @@ func TestEnvironmentVariableOverrideName(t *testing.T) {
|
|||
var args struct {
|
||||
Foo string `arg:"env:BAZ"`
|
||||
}
|
||||
setenv(t, "BAZ", "bar")
|
||||
os.Args = []string{"example"}
|
||||
MustParse(&args)
|
||||
_, err := parseWithEnv(Config{}, "", []string{"BAZ=bar"}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "bar", args.Foo)
|
||||
}
|
||||
|
||||
|
@ -596,19 +747,16 @@ func TestEnvironmentVariableOverrideArgument(t *testing.T) {
|
|||
var args struct {
|
||||
Foo string `arg:"env"`
|
||||
}
|
||||
setenv(t, "FOO", "bar")
|
||||
os.Args = []string{"example", "--foo", "baz"}
|
||||
MustParse(&args)
|
||||
assert.Equal(t, "baz", args.Foo)
|
||||
_, err := parseWithEnv(Config{}, "--foo zzz", []string{"FOO=bar"}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "zzz", args.Foo)
|
||||
}
|
||||
|
||||
func TestEnvironmentVariableError(t *testing.T) {
|
||||
var args struct {
|
||||
Foo int `arg:"env"`
|
||||
}
|
||||
setenv(t, "FOO", "bar")
|
||||
os.Args = []string{"example"}
|
||||
err := Parse(&args)
|
||||
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
|
@ -616,9 +764,8 @@ func TestEnvironmentVariableRequired(t *testing.T) {
|
|||
var args struct {
|
||||
Foo string `arg:"env,required"`
|
||||
}
|
||||
setenv(t, "FOO", "bar")
|
||||
os.Args = []string{"example"}
|
||||
MustParse(&args)
|
||||
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "bar", args.Foo)
|
||||
}
|
||||
|
||||
|
@ -626,17 +773,26 @@ func TestEnvironmentVariableSliceArgumentString(t *testing.T) {
|
|||
var args struct {
|
||||
Foo []string `arg:"env"`
|
||||
}
|
||||
setenv(t, "FOO", `bar,"baz, qux"`)
|
||||
MustParse(&args)
|
||||
_, err := parseWithEnv(Config{}, "", []string{`FOO=bar,"baz, qux"`}, &args)
|
||||
require.NoError(t, err)
|
||||
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) {
|
||||
var args struct {
|
||||
Foo []int `arg:"env"`
|
||||
}
|
||||
setenv(t, "FOO", "1,99")
|
||||
MustParse(&args)
|
||||
_, err := parseWithEnv(Config{}, "", []string{`FOO=1,99`}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 99}, args.Foo)
|
||||
}
|
||||
|
||||
|
@ -644,8 +800,8 @@ func TestEnvironmentVariableSliceArgumentFloat(t *testing.T) {
|
|||
var args struct {
|
||||
Foo []float32 `arg:"env"`
|
||||
}
|
||||
setenv(t, "FOO", "1.1,99.9")
|
||||
MustParse(&args)
|
||||
_, err := parseWithEnv(Config{}, "", []string{`FOO=1.1,99.9`}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []float32{1.1, 99.9}, args.Foo)
|
||||
}
|
||||
|
||||
|
@ -653,8 +809,8 @@ func TestEnvironmentVariableSliceArgumentBool(t *testing.T) {
|
|||
var args struct {
|
||||
Foo []bool `arg:"env"`
|
||||
}
|
||||
setenv(t, "FOO", "true,false,0,1")
|
||||
MustParse(&args)
|
||||
_, err := parseWithEnv(Config{}, "", []string{`FOO=true,false,0,1`}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []bool{true, false, false, true}, args.Foo)
|
||||
}
|
||||
|
||||
|
@ -662,8 +818,7 @@ func TestEnvironmentVariableSliceArgumentWrongCsv(t *testing.T) {
|
|||
var args struct {
|
||||
Foo []int `arg:"env"`
|
||||
}
|
||||
setenv(t, "FOO", "1,99\"")
|
||||
err := Parse(&args)
|
||||
_, err := parseWithEnv(Config{}, "", []string{`FOO=1,99\"`}, &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
|
@ -671,11 +826,40 @@ func TestEnvironmentVariableSliceArgumentWrongType(t *testing.T) {
|
|||
var args struct {
|
||||
Foo []bool `arg:"env"`
|
||||
}
|
||||
setenv(t, "FOO", "one,two")
|
||||
err := Parse(&args)
|
||||
_, err := parseWithEnv(Config{}, "", []string{`FOO=one,two`}, &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestEnvironmentVariableMap(t *testing.T) {
|
||||
var args struct {
|
||||
Foo map[int]string `arg:"env"`
|
||||
}
|
||||
_, err := parseWithEnv(Config{}, "", []string{`FOO=1=one,99=ninetynine`}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, args.Foo, 2)
|
||||
assert.Equal(t, "one", args.Foo[1])
|
||||
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) {
|
||||
var args struct {
|
||||
Foo string `arg:"env"`
|
||||
|
@ -690,6 +874,37 @@ func TestEnvironmentVariableIgnored(t *testing.T) {
|
|||
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) {
|
||||
var args struct {
|
||||
Sub *struct {
|
||||
|
@ -702,10 +917,51 @@ func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
err = p.Parse([]string{"sub"})
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.Sub)
|
||||
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 {
|
||||
val int
|
||||
}
|
||||
|
@ -844,6 +1100,24 @@ func TestPtrToIP(t *testing.T) {
|
|||
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) {
|
||||
var args struct {
|
||||
Host []net.IP
|
||||
|
@ -978,6 +1252,29 @@ func TestEmbeddedWithDuplicateField2(t *testing.T) {
|
|||
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) {
|
||||
origArgs := os.Args
|
||||
|
||||
|
@ -1118,11 +1415,55 @@ func TestReuseParser(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
func TestNoVersion(t *testing.T) {
|
||||
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) {
|
||||
|
@ -1153,13 +1494,21 @@ func TestDefaultOptionValues(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
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, "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, 1.23, *args.F)
|
||||
assert.True(t, args.G)
|
||||
if assert.NotNil(t, args.F) {
|
||||
assert.Equal(t, 1.23, *args.F)
|
||||
}
|
||||
assert.True(t, args.G)
|
||||
if assert.NotNil(t, args.H) {
|
||||
assert.True(t, *args.H)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultUnparseable(t *testing.T) {
|
||||
|
@ -1168,7 +1517,7 @@ func TestDefaultUnparseable(t *testing.T) {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
@ -1187,13 +1536,21 @@ func TestDefaultPositionalValues(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
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.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.F)
|
||||
assert.True(t, args.G)
|
||||
if assert.NotNil(t, args.F) {
|
||||
assert.Equal(t, 1.23, *args.F)
|
||||
}
|
||||
assert.True(t, args.G)
|
||||
if assert.NotNil(t, args.H) {
|
||||
assert.True(t, *args.H)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultValuesNotAllowedWithRequired(t *testing.T) {
|
||||
|
@ -1207,11 +1564,11 @@ func TestDefaultValuesNotAllowedWithRequired(t *testing.T) {
|
|||
|
||||
func TestDefaultValuesNotAllowedWithSlice(t *testing.T) {
|
||||
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)
|
||||
assert.EqualError(t, err, ".A: default values are not supported for slice fields")
|
||||
assert.EqualError(t, err, ".A: default values are not supported for slice or map fields")
|
||||
}
|
||||
|
||||
func TestUnexportedFieldsSkipped(t *testing.T) {
|
||||
|
@ -1222,3 +1579,203 @@ func TestUnexportedFieldsSkipped(t *testing.T) {
|
|||
_, err := NewParser(Config{}, &args)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMustParseInvalidParser(t *testing.T) {
|
||||
var exitCode int
|
||||
var stdout bytes.Buffer
|
||||
exit := func(code int) { exitCode = code }
|
||||
|
||||
var args struct {
|
||||
CannotParse struct{}
|
||||
}
|
||||
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
|
||||
assert.Nil(t, parser)
|
||||
assert.Equal(t, 2, exitCode)
|
||||
}
|
||||
|
||||
func TestMustParsePrintsHelp(t *testing.T) {
|
||||
originalArgs := os.Args
|
||||
defer func() {
|
||||
os.Args = originalArgs
|
||||
}()
|
||||
|
||||
os.Args = []string{"someprogram", "--help"}
|
||||
|
||||
var exitCode int
|
||||
var stdout bytes.Buffer
|
||||
exit := func(code int) { exitCode = code }
|
||||
|
||||
var args struct{}
|
||||
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
|
||||
assert.NotNil(t, parser)
|
||||
assert.Equal(t, 0, exitCode)
|
||||
}
|
||||
|
||||
func TestMustParsePrintsVersion(t *testing.T) {
|
||||
originalArgs := os.Args
|
||||
defer func() {
|
||||
os.Args = originalArgs
|
||||
}()
|
||||
|
||||
var exitCode int
|
||||
var stdout bytes.Buffer
|
||||
exit := func(code int) { exitCode = code }
|
||||
|
||||
os.Args = []string{"someprogram", "--version"}
|
||||
|
||||
var args versioned
|
||||
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
|
||||
require.NotNil(t, parser)
|
||||
assert.Equal(t, 0, exitCode)
|
||||
assert.Equal(t, "example 3.2.1\n", stdout.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)
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func assertCardinality(t *testing.T, typ reflect.Type, expected cardinality) {
|
||||
actual, err := cardinalityOf(typ)
|
||||
assert.Equal(t, expected, actual, "expected %v to have cardinality %v but got %v", typ, expected, actual)
|
||||
if expected == unsupported {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCardinalityOf(t *testing.T) {
|
||||
var b bool
|
||||
var i int
|
||||
var s string
|
||||
var f float64
|
||||
var bs []bool
|
||||
var is []int
|
||||
var m map[string]int
|
||||
var unsupported1 struct{}
|
||||
var unsupported2 []struct{}
|
||||
var unsupported3 map[string]struct{}
|
||||
var unsupported4 map[struct{}]string
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(b), zero)
|
||||
assertCardinality(t, reflect.TypeOf(i), one)
|
||||
assertCardinality(t, reflect.TypeOf(s), one)
|
||||
assertCardinality(t, reflect.TypeOf(f), one)
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(&b), zero)
|
||||
assertCardinality(t, reflect.TypeOf(&s), one)
|
||||
assertCardinality(t, reflect.TypeOf(&i), one)
|
||||
assertCardinality(t, reflect.TypeOf(&f), one)
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(bs), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(is), multiple)
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(&bs), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(&is), multiple)
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(m), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(&m), multiple)
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(unsupported1), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(&unsupported1), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(unsupported2), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(&unsupported2), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(unsupported3), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(&unsupported3), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(unsupported4), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(&unsupported4), unsupported)
|
||||
}
|
||||
|
||||
type implementsTextUnmarshaler struct{}
|
||||
|
||||
func (*implementsTextUnmarshaler) UnmarshalText(text []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCardinalityTextUnmarshaler(t *testing.T) {
|
||||
var x implementsTextUnmarshaler
|
||||
var s []implementsTextUnmarshaler
|
||||
var m []implementsTextUnmarshaler
|
||||
assertCardinality(t, reflect.TypeOf(x), one)
|
||||
assertCardinality(t, reflect.TypeOf(&x), one)
|
||||
assertCardinality(t, reflect.TypeOf(s), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(&s), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(m), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(&m), multiple)
|
||||
}
|
||||
|
||||
func TestIsExported(t *testing.T) {
|
||||
assert.True(t, isExported("Exported"))
|
||||
assert.False(t, isExported("notExported"))
|
||||
assert.False(t, isExported(""))
|
||||
assert.False(t, isExported(string([]byte{255})))
|
||||
}
|
||||
|
||||
func TestCardinalityString(t *testing.T) {
|
||||
assert.Equal(t, "zero", zero.String())
|
||||
assert.Equal(t, "one", one.String())
|
||||
assert.Equal(t, "multiple", multiple.String())
|
||||
assert.Equal(t, "unsupported", unsupported.String())
|
||||
assert.Equal(t, "unknown(42)", cardinality(42).String())
|
||||
}
|
||||
|
||||
func TestIsZero(t *testing.T) {
|
||||
var zero int
|
||||
var notZero = 3
|
||||
var nilSlice []int
|
||||
var nonNilSlice = []int{1, 2, 3}
|
||||
var nilMap map[string]string
|
||||
var nonNilMap = map[string]string{"foo": "bar"}
|
||||
var uncomparable = func() {}
|
||||
|
||||
assert.True(t, isZero(reflect.ValueOf(zero)))
|
||||
assert.False(t, isZero(reflect.ValueOf(notZero)))
|
||||
|
||||
assert.True(t, isZero(reflect.ValueOf(nilSlice)))
|
||||
assert.False(t, isZero(reflect.ValueOf(nonNilSlice)))
|
||||
|
||||
assert.True(t, isZero(reflect.ValueOf(nilMap)))
|
||||
assert.False(t, isZero(reflect.ValueOf(nonNilMap)))
|
||||
|
||||
assert.False(t, isZero(reflect.ValueOf(uncomparable)))
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSetSliceWithoutClearing(t *testing.T) {
|
||||
xs := []int{10}
|
||||
entries := []string{"1", "2", "3"}
|
||||
err := setSlice(reflect.ValueOf(&xs).Elem(), entries, false)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{10, 1, 2, 3}, xs)
|
||||
}
|
||||
|
||||
func TestSetSliceAfterClearing(t *testing.T) {
|
||||
xs := []int{100}
|
||||
entries := []string{"1", "2", "3"}
|
||||
err := setSlice(reflect.ValueOf(&xs).Elem(), entries, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 2, 3}, xs)
|
||||
}
|
||||
|
||||
func TestSetSliceInvalid(t *testing.T) {
|
||||
xs := []int{100}
|
||||
entries := []string{"invalid"}
|
||||
err := setSlice(reflect.ValueOf(&xs).Elem(), entries, true)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetSlicePtr(t *testing.T) {
|
||||
var xs []*int
|
||||
entries := []string{"1", "2", "3"}
|
||||
err := setSlice(reflect.ValueOf(&xs).Elem(), entries, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, xs, 3)
|
||||
assert.Equal(t, 1, *xs[0])
|
||||
assert.Equal(t, 2, *xs[1])
|
||||
assert.Equal(t, 3, *xs[2])
|
||||
}
|
||||
|
||||
func TestSetSliceTextUnmarshaller(t *testing.T) {
|
||||
// textUnmarshaler is a struct that captures the length of the string passed to it
|
||||
var xs []*textUnmarshaler
|
||||
entries := []string{"a", "aa", "aaa"}
|
||||
err := setSlice(reflect.ValueOf(&xs).Elem(), entries, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, xs, 3)
|
||||
assert.Equal(t, 1, xs[0].val)
|
||||
assert.Equal(t, 2, xs[1].val)
|
||||
assert.Equal(t, 3, xs[2].val)
|
||||
}
|
||||
|
||||
func TestSetMapWithoutClearing(t *testing.T) {
|
||||
m := map[string]int{"foo": 10}
|
||||
entries := []string{"a=1", "b=2"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m, 3)
|
||||
assert.Equal(t, 1, m["a"])
|
||||
assert.Equal(t, 2, m["b"])
|
||||
assert.Equal(t, 10, m["foo"])
|
||||
}
|
||||
|
||||
func TestSetMapAfterClearing(t *testing.T) {
|
||||
m := map[string]int{"foo": 10}
|
||||
entries := []string{"a=1", "b=2"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m, 2)
|
||||
assert.Equal(t, 1, m["a"])
|
||||
assert.Equal(t, 2, m["b"])
|
||||
}
|
||||
|
||||
func TestSetMapWithKeyPointer(t *testing.T) {
|
||||
// textUnmarshaler is a struct that captures the length of the string passed to it
|
||||
var m map[*string]int
|
||||
entries := []string{"abc=123"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m, 1)
|
||||
}
|
||||
|
||||
func TestSetMapWithValuePointer(t *testing.T) {
|
||||
// textUnmarshaler is a struct that captures the length of the string passed to it
|
||||
var m map[string]*int
|
||||
entries := []string{"abc=123"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m, 1)
|
||||
assert.Equal(t, 123, *m["abc"])
|
||||
}
|
||||
|
||||
func TestSetMapTextUnmarshaller(t *testing.T) {
|
||||
// textUnmarshaler is a struct that captures the length of the string passed to it
|
||||
var m map[textUnmarshaler]*textUnmarshaler
|
||||
entries := []string{"a=123", "aa=12", "aaa=1"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m, 3)
|
||||
assert.Equal(t, &textUnmarshaler{3}, m[textUnmarshaler{1}])
|
||||
assert.Equal(t, &textUnmarshaler{2}, m[textUnmarshaler{2}])
|
||||
assert.Equal(t, &textUnmarshaler{1}, m[textUnmarshaler{3}])
|
||||
}
|
||||
|
||||
func TestSetMapInvalidKey(t *testing.T) {
|
||||
var m map[int]int
|
||||
entries := []string{"invalid=123"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetMapInvalidValue(t *testing.T) {
|
||||
var m map[int]int
|
||||
entries := []string{"123=invalid"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetMapMalformed(t *testing.T) {
|
||||
// textUnmarshaler is a struct that captures the length of the string passed to it
|
||||
var m map[string]string
|
||||
entries := []string{"missing_equals_sign"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetSliceOrMapErrors(t *testing.T) {
|
||||
var err error
|
||||
var dest reflect.Value
|
||||
|
||||
// converting a slice to a reflect.Value in this way will make it read only
|
||||
var cannotSet []int
|
||||
dest = reflect.ValueOf(cannotSet)
|
||||
err = setSliceOrMap(dest, nil, false)
|
||||
assert.Error(t, err)
|
||||
|
||||
// check what happens when we pass in something that is not a slice or a map
|
||||
var notSliceOrMap string
|
||||
dest = reflect.ValueOf(¬SliceOrMap).Elem()
|
||||
err = setSliceOrMap(dest, nil, false)
|
||||
assert.Error(t, err)
|
||||
|
||||
// check what happens when we pass in a pointer to something that is not a slice or a map
|
||||
var stringPtr *string
|
||||
dest = reflect.ValueOf(&stringPtr).Elem()
|
||||
err = setSliceOrMap(dest, nil, false)
|
||||
assert.Error(t, err)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -48,6 +49,17 @@ func TestMinimalSubcommand(t *testing.T) {
|
|||
assert.Equal(t, []string{"list"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
func TestSubcommandNamesBeforeParsing(t *testing.T) {
|
||||
type listCmd struct{}
|
||||
var args struct {
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
p, err := NewParser(Config{}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, p.Subcommand())
|
||||
assert.Nil(t, p.SubcommandNames())
|
||||
}
|
||||
|
||||
func TestNoSuchSubcommand(t *testing.T) {
|
||||
type listCmd struct {
|
||||
}
|
||||
|
@ -71,6 +83,19 @@ func TestNamedSubcommand(t *testing.T) {
|
|||
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) {
|
||||
type listCmd struct {
|
||||
}
|
||||
|
@ -101,6 +126,23 @@ func TestTwoSubcommands(t *testing.T) {
|
|||
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) {
|
||||
type getCmd struct {
|
||||
Name string
|
||||
|
@ -179,6 +221,36 @@ func TestSubcommandsWithOptions(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSubcommandsWithEnvVars(t *testing.T) {
|
||||
type getCmd struct {
|
||||
Name string `arg:"env"`
|
||||
}
|
||||
type listCmd struct {
|
||||
Limit int `arg:"env"`
|
||||
}
|
||||
type cmd struct {
|
||||
Verbose bool
|
||||
Get *getCmd `arg:"subcommand"`
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
setenv(t, "LIMIT", "123")
|
||||
err := parse("list", &args)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.List)
|
||||
assert.Equal(t, 123, args.List.Limit)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
setenv(t, "LIMIT", "not_an_integer")
|
||||
err := parse("list", &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedSubcommands(t *testing.T) {
|
||||
type child struct{}
|
||||
type parent struct {
|
||||
|
@ -233,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) {
|
||||
type listCmd struct {
|
||||
Pattern string `arg:"positional"`
|
||||
|
@ -353,3 +479,30 @@ func TestSubcommandsWithMultiplePositionals(t *testing.T) {
|
|||
assert.Equal(t, 5, args.Limit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValForNilStruct(t *testing.T) {
|
||||
type subcmd struct{}
|
||||
var cmd struct {
|
||||
Sub *subcmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
p, err := NewParser(Config{}, &cmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
typ := reflect.TypeOf(cmd)
|
||||
subField, _ := typ.FieldByName("Sub")
|
||||
|
||||
v := p.val(path{fields: []reflect.StructField{subField, subField}})
|
||||
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)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
325
usage.go
325
usage.go
|
@ -3,68 +3,82 @@ package arg
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// the width of the left column
|
||||
const colWidth = 25
|
||||
|
||||
// to allow monkey patching in tests
|
||||
var stderr = os.Stderr
|
||||
|
||||
// Fail prints usage information to stderr and exits with non-zero status
|
||||
// Fail prints usage information to p.Config.Out and exits with status code 2.
|
||||
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
|
||||
func (p *Parser) failWithCommand(msg string, cmd *command) {
|
||||
p.writeUsageForCommand(stderr, cmd)
|
||||
fmt.Fprintln(stderr, "error:", msg)
|
||||
osExit(-1)
|
||||
// FailSubcommand prints usage information for a specified subcommand to p.Config.Out,
|
||||
// then exits with status code 2. 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) 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
|
||||
func (p *Parser) WriteUsage(w io.Writer) {
|
||||
cmd := p.cmd
|
||||
if p.lastCmd != nil {
|
||||
cmd = p.lastCmd
|
||||
}
|
||||
p.writeUsageForCommand(w, cmd)
|
||||
p.WriteUsageForSubcommand(w, p.subcommand...)
|
||||
}
|
||||
|
||||
// writeUsageForCommand writes usage information for the given subcommand
|
||||
func (p *Parser) writeUsageForCommand(w io.Writer, cmd *command) {
|
||||
var positionals, options []*spec
|
||||
// WriteUsageForSubcommand writes the usage information for a specified
|
||||
// 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
|
||||
for _, spec := range cmd.specs {
|
||||
if spec.positional {
|
||||
switch {
|
||||
case spec.positional:
|
||||
positionals = append(positionals, spec)
|
||||
} else {
|
||||
options = append(options, spec)
|
||||
case spec.long != "":
|
||||
longOptions = append(longOptions, spec)
|
||||
case spec.short != "":
|
||||
shortOptions = append(shortOptions, spec)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
fmt.Fprint(w, "Usage:")
|
||||
for i := len(ancestors) - 1; i >= 0; i-- {
|
||||
fmt.Fprint(w, " "+ancestors[i])
|
||||
fmt.Fprintf(w, "Usage: %s", p.cmd.name)
|
||||
for _, s := range subcommand {
|
||||
fmt.Fprint(w, " "+s)
|
||||
}
|
||||
|
||||
// write the option component of the usage message
|
||||
for _, spec := range options {
|
||||
for _, spec := range shortOptions {
|
||||
// prefix with a space
|
||||
fmt.Fprint(w, " ")
|
||||
if !spec.required {
|
||||
fmt.Fprint(w, "[")
|
||||
}
|
||||
fmt.Fprint(w, synopsis(spec, "-"+spec.short))
|
||||
if !spec.required {
|
||||
fmt.Fprint(w, "]")
|
||||
}
|
||||
}
|
||||
|
||||
for _, spec := range longOptions {
|
||||
// prefix with a space
|
||||
fmt.Fprint(w, " ")
|
||||
if !spec.required {
|
||||
|
@ -76,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 {
|
||||
// prefix with a space
|
||||
fmt.Fprint(w, " ")
|
||||
if spec.multiple {
|
||||
if !spec.required {
|
||||
fmt.Fprint(w, "[")
|
||||
}
|
||||
if !spec.required {
|
||||
fmt.Fprint(w, "[")
|
||||
closeBrackets += 1
|
||||
}
|
||||
if spec.cardinality == multiple {
|
||||
fmt.Fprintf(w, "%s [%s ...]", spec.placeholder, spec.placeholder)
|
||||
if !spec.required {
|
||||
fmt.Fprint(w, "]")
|
||||
}
|
||||
} else {
|
||||
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 len(cmd.subcommands) > 0 {
|
||||
|
@ -99,87 +123,139 @@ func (p *Parser) writeUsageForCommand(w io.Writer, cmd *command) {
|
|||
}
|
||||
|
||||
fmt.Fprint(w, "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func printTwoCols(w io.Writer, left, help string, defaultVal string, envVal string) {
|
||||
lhs := " " + left
|
||||
// print prints a line like this:
|
||||
//
|
||||
// --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)
|
||||
if help != "" {
|
||||
if description != "" {
|
||||
if len(lhs)+2 < colWidth {
|
||||
fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs)))
|
||||
} else {
|
||||
fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
|
||||
}
|
||||
fmt.Fprint(w, help)
|
||||
fmt.Fprint(w, description)
|
||||
}
|
||||
|
||||
bracketsContent := []string{}
|
||||
|
||||
if defaultVal != "" {
|
||||
bracketsContent = append(bracketsContent,
|
||||
fmt.Sprintf("default: %s", defaultVal),
|
||||
)
|
||||
var brack string
|
||||
for _, s := range bracketed {
|
||||
if s != "" {
|
||||
if brack != "" {
|
||||
brack += ", "
|
||||
}
|
||||
brack += s
|
||||
}
|
||||
}
|
||||
|
||||
if envVal != "" {
|
||||
bracketsContent = append(bracketsContent,
|
||||
fmt.Sprintf("env: %s", envVal),
|
||||
)
|
||||
}
|
||||
|
||||
if len(bracketsContent) > 0 {
|
||||
fmt.Fprintf(w, " [%s]", strings.Join(bracketsContent, ", "))
|
||||
if brack != "" {
|
||||
fmt.Fprintf(w, " [%s]", brack)
|
||||
}
|
||||
fmt.Fprint(w, "\n")
|
||||
}
|
||||
|
||||
// WriteHelp writes the usage string followed by the full help string for each option
|
||||
func (p *Parser) WriteHelp(w io.Writer) {
|
||||
cmd := p.cmd
|
||||
if p.lastCmd != nil {
|
||||
cmd = p.lastCmd
|
||||
func withDefault(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
p.writeHelpForCommand(w, cmd)
|
||||
return "default: " + s
|
||||
}
|
||||
|
||||
// writeHelp writes the usage string for the given subcommand
|
||||
func (p *Parser) writeHelpForCommand(w io.Writer, cmd *command) {
|
||||
var positionals, options []*spec
|
||||
func withEnv(env string) string {
|
||||
if env == "" {
|
||||
return ""
|
||||
}
|
||||
return "env: " + env
|
||||
}
|
||||
|
||||
// WriteHelp writes the usage string followed by the full help string for each option
|
||||
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 {
|
||||
if spec.positional {
|
||||
switch {
|
||||
case spec.positional:
|
||||
positionals = append(positionals, spec)
|
||||
} else {
|
||||
options = append(options, spec)
|
||||
case spec.long != "":
|
||||
longOptions = append(longOptions, spec)
|
||||
if spec.long == "version" {
|
||||
hasVersionOption = true
|
||||
}
|
||||
case spec.short != "":
|
||||
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 != "" {
|
||||
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
|
||||
if len(positionals) > 0 {
|
||||
fmt.Fprint(w, "\nPositional arguments:\n")
|
||||
for _, spec := range positionals {
|
||||
printTwoCols(w, spec.placeholder, spec.help, "", "")
|
||||
print(w, spec.placeholder, spec.help, withDefault(spec.defaultString), withEnv(spec.env))
|
||||
}
|
||||
}
|
||||
|
||||
// write the list of options
|
||||
if len(options) > 0 || cmd.parent == nil {
|
||||
// write the list of options with the short-only ones first to match the usage string
|
||||
if len(shortOptions)+len(longOptions) > 0 || cmd.parent == nil {
|
||||
fmt.Fprint(w, "\nOptions:\n")
|
||||
for _, spec := range options {
|
||||
for _, spec := range shortOptions {
|
||||
p.printOption(w, spec)
|
||||
}
|
||||
for _, spec := range longOptions {
|
||||
p.printOption(w, spec)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -192,43 +268,76 @@ func (p *Parser) writeHelpForCommand(w io.Writer, cmd *command) {
|
|||
|
||||
// write the list of built in options
|
||||
p.printOption(w, &spec{
|
||||
boolean: true,
|
||||
long: "help",
|
||||
short: "h",
|
||||
help: "display this help and exit",
|
||||
cardinality: zero,
|
||||
long: "help",
|
||||
short: "h",
|
||||
help: "display this help and exit",
|
||||
})
|
||||
if p.version != "" {
|
||||
if !hasVersionOption && p.version != "" {
|
||||
p.printOption(w, &spec{
|
||||
boolean: true,
|
||||
long: "version",
|
||||
help: "display version and exit",
|
||||
cardinality: zero,
|
||||
long: "version",
|
||||
help: "display version and exit",
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
if len(cmd.subcommands) > 0 {
|
||||
fmt.Fprint(w, "\nCommands:\n")
|
||||
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) {
|
||||
left := synopsis(spec, "--"+spec.long)
|
||||
if spec.short != "" {
|
||||
left += ", " + synopsis(spec, "-"+spec.short)
|
||||
ways := make([]string, 0, 2)
|
||||
if spec.long != "" {
|
||||
ways = append(ways, synopsis(spec, "--"+spec.long))
|
||||
}
|
||||
printTwoCols(w, left, spec.help, spec.defaultVal, spec.env)
|
||||
if spec.short != "" {
|
||||
ways = append(ways, synopsis(spec, "-"+spec.short))
|
||||
}
|
||||
if len(ways) > 0 {
|
||||
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 {
|
||||
if spec.boolean {
|
||||
// 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 + " " + spec.placeholder
|
||||
}
|
||||
|
||||
func ptrTo(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
|
311
usage_test.go
311
usage_test.go
|
@ -1,311 +0,0 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type NameDotName struct {
|
||||
Head, Tail string
|
||||
}
|
||||
|
||||
func (n *NameDotName) UnmarshalText(b []byte) error {
|
||||
s := string(b)
|
||||
pos := strings.Index(s, ".")
|
||||
if pos == -1 {
|
||||
return fmt.Errorf("missing period in %s", s)
|
||||
}
|
||||
n.Head = s[:pos]
|
||||
n.Tail = s[pos+1:]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NameDotName) MarshalText() (text []byte, err error) {
|
||||
text = []byte(fmt.Sprintf("%s.%s", n.Head, n.Tail))
|
||||
return
|
||||
}
|
||||
|
||||
func TestWriteUsage(t *testing.T) {
|
||||
expectedUsage := "Usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] [--testenv TESTENV] [--file FILE] INPUT [OUTPUT [OUTPUT ...]]\n"
|
||||
|
||||
expectedHelp := `Usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] [--testenv TESTENV] [--file FILE] INPUT [OUTPUT [OUTPUT ...]]
|
||||
|
||||
Positional arguments:
|
||||
INPUT
|
||||
OUTPUT list of outputs
|
||||
|
||||
Options:
|
||||
--name NAME name to use [default: Foo Bar]
|
||||
--value VALUE secret value [default: 42]
|
||||
--verbose, -v verbosity level
|
||||
--dataset DATASET dataset to use
|
||||
--optimize OPTIMIZE, -O OPTIMIZE
|
||||
optimization level
|
||||
--ids IDS Ids
|
||||
--values VALUES Values [default: [3.14 42 256]]
|
||||
--workers WORKERS, -w WORKERS
|
||||
number of workers to start [default: 10, env: WORKERS]
|
||||
--testenv TESTENV, -a TESTENV [env: TEST_ENV]
|
||||
--file FILE, -f FILE File with mandatory extension [default: scratch.txt]
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
Input string `arg:"positional"`
|
||||
Output []string `arg:"positional" help:"list of outputs"`
|
||||
Name string `help:"name to use"`
|
||||
Value int `help:"secret value"`
|
||||
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||
Dataset string `help:"dataset to use"`
|
||||
Optimize int `arg:"-O" help:"optimization level"`
|
||||
Ids []int64 `help:"Ids"`
|
||||
Values []float64 `help:"Values"`
|
||||
Workers int `arg:"-w,env:WORKERS" help:"number of workers to start" default:"10"`
|
||||
TestEnv string `arg:"-a,env:TEST_ENV"`
|
||||
File *NameDotName `arg:"-f" help:"File with mandatory extension"`
|
||||
}
|
||||
args.Name = "Foo Bar"
|
||||
args.Value = 42
|
||||
args.Values = []float64{3.14, 42, 256}
|
||||
args.File = &NameDotName{"scratch", "txt"}
|
||||
p, err := NewParser(Config{Program: "example"}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
os.Args[0] = "example"
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, usage.String())
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp, help.String())
|
||||
}
|
||||
|
||||
type MyEnum int
|
||||
|
||||
func (n *MyEnum) UnmarshalText(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *MyEnum) MarshalText() ([]byte, error) {
|
||||
return nil, errors.New("There was a problem")
|
||||
}
|
||||
|
||||
func TestUsageWithDefaults(t *testing.T) {
|
||||
expectedHelp := `Usage: example [--label LABEL] [--content CONTENT]
|
||||
|
||||
Options:
|
||||
--label LABEL [default: cat]
|
||||
--content CONTENT [default: dog]
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
Label string
|
||||
Content string `default:"dog"`
|
||||
}
|
||||
args.Label = "cat"
|
||||
p, err := NewParser(Config{Program: "example"}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
args.Label = "should_ignore_this"
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp, help.String())
|
||||
}
|
||||
|
||||
func TestUsageCannotMarshalToString(t *testing.T) {
|
||||
var args struct {
|
||||
Name *MyEnum
|
||||
}
|
||||
v := MyEnum(42)
|
||||
args.Name = &v
|
||||
_, err := NewParser(Config{Program: "example"}, &args)
|
||||
assert.EqualError(t, err, `args.Name: error marshaling default value to string: There was a problem`)
|
||||
}
|
||||
|
||||
func TestUsageLongPositionalWithHelp_legacyForm(t *testing.T) {
|
||||
expectedHelp := `Usage: example VERYLONGPOSITIONALWITHHELP
|
||||
|
||||
Positional arguments:
|
||||
VERYLONGPOSITIONALWITHHELP
|
||||
this positional argument is very long but cannot include commas
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
VeryLongPositionalWithHelp string `arg:"positional,help:this positional argument is very long but cannot include commas"`
|
||||
}
|
||||
|
||||
p, err := NewParser(Config{}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
os.Args[0] = "example"
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp, help.String())
|
||||
}
|
||||
|
||||
func TestUsageLongPositionalWithHelp_newForm(t *testing.T) {
|
||||
expectedHelp := `Usage: example VERYLONGPOSITIONALWITHHELP
|
||||
|
||||
Positional arguments:
|
||||
VERYLONGPOSITIONALWITHHELP
|
||||
this positional argument is very long, and includes: commas, colons etc
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
VeryLongPositionalWithHelp string `arg:"positional" help:"this positional argument is very long, and includes: commas, colons etc"`
|
||||
}
|
||||
|
||||
p, err := NewParser(Config{}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
os.Args[0] = "example"
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp, help.String())
|
||||
}
|
||||
|
||||
func TestUsageWithProgramName(t *testing.T) {
|
||||
expectedHelp := `Usage: myprogram
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
config := Config{
|
||||
Program: "myprogram",
|
||||
}
|
||||
p, err := NewParser(config, &struct{}{})
|
||||
require.NoError(t, err)
|
||||
|
||||
os.Args[0] = "example"
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp, help.String())
|
||||
}
|
||||
|
||||
type versioned struct{}
|
||||
|
||||
// Version returns the version for this program
|
||||
func (versioned) Version() string {
|
||||
return "example 3.2.1"
|
||||
}
|
||||
|
||||
func TestUsageWithVersion(t *testing.T) {
|
||||
expectedHelp := `example 3.2.1
|
||||
Usage: example
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
--version display version and exit
|
||||
`
|
||||
os.Args[0] = "example"
|
||||
p, err := NewParser(Config{}, &versioned{})
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
actual := help.String()
|
||||
if expectedHelp != actual {
|
||||
t.Logf("Expected:\n%s", expectedHelp)
|
||||
t.Logf("Actual:\n%s", actual)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
type described struct{}
|
||||
|
||||
// Described returns the description for this program
|
||||
func (described) Description() string {
|
||||
return "this program does this and that"
|
||||
}
|
||||
|
||||
func TestUsageWithDescription(t *testing.T) {
|
||||
expectedHelp := `this program does this and that
|
||||
Usage: example
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
os.Args[0] = "example"
|
||||
p, err := NewParser(Config{}, &described{})
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
actual := help.String()
|
||||
if expectedHelp != actual {
|
||||
t.Logf("Expected:\n%s", expectedHelp)
|
||||
t.Logf("Actual:\n%s", actual)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiredMultiplePositionals(t *testing.T) {
|
||||
expectedHelp := `Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]
|
||||
|
||||
Positional arguments:
|
||||
REQUIREDMULTIPLE required multiple positional
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
RequiredMultiple []string `arg:"positional,required" help:"required multiple positional"`
|
||||
}
|
||||
|
||||
p, err := NewParser(Config{}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp, help.String())
|
||||
}
|
||||
|
||||
func TestUsageWithNestedSubcommands(t *testing.T) {
|
||||
expectedHelp := `Usage: example child nested [--enable] OUTPUT
|
||||
|
||||
Positional arguments:
|
||||
OUTPUT
|
||||
|
||||
Options:
|
||||
--enable
|
||||
|
||||
Global options:
|
||||
--values VALUES Values
|
||||
--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"`
|
||||
Nested *struct {
|
||||
Enable bool
|
||||
Output string `arg:"positional,required"`
|
||||
} `arg:"subcommand:nested"`
|
||||
} `arg:"subcommand:child"`
|
||||
}
|
||||
|
||||
os.Args[0] = "example"
|
||||
p, err := NewParser(Config{}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
_ = p.Parse([]string{"child", "nested", "value"})
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp, help.String())
|
||||
}
|
Loading…
Reference in New Issue