Compare commits
19 Commits
main
...
prototype-
Author | SHA1 | Date |
---|---|---|
|
c046f49e12 | |
|
f2539d7ad2 | |
|
2ffe24630b | |
|
47ff44303f | |
|
60a0117880 | |
|
55d9025329 | |
|
0769dd5839 | |
|
84b7154efc | |
|
1cc263f9f2 | |
|
b365ec0781 | |
|
64288c5521 | |
|
2775f58376 | |
|
5f0c48f092 | |
|
5ca19cd72d | |
|
4aea783023 | |
|
a1e2b672ea | |
|
22f214d7ed | |
|
09d28e1195 | |
|
2e6284635a |
|
@ -1 +0,0 @@
|
|||
github: [alexflint]
|
|
@ -15,17 +15,17 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go: ['1.20', '1.21', '1.22']
|
||||
go: ['1.17', '1.18', '1.19']
|
||||
|
||||
steps:
|
||||
- id: go
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build
|
||||
run: go build -v .
|
||||
|
|
|
@ -22,5 +22,4 @@ _testmain.go
|
|||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
go.*
|
||||
go.work
|
||||
|
|
11
Makefile
11
Makefile
|
@ -1,11 +0,0 @@
|
|||
all:
|
||||
@echo
|
||||
@echo
|
||||
|
||||
clean:
|
||||
rm -f go.*
|
||||
|
||||
redomod:
|
||||
rm -f go.*
|
||||
GO111MODULE= go mod init
|
||||
GO111MODULE= go mod tidy
|
549
README.md
549
README.md
|
@ -16,6 +16,12 @@
|
|||
|
||||
Declare command line arguments for your program by defining a struct.
|
||||
|
||||
```go
|
||||
import "github.com/go-arg/v2
|
||||
```
|
||||
|
||||
TODO
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Foo string
|
||||
|
@ -33,7 +39,7 @@ hello true
|
|||
### Installation
|
||||
|
||||
```shell
|
||||
go get github.com/alexflint/go-arg
|
||||
go get github.com/alexflint/go-arg/v2
|
||||
```
|
||||
|
||||
### Required arguments
|
||||
|
@ -64,7 +70,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,85 +86,17 @@ arg.MustParse(&args)
|
|||
fmt.Println("Workers:", args.Workers)
|
||||
```
|
||||
|
||||
```shell
|
||||
```
|
||||
$ WORKERS=4 ./example
|
||||
Workers: 4
|
||||
```
|
||||
|
||||
```shell
|
||||
```
|
||||
$ WORKERS=4 ./example --workers=6
|
||||
Workers: 6
|
||||
```
|
||||
|
||||
You can also override the name of the environment variable:
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Workers int `arg:"env:NUM_WORKERS"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
fmt.Println("Workers:", args.Workers)
|
||||
```
|
||||
|
||||
```shell
|
||||
$ NUM_WORKERS=4 ./example
|
||||
Workers: 4
|
||||
```
|
||||
|
||||
You can provide multiple values in environment variables using commas:
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Workers []int `arg:"env"`
|
||||
}
|
||||
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"`
|
||||
|
@ -196,28 +134,19 @@ var args struct {
|
|||
arg.MustParse(&args)
|
||||
```
|
||||
|
||||
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.
|
||||
### Overriding the name of an environment variable
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Test string `arg:"-t,env:TEST" default:"something"`
|
||||
Workers int `arg:"env:NUM_WORKERS"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
fmt.Println("Workers:", args.Workers)
|
||||
```
|
||||
|
||||
#### 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:])
|
||||
```
|
||||
$ NUM_WORKERS=4 ./example
|
||||
Workers: 4
|
||||
```
|
||||
|
||||
### Arguments with multiple values
|
||||
|
@ -236,26 +165,7 @@ fmt.Printf("Fetching the following IDs from %s: %q", args.Database, args.IDs)
|
|||
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
|
||||
./example -c cmd1 db1 -f file1 db2 -c cmd2 -f file2 -f file3 db3 -c cmd3
|
||||
Commands: [cmd1 cmd2 cmd3]
|
||||
Files [file1 file2 file3]
|
||||
Databases [db1 db2 db3]
|
||||
```
|
||||
|
||||
### Arguments with keys and values
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
UserIDs map[string]int
|
||||
|
@ -269,33 +179,7 @@ fmt.Println(args.UserIDs)
|
|||
map[john:123 mary:456]
|
||||
```
|
||||
|
||||
### Version strings
|
||||
|
||||
```go
|
||||
type args struct {
|
||||
...
|
||||
}
|
||||
|
||||
func (args) Version() string {
|
||||
return "someprogram 4.3.0"
|
||||
}
|
||||
|
||||
func main() {
|
||||
var args args
|
||||
arg.MustParse(&args)
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./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
|
||||
|
@ -313,6 +197,28 @@ Usage: samples [--foo FOO] [--bar BAR]
|
|||
error: you must provide either --foo or --bar
|
||||
```
|
||||
|
||||
### Version strings
|
||||
|
||||
```go
|
||||
type args struct {
|
||||
// ...
|
||||
}
|
||||
|
||||
func (args) Version() string {
|
||||
return "someprogram 4.3.0"
|
||||
}
|
||||
|
||||
func main() {
|
||||
var args args
|
||||
arg.MustParse(&args)
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example --version
|
||||
someprogram 4.3.0
|
||||
```
|
||||
|
||||
### Overriding option names
|
||||
|
||||
```go
|
||||
|
@ -337,11 +243,13 @@ Options:
|
|||
--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
|
||||
|
@ -380,7 +288,7 @@ The following types may be used as arguments:
|
|||
- maps using any of the above as keys and values
|
||||
- any type that implements `encoding.TextUnmarshaler`
|
||||
|
||||
### Custom parsing
|
||||
### Custom parsing
|
||||
|
||||
Implement `encoding.TextUnmarshaler` to define your own parsing logic.
|
||||
|
||||
|
@ -409,7 +317,6 @@ func main() {
|
|||
fmt.Printf("%#v\n", args.Name)
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example --name=foo.bar
|
||||
main.NameDotName{Head:"foo", Tail:"bar"}
|
||||
|
@ -419,73 +326,121 @@ Usage: example [--name NAME]
|
|||
error: error processing --name: missing period in "oops"
|
||||
```
|
||||
|
||||
### Custom parsing with default values
|
||||
### Slice-valued environment variables
|
||||
|
||||
Implement `encoding.TextMarshaler` to define your own default value strings:
|
||||
|
||||
```go
|
||||
// Accepts command line arguments of the form "head.tail"
|
||||
type NameDotName struct {
|
||||
Head, Tail string
|
||||
}
|
||||
|
||||
func (n *NameDotName) UnmarshalText(b []byte) error {
|
||||
// same as previous example
|
||||
}
|
||||
|
||||
// this is only needed if you want to display a default value in the usage string
|
||||
func (n *NameDotName) MarshalText() ([]byte, error) {
|
||||
return []byte(fmt.Sprintf("%s.%s", n.Head, n.Tail)), nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
var args struct {
|
||||
Name NameDotName `default:"file.txt"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
fmt.Printf("%#v\n", args.Name)
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example --help
|
||||
Usage: test [--name NAME]
|
||||
|
||||
Options:
|
||||
--name NAME [default: file.txt]
|
||||
--help, -h display this help and exit
|
||||
|
||||
$ ./example
|
||||
main.NameDotName{Head:"file", Tail:"txt"}
|
||||
```
|
||||
|
||||
### Custom placeholders
|
||||
|
||||
Use the `placeholder` tag to control which placeholder text is used in the usage text.
|
||||
You can provide multiple values using the CSV (RFC 4180) format:
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Input string `arg:"positional" 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"`
|
||||
Workers []int `arg:"env"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
fmt.Println("Workers:", args.Workers)
|
||||
```
|
||||
|
||||
```
|
||||
$ WORKERS='1,99' ./example
|
||||
Workers: [1 99]
|
||||
```
|
||||
|
||||
### Parsing command line tokens and environment variables from a slice
|
||||
|
||||
You can override the command line tokens and environment variables processed by go-arg:
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Samsara int
|
||||
Nirvana float64 `arg:"env:NIRVANA"`
|
||||
}
|
||||
p, err := arg.NewParser(&args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cmdline := []string{"./thisprogram", "--samsara=123"}
|
||||
environ := []string{"NIRVANA=45.6"}
|
||||
err = p.Parse(cmdline, environ)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
```
|
||||
./example
|
||||
SAMSARA: 123
|
||||
NIRVANA: 45.6
|
||||
```
|
||||
|
||||
### Configuration files
|
||||
|
||||
TODO
|
||||
|
||||
### Combining command line options, environment variables, and default values
|
||||
|
||||
By default, 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.
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Test string `arg:"-t,env:TEST" default:"something"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
```
|
||||
|
||||
### Changing precedence of command line options, environment variables, and default values
|
||||
|
||||
You can use the low-level functions `Process*` and `OverwriteWith*` to control which things override which other things. Here is an example in which environment variables take precedence over command line options, which is the opposite of the default behavior:
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Test string `arg:"env:TEST"`
|
||||
}
|
||||
|
||||
p, err := arg.NewParser(&args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = p.ParseCommandLine(os.Args)
|
||||
if err != nil {
|
||||
p.Fail(err.Error())
|
||||
}
|
||||
|
||||
err = p.OverwriteWithEnvironment(os.Environ())
|
||||
if err != nil {
|
||||
p.Fail(err.Error())
|
||||
}
|
||||
|
||||
err = p.Validate()
|
||||
if err != nil {
|
||||
p.Fail(err.Error())
|
||||
}
|
||||
|
||||
fmt.Printf("test=%s\n", args.Test)
|
||||
```
|
||||
```
|
||||
TEST=value_from_env ./example --test=value_from_option
|
||||
test=value_from_env
|
||||
```
|
||||
|
||||
### Ignoring environment variables
|
||||
|
||||
TODO
|
||||
|
||||
### Ignoring default values
|
||||
|
||||
TODO
|
||||
|
||||
### Arguments that can be specified multiple times
|
||||
```go
|
||||
var args struct {
|
||||
Commands []string `arg:"-c,separate"`
|
||||
Files []string `arg:"-f,separate"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example -h
|
||||
Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
|
||||
|
||||
Positional arguments:
|
||||
SRC
|
||||
DST
|
||||
|
||||
Options:
|
||||
--optimize LEVEL, -O LEVEL
|
||||
optimization level
|
||||
--maxjobs N, -j N maximum number of simultaneous jobs
|
||||
--help, -h display this help and exit
|
||||
./example -c cmd1 -f file1 -c cmd2 -f file2 -f file3 -c cmd3
|
||||
Commands: [cmd1 cmd2 cmd3]
|
||||
Files [file1 file2 file3]
|
||||
```
|
||||
|
||||
### Description strings
|
||||
|
@ -549,16 +504,14 @@ For more information visit github.com/alexflint/go-arg
|
|||
|
||||
### Subcommands
|
||||
|
||||
Subcommands are commonly used in tools that wish to group multiple functions into a single program. An example is the `git` tool:
|
||||
Subcommands are commonly used in tools that group multiple functions into a single program. An example is the `git` tool:
|
||||
```shell
|
||||
$ git checkout [arguments specific to checking out code]
|
||||
$ git commit [arguments specific to committing]
|
||||
$ git push [arguments specific to pushing]
|
||||
$ git commit [arguments specific to committing code]
|
||||
$ git push [arguments specific to pushing code]
|
||||
```
|
||||
|
||||
The strings "checkout", "commit", and "push" are different from simple positional arguments because the options available to the user change depending on which subcommand they choose.
|
||||
|
||||
This can be implemented with `go-arg` as follows:
|
||||
This can be implemented with `go-arg` with the `arg:"subcommand"` tag:
|
||||
|
||||
```go
|
||||
type CheckoutCmd struct {
|
||||
|
@ -593,14 +546,9 @@ case args.Push != nil:
|
|||
}
|
||||
```
|
||||
|
||||
Some additional rules apply when working with subcommands:
|
||||
* The `subcommand` tag can only be used with fields that are pointers to structs
|
||||
* Any struct that contains a subcommand must not contain any positionals
|
||||
Note that the `subcommand` tag can only be used with fields that are pointers to structs, and that any struct that contains subcommands cannot also contain positionals.
|
||||
|
||||
This package allows to have a program that accepts subcommands, but also does something else
|
||||
when no subcommands are specified.
|
||||
If on the other hand you want the program to terminate when no subcommands are specified,
|
||||
the recommended way is:
|
||||
### Terminating when no subcommands are specified
|
||||
|
||||
```go
|
||||
p := arg.MustParse(&args)
|
||||
|
@ -609,192 +557,39 @@ if p.Subcommand() == nil {
|
|||
}
|
||||
```
|
||||
|
||||
### Custom handling of --help and --version
|
||||
### Customizing placeholder strings
|
||||
|
||||
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.
|
||||
Use the `placeholder` tag to control which placeholder text is used in the usage text.
|
||||
|
||||
```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)
|
||||
Input string `arg:"positional" placeholder:"SRC"`
|
||||
Output []string `arg:"positional" placeholder:"DST"`
|
||||
Optimize int `arg:"-O" placeholder:"LEVEL"`
|
||||
MaxJobs int `arg:"-j" placeholder:"N"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
```
|
||||
|
||||
```shell
|
||||
$ go run ./example --help
|
||||
Usage: ./example --something SOMETHING
|
||||
$ ./example -h
|
||||
Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
|
||||
|
||||
Positional arguments:
|
||||
SRC
|
||||
DST
|
||||
|
||||
Options:
|
||||
--something SOMETHING
|
||||
--optimize LEVEL, -O LEVEL
|
||||
--maxjobs N, -j N
|
||||
--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://pkg.go.dev/github.com/alexflint/go-arg
|
||||
https://godoc.org/github.com/alexflint/go-arg
|
||||
|
||||
### Rationale
|
||||
### Migrating from v1.x
|
||||
|
||||
There are many command line argument parsing libraries for Go, including one in the standard library, so why build another?
|
||||
Migrating IgnoreEnv to passing a nil environ
|
||||
|
||||
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.
|
||||
|
||||
The idea behind `go-arg` is that Go already has an excellent way to describe data structures using structs, so there is no need to develop additional levels of abstraction. Instead of one API to specify which arguments your program accepts, and then another API to get the values of those arguments, `go-arg` replaces both with a single struct.
|
||||
|
||||
### 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 makes it possible to include commas inside help text.
|
||||
Migrating from IgnoreDefault to calling ProcessCommandLine
|
|
@ -162,8 +162,8 @@ func Example_helpText() {
|
|||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
osExit = func(int) {}
|
||||
stdout = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
|
@ -195,18 +195,19 @@ func Example_helpPlaceholder() {
|
|||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
osExit = func(int) {}
|
||||
stdout = 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
|
||||
|
@ -235,8 +236,8 @@ func Example_helpTextWithSubcommand() {
|
|||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
osExit = func(int) {}
|
||||
stdout = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
|
@ -273,8 +274,8 @@ func Example_helpTextWhenUsingSubcommand() {
|
|||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
osExit = func(int) {}
|
||||
stdout = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
|
@ -310,9 +311,10 @@ func Example_writeHelpForSubcommand() {
|
|||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
exit := func(int) {}
|
||||
osExit = func(int) {}
|
||||
stdout = os.Stdout
|
||||
|
||||
p, err := NewParser(Config{Exit: exit}, &args)
|
||||
p, err := NewParser(Config{}, &args)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
|
@ -358,9 +360,10 @@ func Example_writeHelpForSubcommandNested() {
|
|||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
exit := func(int) {}
|
||||
osExit = func(int) {}
|
||||
stdout = os.Stdout
|
||||
|
||||
p, err := NewParser(Config{Exit: exit}, &args)
|
||||
p, err := NewParser(Config{}, &args)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
|
@ -394,8 +397,8 @@ func Example_errorText() {
|
|||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
osExit = func(int) {}
|
||||
stderr = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
|
@ -418,8 +421,8 @@ func Example_errorTextForSubcommand() {
|
|||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
osExit = func(int) {}
|
||||
stderr = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
|
@ -454,8 +457,8 @@ func Example_subcommand() {
|
|||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
osExit = func(int) {}
|
||||
stderr = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
|
@ -502,45 +505,3 @@ func Example_allSupportedTypes() {
|
|||
|
||||
// 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,8 @@
|
|||
module github.com/alexflint/go-arg
|
||||
|
||||
require (
|
||||
github.com/alexflint/go-scalar v1.2.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
)
|
||||
|
||||
go 1.13
|
|
@ -0,0 +1,15 @@
|
|||
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
||||
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -0,0 +1,11 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/alexflint/go-arg/v2"
|
||||
{{if contains .Code "fmt."}}"fmt"{{end}}
|
||||
{{if contains .Code "strings."}}"strings"{{end}}
|
||||
)
|
||||
|
||||
func main() {
|
||||
{{.Code}}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/alexflint/go-arg/v2"
|
||||
{{if contains .Code "fmt."}}"fmt"{{end}}
|
||||
{{if contains .Code "strings."}}"strings"{{end}}
|
||||
)
|
||||
|
||||
{{.Code}}
|
|
@ -0,0 +1,179 @@
|
|||
// mdtest executes code blocks in markdown and checks that they run as expected
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/alexflint/go-arg/v2"
|
||||
)
|
||||
|
||||
// var pattern = "```go(.*)```\\s*```\\s*\\$(.*)\\n(.*)```"
|
||||
var pattern = "(?s)```go([^`]*?)```\\s*```([^`]*?)```" //go(.*)```\\s*```\\s*\\$(.*)\\n(.*)```"
|
||||
|
||||
var re = regexp.MustCompile(pattern)
|
||||
|
||||
var funcs = map[string]any{
|
||||
"contains": strings.Contains,
|
||||
}
|
||||
|
||||
//go:embed example1.go.tpl
|
||||
var templateSource1 string
|
||||
|
||||
//go:embed example2.go.tpl
|
||||
var templateSource2 string
|
||||
|
||||
var t1 = template.Must(template.New("example1.go").Funcs(funcs).Parse(templateSource1))
|
||||
var t2 = template.Must(template.New("example2.go").Funcs(funcs).Parse(templateSource2))
|
||||
|
||||
type payload struct {
|
||||
Code string
|
||||
}
|
||||
|
||||
func runCode(ctx context.Context, code []byte, cmd string) ([]byte, error) {
|
||||
dir, err := os.MkdirTemp("", "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating temp dir to build and run code: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(dir)
|
||||
fmt.Println(strings.Repeat("-", 80))
|
||||
|
||||
srcpath := filepath.Join(dir, "src.go")
|
||||
binpath := filepath.Join(dir, "example")
|
||||
|
||||
// If the code contains a main function then use t2, otherwise use t1
|
||||
t := t1
|
||||
if strings.Contains(string(code), "func main") {
|
||||
t = t2
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
err = t.Execute(&b, payload{Code: string(code)})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error executing template for source file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(b.String())
|
||||
fmt.Println(strings.Repeat("-", 80))
|
||||
|
||||
err = os.WriteFile(srcpath, b.Bytes(), os.ModePerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error writing temporary source file: %w", err)
|
||||
}
|
||||
|
||||
compiler, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not find path to go compiler: %w", err)
|
||||
}
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, compiler, "build", "-o", binpath, srcpath)
|
||||
out, err := buildCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error building source: %w. Compiler said:\n%s", err, string(out))
|
||||
}
|
||||
|
||||
// replace "./example" with full path to compiled program
|
||||
var env, args []string
|
||||
var found bool
|
||||
for _, part := range strings.Split(cmd, " ") {
|
||||
if found {
|
||||
args = append(args, part)
|
||||
} else if part == "./example" {
|
||||
found = true
|
||||
} else {
|
||||
env = append(env, part)
|
||||
}
|
||||
}
|
||||
|
||||
runCmd := exec.CommandContext(ctx, binpath, args...)
|
||||
runCmd.Env = env
|
||||
output, err := runCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error runing example: %w. Program said:\n%s", err, string(output))
|
||||
}
|
||||
|
||||
// Clean up the temp dir
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
return nil, fmt.Errorf("error deleting temp dir: %w", err)
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func Main() error {
|
||||
ctx := context.Background()
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional,required"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
|
||||
buf, err := os.ReadFile(args.Input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(strings.Repeat("=", 80))
|
||||
|
||||
matches := re.FindAllSubmatchIndex(buf, -1)
|
||||
for k, match := range matches {
|
||||
codebegin, codeend := match[2], match[3]
|
||||
code := buf[codebegin:codeend]
|
||||
|
||||
shellbegin, shellend := match[4], match[5]
|
||||
shell := buf[shellbegin:shellend]
|
||||
|
||||
lines := strings.Split(string(shell), "\n")
|
||||
for i := 0; i < len(lines); i++ {
|
||||
if strings.HasPrefix(lines[i], "$") && strings.Contains(lines[i], "./example") {
|
||||
cmd := strings.TrimSpace(strings.TrimPrefix(lines[i], "$"))
|
||||
|
||||
var output []string
|
||||
i++
|
||||
for i < len(lines) && !strings.HasPrefix(lines[i], "$") {
|
||||
output = append(output, lines[i])
|
||||
i++
|
||||
}
|
||||
|
||||
expected := strings.TrimSpace(strings.Join(output, "\n"))
|
||||
|
||||
fmt.Println(string(code))
|
||||
fmt.Println(strings.Repeat("-", 80))
|
||||
fmt.Println(string(cmd))
|
||||
fmt.Println(strings.Repeat("-", 80))
|
||||
fmt.Println(string(expected))
|
||||
fmt.Println(strings.Repeat("-", 80))
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
actual, err := runCode(ctx, code, cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running example %d: %w\nCode was:\n%s", k, err, string(code))
|
||||
}
|
||||
|
||||
fmt.Println(string(actual))
|
||||
fmt.Println(strings.Repeat("=", 80))
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("found %d matches\n", len(matches))
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := Main(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
308
parse.go
308
parse.go
|
@ -5,13 +5,12 @@ import (
|
|||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"go.wit.com/dev/alexflint/scalar"
|
||||
scalar "github.com/alexflint/go-scalar"
|
||||
)
|
||||
|
||||
// path represents a sequence of steps to find the output location for an
|
||||
|
@ -44,25 +43,23 @@ func (p path) Child(f reflect.StructField) path {
|
|||
|
||||
// spec represents a command line option
|
||||
type spec struct {
|
||||
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
|
||||
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
|
||||
defaultVal string // default value for this option
|
||||
placeholder string // name of the data 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
|
||||
|
@ -70,49 +67,33 @@ type command struct {
|
|||
parent *command
|
||||
}
|
||||
|
||||
// ErrHelp indicates that the builtin -h or --help were provided
|
||||
// ErrHelp indicates that -h or --help were provided
|
||||
var ErrHelp = errors.New("help requested by user")
|
||||
|
||||
// ErrVersion indicates that the builtin --version was provided
|
||||
// ErrVersion indicates that --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 {
|
||||
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...)
|
||||
p, err := NewParser(Config{}, dest...)
|
||||
if err != nil {
|
||||
fmt.Fprintln(config.Out, err)
|
||||
config.Exit(2)
|
||||
return nil
|
||||
fmt.Fprintln(stdout, err)
|
||||
osExit(-1)
|
||||
return nil // just in case osExit was monkey-patched
|
||||
}
|
||||
|
||||
err = p.Parse(flags())
|
||||
switch {
|
||||
case err == ErrHelp:
|
||||
p.writeHelpForSubcommand(stdout, p.lastCmd)
|
||||
osExit(0)
|
||||
case err == ErrVersion:
|
||||
fmt.Fprintln(stdout, p.version)
|
||||
osExit(0)
|
||||
case err != nil:
|
||||
p.failWithSubcommand(err.Error(), p.lastCmd)
|
||||
}
|
||||
|
||||
p.MustParse(flags())
|
||||
return p
|
||||
}
|
||||
|
||||
|
@ -144,19 +125,6 @@ type Config struct {
|
|||
// 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
|
||||
|
@ -169,7 +137,7 @@ type Parser struct {
|
|||
epilogue string
|
||||
|
||||
// the following field changes during processing of command line arguments
|
||||
subcommand []string
|
||||
lastCmd *command
|
||||
}
|
||||
|
||||
// Versioned is the interface that the destination struct should implement to
|
||||
|
@ -219,14 +187,6 @@ 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 {
|
||||
|
@ -256,36 +216,23 @@ 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, config.EnvPrefix)
|
||||
cmd, err := cmdFromStruct(name, path{root: i}, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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)
|
||||
// add nonzero field values as defaults
|
||||
for _, spec := range cmd.specs {
|
||||
// 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)
|
||||
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)
|
||||
}
|
||||
spec.defaultString = string(s)
|
||||
} else {
|
||||
spec.defaultString = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -306,7 +253,7 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
|||
return &p, nil
|
||||
}
|
||||
|
||||
func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*command, error) {
|
||||
func cmdFromStruct(name string, dest path, t reflect.Type) (*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",
|
||||
|
@ -357,8 +304,13 @@ func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*c
|
|||
spec.help = help
|
||||
}
|
||||
|
||||
// process each comma-separated part of the tag
|
||||
var isSubcommand bool
|
||||
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
|
||||
for _, key := range strings.Split(tag, ",") {
|
||||
if key == "" {
|
||||
continue
|
||||
|
@ -376,13 +328,18 @@ func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*c
|
|||
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
|
||||
|
@ -393,30 +350,24 @@ func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*c
|
|||
case key == "env":
|
||||
// Use override name if provided
|
||||
if value != "" {
|
||||
spec.env = envPrefix + value
|
||||
spec.env = value
|
||||
} else {
|
||||
spec.env = envPrefix + strings.ToUpper(field.Name)
|
||||
spec.env = strings.ToUpper(field.Name)
|
||||
}
|
||||
case key == "subcommand":
|
||||
// decide on a name for the subcommand
|
||||
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])
|
||||
cmdname := value
|
||||
if cmdname == "" {
|
||||
cmdname = strings.ToLower(field.Name)
|
||||
}
|
||||
|
||||
// parse the subcommand recursively
|
||||
subcmd, err := cmdFromStruct(cmdnames[0], subdest, field.Type, envPrefix)
|
||||
subcmd, err := cmdFromStruct(cmdname, subdest, field.Type)
|
||||
if err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
subcmd.aliases = cmdnames[1:]
|
||||
subcmd.parent = &cmd
|
||||
subcmd.help = field.Tag.Get("help")
|
||||
|
||||
|
@ -428,7 +379,6 @@ func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*c
|
|||
}
|
||||
}
|
||||
|
||||
// placeholder is the string used in the help text like this: "--somearg PLACEHOLDER"
|
||||
placeholder, hasPlaceholder := field.Tag.Lookup("placeholder")
|
||||
if hasPlaceholder {
|
||||
spec.placeholder = placeholder
|
||||
|
@ -438,60 +388,27 @@ func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*c
|
|||
spec.placeholder = strings.ToUpper(spec.field.Name)
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
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
|
||||
}
|
||||
if !isSubcommand {
|
||||
cmd.specs = append(cmd.specs, &spec)
|
||||
|
||||
defaultString, hasDefault := field.Tag.Lookup("default")
|
||||
if hasDefault {
|
||||
// we do not support default values for maps and slices
|
||||
if spec.cardinality == multiple {
|
||||
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
|
||||
}
|
||||
if spec.cardinality == multiple && hasDefault {
|
||||
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
|
||||
})
|
||||
|
@ -514,15 +431,8 @@ func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*c
|
|||
return &cmd, nil
|
||||
}
|
||||
|
||||
// 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".
|
||||
// Parse processes the given command line option, storing the results in the field
|
||||
// of the structs from which NewParser was constructed
|
||||
func (p *Parser) Parse(args []string) error {
|
||||
err := p.process(args)
|
||||
if err != nil {
|
||||
|
@ -539,20 +449,6 @@ 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 {
|
||||
|
@ -606,7 +502,7 @@ func (p *Parser) process(args []string) error {
|
|||
|
||||
// union of specs for the chain of subcommands encountered so far
|
||||
curCmd := p.cmd
|
||||
p.subcommand = nil
|
||||
p.lastCmd = curCmd
|
||||
|
||||
// make a copy of the specs because we will add to this list each time we expand a subcommand
|
||||
specs := make([]*spec, len(curCmd.specs))
|
||||
|
@ -620,15 +516,6 @@ func (p *Parser) process(args []string) error {
|
|||
}
|
||||
}
|
||||
|
||||
// determine if the current command has a version option spec
|
||||
var hasVersionOption bool
|
||||
for _, spec := range curCmd.specs {
|
||||
if spec.long == "version" {
|
||||
hasVersionOption = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// process each string from the command line
|
||||
var allpositional bool
|
||||
var positionals []string
|
||||
|
@ -636,7 +523,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 == "--" && !allpositional {
|
||||
if arg == "--" {
|
||||
allpositional = true
|
||||
continue
|
||||
}
|
||||
|
@ -661,12 +548,7 @@ func (p *Parser) process(args []string) error {
|
|||
}
|
||||
|
||||
// add the new options to the set of allowed options
|
||||
if p.config.StrictSubcommands {
|
||||
specs = make([]*spec, len(subcmd.specs))
|
||||
copy(specs, subcmd.specs)
|
||||
} else {
|
||||
specs = append(specs, subcmd.specs...)
|
||||
}
|
||||
specs = append(specs, subcmd.specs...)
|
||||
|
||||
// capture environment vars for these new options
|
||||
if !p.config.IgnoreEnv {
|
||||
|
@ -677,7 +559,7 @@ func (p *Parser) process(args []string) error {
|
|||
}
|
||||
|
||||
curCmd = subcmd
|
||||
p.subcommand = append(p.subcommand, arg)
|
||||
p.lastCmd = curCmd
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -686,9 +568,7 @@ func (p *Parser) process(args []string) error {
|
|||
case "-h", "--help":
|
||||
return ErrHelp
|
||||
case "--version":
|
||||
if !hasVersionOption && p.version != "" {
|
||||
return ErrVersion
|
||||
}
|
||||
return ErrVersion
|
||||
}
|
||||
|
||||
// check for an equals sign, as in "--foo=bar"
|
||||
|
@ -702,7 +582,7 @@ 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 || opt == "" {
|
||||
if spec == nil {
|
||||
return fmt.Errorf("unknown argument %s", arg)
|
||||
}
|
||||
wasPresent[spec] = true
|
||||
|
@ -791,26 +671,17 @@ func (p *Parser) process(args []string) error {
|
|||
}
|
||||
|
||||
if spec.required {
|
||||
if spec.short == "" && spec.long == "" {
|
||||
msg := fmt.Sprintf("environment variable %s is required", spec.env)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("%s is required", name)
|
||||
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)
|
||||
if !p.config.IgnoreDefault && 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -871,11 +742,6 @@ 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
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package arg
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
|
@ -28,11 +27,11 @@ func parse(cmdline string, dest interface{}) error {
|
|||
}
|
||||
|
||||
func pparse(cmdline string, dest interface{}) (*Parser, error) {
|
||||
return parseWithEnv(Config{}, cmdline, nil, dest)
|
||||
return parseWithEnv(cmdline, nil, dest)
|
||||
}
|
||||
|
||||
func parseWithEnv(config Config, cmdline string, env []string, dest interface{}) (*Parser, error) {
|
||||
p, err := NewParser(config, dest)
|
||||
func parseWithEnv(cmdline string, env []string, dest interface{}) (*Parser, error) {
|
||||
p, err := NewParser(Config{}, dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -98,9 +97,9 @@ func TestInt(t *testing.T) {
|
|||
|
||||
func TestHexOctBin(t *testing.T) {
|
||||
var args struct {
|
||||
Hex int
|
||||
Oct int
|
||||
Bin int
|
||||
Hex int
|
||||
Oct int
|
||||
Bin int
|
||||
Underscored int
|
||||
}
|
||||
err := parse("--hex 0xA --oct 0o10 --bin 0b101 --underscored 123_456", &args)
|
||||
|
@ -227,14 +226,6 @@ func TestRequiredWithEnv(t *testing.T) {
|
|||
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"`
|
||||
|
@ -609,15 +600,6 @@ func TestNoMoreOptionsBeforeHelp(t *testing.T) {
|
|||
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
|
||||
|
@ -701,26 +683,11 @@ 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"`
|
||||
}
|
||||
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
|
||||
_, err := parseWithEnv("", []string{"FOO=bar"}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "bar", args.Foo)
|
||||
}
|
||||
|
@ -729,7 +696,7 @@ func TestEnvironmentVariableNotPresent(t *testing.T) {
|
|||
var args struct {
|
||||
NotPresent string `arg:"env"`
|
||||
}
|
||||
_, err := parseWithEnv(Config{}, "", nil, &args)
|
||||
_, err := parseWithEnv("", nil, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", args.NotPresent)
|
||||
}
|
||||
|
@ -738,7 +705,7 @@ func TestEnvironmentVariableOverrideName(t *testing.T) {
|
|||
var args struct {
|
||||
Foo string `arg:"env:BAZ"`
|
||||
}
|
||||
_, err := parseWithEnv(Config{}, "", []string{"BAZ=bar"}, &args)
|
||||
_, err := parseWithEnv("", []string{"BAZ=bar"}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "bar", args.Foo)
|
||||
}
|
||||
|
@ -747,7 +714,7 @@ func TestEnvironmentVariableOverrideArgument(t *testing.T) {
|
|||
var args struct {
|
||||
Foo string `arg:"env"`
|
||||
}
|
||||
_, err := parseWithEnv(Config{}, "--foo zzz", []string{"FOO=bar"}, &args)
|
||||
_, err := parseWithEnv("--foo zzz", []string{"FOO=bar"}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "zzz", args.Foo)
|
||||
}
|
||||
|
@ -756,7 +723,7 @@ func TestEnvironmentVariableError(t *testing.T) {
|
|||
var args struct {
|
||||
Foo int `arg:"env"`
|
||||
}
|
||||
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
|
||||
_, err := parseWithEnv("", []string{"FOO=bar"}, &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
|
@ -764,7 +731,7 @@ func TestEnvironmentVariableRequired(t *testing.T) {
|
|||
var args struct {
|
||||
Foo string `arg:"env,required"`
|
||||
}
|
||||
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
|
||||
_, err := parseWithEnv("", []string{"FOO=bar"}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "bar", args.Foo)
|
||||
}
|
||||
|
@ -773,7 +740,7 @@ func TestEnvironmentVariableSliceArgumentString(t *testing.T) {
|
|||
var args struct {
|
||||
Foo []string `arg:"env"`
|
||||
}
|
||||
_, err := parseWithEnv(Config{}, "", []string{`FOO=bar,"baz, qux"`}, &args)
|
||||
_, err := parseWithEnv("", []string{`FOO=bar,"baz, qux"`}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"bar", "baz, qux"}, args.Foo)
|
||||
}
|
||||
|
@ -782,7 +749,7 @@ func TestEnvironmentVariableSliceEmpty(t *testing.T) {
|
|||
var args struct {
|
||||
Foo []string `arg:"env"`
|
||||
}
|
||||
_, err := parseWithEnv(Config{}, "", []string{`FOO=`}, &args)
|
||||
_, err := parseWithEnv("", []string{`FOO=`}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, args.Foo, 0)
|
||||
}
|
||||
|
@ -791,7 +758,7 @@ func TestEnvironmentVariableSliceArgumentInteger(t *testing.T) {
|
|||
var args struct {
|
||||
Foo []int `arg:"env"`
|
||||
}
|
||||
_, err := parseWithEnv(Config{}, "", []string{`FOO=1,99`}, &args)
|
||||
_, err := parseWithEnv("", []string{`FOO=1,99`}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 99}, args.Foo)
|
||||
}
|
||||
|
@ -800,7 +767,7 @@ func TestEnvironmentVariableSliceArgumentFloat(t *testing.T) {
|
|||
var args struct {
|
||||
Foo []float32 `arg:"env"`
|
||||
}
|
||||
_, err := parseWithEnv(Config{}, "", []string{`FOO=1.1,99.9`}, &args)
|
||||
_, err := parseWithEnv("", []string{`FOO=1.1,99.9`}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []float32{1.1, 99.9}, args.Foo)
|
||||
}
|
||||
|
@ -809,7 +776,7 @@ func TestEnvironmentVariableSliceArgumentBool(t *testing.T) {
|
|||
var args struct {
|
||||
Foo []bool `arg:"env"`
|
||||
}
|
||||
_, err := parseWithEnv(Config{}, "", []string{`FOO=true,false,0,1`}, &args)
|
||||
_, err := parseWithEnv("", []string{`FOO=true,false,0,1`}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []bool{true, false, false, true}, args.Foo)
|
||||
}
|
||||
|
@ -818,7 +785,7 @@ func TestEnvironmentVariableSliceArgumentWrongCsv(t *testing.T) {
|
|||
var args struct {
|
||||
Foo []int `arg:"env"`
|
||||
}
|
||||
_, err := parseWithEnv(Config{}, "", []string{`FOO=1,99\"`}, &args)
|
||||
_, err := parseWithEnv("", []string{`FOO=1,99\"`}, &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
|
@ -826,7 +793,7 @@ func TestEnvironmentVariableSliceArgumentWrongType(t *testing.T) {
|
|||
var args struct {
|
||||
Foo []bool `arg:"env"`
|
||||
}
|
||||
_, err := parseWithEnv(Config{}, "", []string{`FOO=one,two`}, &args)
|
||||
_, err := parseWithEnv("", []string{`FOO=one,two`}, &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
|
@ -834,7 +801,7 @@ func TestEnvironmentVariableMap(t *testing.T) {
|
|||
var args struct {
|
||||
Foo map[int]string `arg:"env"`
|
||||
}
|
||||
_, err := parseWithEnv(Config{}, "", []string{`FOO=1=one,99=ninetynine`}, &args)
|
||||
_, err := parseWithEnv("", []string{`FOO=1=one,99=ninetynine`}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, args.Foo, 2)
|
||||
assert.Equal(t, "one", args.Foo[1])
|
||||
|
@ -845,21 +812,11 @@ func TestEnvironmentVariableEmptyMap(t *testing.T) {
|
|||
var args struct {
|
||||
Foo map[int]string `arg:"env"`
|
||||
}
|
||||
_, err := parseWithEnv(Config{}, "", []string{`FOO=`}, &args)
|
||||
_, err := parseWithEnv("", []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"`
|
||||
|
@ -887,24 +844,6 @@ func TestDefaultValuesIgnored(t *testing.T) {
|
|||
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 {
|
||||
|
@ -917,51 +856,10 @@ func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
err = p.Parse([]string{"sub"})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.Sub)
|
||||
assert.NoError(t, err)
|
||||
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
|
||||
}
|
||||
|
@ -1415,55 +1313,11 @@ func TestReuseParser(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNoVersion(t *testing.T) {
|
||||
func TestVersion(t *testing.T) {
|
||||
var args struct{}
|
||||
|
||||
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"})
|
||||
err := parse("--version", &args)
|
||||
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) {
|
||||
|
@ -1494,21 +1348,13 @@ func TestDefaultOptionValues(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, args.A)
|
||||
if assert.NotNil(t, args.B) {
|
||||
assert.Equal(t, 123, *args.B)
|
||||
}
|
||||
assert.Equal(t, 123, *args.B)
|
||||
assert.Equal(t, "xyz", args.C)
|
||||
if assert.NotNil(t, args.D) {
|
||||
assert.Equal(t, "abc", *args.D)
|
||||
}
|
||||
assert.Equal(t, "abc", *args.D)
|
||||
assert.Equal(t, 4.56, args.E)
|
||||
if assert.NotNil(t, args.F) {
|
||||
assert.Equal(t, 1.23, *args.F)
|
||||
}
|
||||
assert.Equal(t, 1.23, *args.F)
|
||||
assert.True(t, args.G)
|
||||
assert.True(t, args.G)
|
||||
if assert.NotNil(t, args.H) {
|
||||
assert.True(t, *args.H)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultUnparseable(t *testing.T) {
|
||||
|
@ -1517,7 +1363,7 @@ func TestDefaultUnparseable(t *testing.T) {
|
|||
}
|
||||
|
||||
err := parse("", &args)
|
||||
assert.EqualError(t, err, `.A: error processing default value: strconv.ParseInt: parsing "x": invalid syntax`)
|
||||
assert.EqualError(t, err, `error processing default value for --a: strconv.ParseInt: parsing "x": invalid syntax`)
|
||||
}
|
||||
|
||||
func TestDefaultPositionalValues(t *testing.T) {
|
||||
|
@ -1536,21 +1382,13 @@ func TestDefaultPositionalValues(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 456, args.A)
|
||||
if assert.NotNil(t, args.B) {
|
||||
assert.Equal(t, 789, *args.B)
|
||||
}
|
||||
assert.Equal(t, 789, *args.B)
|
||||
assert.Equal(t, "abc", args.C)
|
||||
if assert.NotNil(t, args.D) {
|
||||
assert.Equal(t, "abc", *args.D)
|
||||
}
|
||||
assert.Equal(t, "abc", *args.D)
|
||||
assert.Equal(t, 1.23, args.E)
|
||||
if assert.NotNil(t, args.F) {
|
||||
assert.Equal(t, 1.23, *args.F)
|
||||
}
|
||||
assert.Equal(t, 1.23, *args.F)
|
||||
assert.True(t, args.G)
|
||||
assert.True(t, args.G)
|
||||
if assert.NotNil(t, args.H) {
|
||||
assert.True(t, *args.H)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultValuesNotAllowedWithRequired(t *testing.T) {
|
||||
|
@ -1564,7 +1402,7 @@ func TestDefaultValuesNotAllowedWithRequired(t *testing.T) {
|
|||
|
||||
func TestDefaultValuesNotAllowedWithSlice(t *testing.T) {
|
||||
var args struct {
|
||||
A []int `default:"invalid"` // default values not allowed with slices
|
||||
A []int `default:"123"` // required not allowed with default!
|
||||
}
|
||||
|
||||
err := parse("", &args)
|
||||
|
@ -1581,201 +1419,68 @@ func TestUnexportedFieldsSkipped(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMustParseInvalidParser(t *testing.T) {
|
||||
originalExit := osExit
|
||||
originalStdout := stdout
|
||||
defer func() {
|
||||
osExit = originalExit
|
||||
stdout = originalStdout
|
||||
}()
|
||||
|
||||
var exitCode int
|
||||
var stdout bytes.Buffer
|
||||
exit := func(code int) { exitCode = code }
|
||||
osExit = func(code int) { exitCode = code }
|
||||
stdout = &bytes.Buffer{}
|
||||
|
||||
var args struct {
|
||||
CannotParse struct{}
|
||||
}
|
||||
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
|
||||
parser := MustParse(&args)
|
||||
assert.Nil(t, parser)
|
||||
assert.Equal(t, 2, exitCode)
|
||||
assert.Equal(t, -1, exitCode)
|
||||
}
|
||||
|
||||
func TestMustParsePrintsHelp(t *testing.T) {
|
||||
originalExit := osExit
|
||||
originalStdout := stdout
|
||||
originalArgs := os.Args
|
||||
defer func() {
|
||||
osExit = originalExit
|
||||
stdout = originalStdout
|
||||
os.Args = originalArgs
|
||||
}()
|
||||
|
||||
var exitCode *int
|
||||
osExit = func(code int) { exitCode = &code }
|
||||
os.Args = []string{"someprogram", "--help"}
|
||||
|
||||
var exitCode int
|
||||
var stdout bytes.Buffer
|
||||
exit := func(code int) { exitCode = code }
|
||||
stdout = &bytes.Buffer{}
|
||||
|
||||
var args struct{}
|
||||
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
|
||||
parser := MustParse(&args)
|
||||
assert.NotNil(t, parser)
|
||||
assert.Equal(t, 0, exitCode)
|
||||
require.NotNil(t, exitCode)
|
||||
assert.Equal(t, 0, *exitCode)
|
||||
}
|
||||
|
||||
func TestMustParsePrintsVersion(t *testing.T) {
|
||||
originalExit := osExit
|
||||
originalStdout := stdout
|
||||
originalArgs := os.Args
|
||||
defer func() {
|
||||
osExit = originalExit
|
||||
stdout = originalStdout
|
||||
os.Args = originalArgs
|
||||
}()
|
||||
|
||||
var exitCode int
|
||||
var stdout bytes.Buffer
|
||||
exit := func(code int) { exitCode = code }
|
||||
|
||||
var exitCode *int
|
||||
osExit = func(code int) { exitCode = &code }
|
||||
os.Args = []string{"someprogram", "--version"}
|
||||
|
||||
var b bytes.Buffer
|
||||
stdout = &b
|
||||
|
||||
var args versioned
|
||||
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
|
||||
parser := MustParse(&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)
|
||||
require.NotNil(t, exitCode)
|
||||
assert.Equal(t, 0, *exitCode)
|
||||
assert.Equal(t, "example 3.2.1\n", b.String())
|
||||
}
|
19
reflect.go
19
reflect.go
|
@ -7,15 +7,15 @@ import (
|
|||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"go.wit.com/dev/alexflint/scalar"
|
||||
scalar "github.com/alexflint/go-scalar"
|
||||
)
|
||||
|
||||
var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()
|
||||
|
||||
// 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
|
||||
// - 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
|
||||
|
||||
const (
|
||||
|
@ -74,10 +74,10 @@ func cardinalityOf(t reflect.Type) (cardinality, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// isBoolean returns true if the type is a boolean or a pointer to a boolean
|
||||
// isBoolean returns true if the type can be parsed from a single string
|
||||
func isBoolean(t reflect.Type) bool {
|
||||
switch {
|
||||
case isTextUnmarshaler(t):
|
||||
case t.Implements(textUnmarshalerType):
|
||||
return false
|
||||
case t.Kind() == reflect.Bool:
|
||||
return true
|
||||
|
@ -88,11 +88,6 @@ func isBoolean(t reflect.Type) bool {
|
|||
}
|
||||
}
|
||||
|
||||
// isTextUnmarshaler returns true if the type or its pointer implements encoding.TextUnmarshaler
|
||||
func isTextUnmarshaler(t reflect.Type) bool {
|
||||
return t.Implements(textUnmarshalerType) || reflect.PtrTo(t).Implements(textUnmarshalerType)
|
||||
}
|
||||
|
||||
// isExported returns true if the struct field name is exported
|
||||
func isExported(field string) bool {
|
||||
r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8
|
||||
|
@ -102,7 +97,7 @@ func isExported(field string) bool {
|
|||
// isZero returns true if v contains the zero value for its type
|
||||
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 {
|
||||
if t.Kind() == reflect.Slice || t.Kind() == reflect.Map {
|
||||
return v.IsNil()
|
||||
}
|
||||
if !t.Comparable() {
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"reflect"
|
||||
"strings"
|
||||
|
||||
"go.wit.com/dev/alexflint/scalar"
|
||||
scalar "github.com/alexflint/go-scalar"
|
||||
)
|
||||
|
||||
// setSliceOrMap parses a sequence of strings into a slice or map. If clear is
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
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
|
||||
|
@ -9,35 +7,31 @@ import "fmt"
|
|||
// no command line arguments have been processed by this parser then it
|
||||
// returns nil.
|
||||
func (p *Parser) Subcommand() interface{} {
|
||||
if len(p.subcommand) == 0 {
|
||||
if p.lastCmd == nil || p.lastCmd.parent == nil {
|
||||
return nil
|
||||
}
|
||||
cmd, err := p.lookupCommand(p.subcommand...)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return p.val(cmd.dest).Interface()
|
||||
return p.val(p.lastCmd.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 {
|
||||
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
|
||||
if p.lastCmd == nil {
|
||||
return nil
|
||||
}
|
||||
return cmd, nil
|
||||
|
||||
// make a list of ancestor commands
|
||||
var ancestors []string
|
||||
cur := p.lastCmd
|
||||
for cur.parent != nil { // we want to exclude the root
|
||||
ancestors = append(ancestors, cur.name)
|
||||
cur = cur.parent
|
||||
}
|
||||
|
||||
// reverse the list
|
||||
out := make([]string, len(ancestors))
|
||||
for i := 0; i < len(ancestors); i++ {
|
||||
out[i] = ancestors[len(ancestors)-i-1]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
|
@ -83,19 +83,6 @@ func TestNamedSubcommand(t *testing.T) {
|
|||
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
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 {
|
||||
}
|
||||
|
@ -126,23 +113,6 @@ 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
|
||||
|
@ -305,60 +275,6 @@ func TestNestedSubcommands(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNestedSubcommandsWithAliases(t *testing.T) {
|
||||
type child struct{}
|
||||
type parent struct {
|
||||
Child *child `arg:"subcommand:child|ch"`
|
||||
}
|
||||
type grandparent struct {
|
||||
Parent *parent `arg:"subcommand:parent|pa"`
|
||||
}
|
||||
type root struct {
|
||||
Grandparent *grandparent `arg:"subcommand:grandparent|gp"`
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := pparse("gp parent child", &args)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.Grandparent)
|
||||
require.NotNil(t, args.Grandparent.Parent)
|
||||
require.NotNil(t, args.Grandparent.Parent.Child)
|
||||
assert.Equal(t, args.Grandparent.Parent.Child, p.Subcommand())
|
||||
assert.Equal(t, []string{"gp", "parent", "child"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := pparse("grandparent pa", &args)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.Grandparent)
|
||||
require.NotNil(t, args.Grandparent.Parent)
|
||||
require.Nil(t, args.Grandparent.Parent.Child)
|
||||
assert.Equal(t, args.Grandparent.Parent, p.Subcommand())
|
||||
assert.Equal(t, []string{"grandparent", "pa"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := pparse("grandparent", &args)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.Grandparent)
|
||||
require.Nil(t, args.Grandparent.Parent)
|
||||
assert.Equal(t, args.Grandparent, p.Subcommand())
|
||||
assert.Equal(t, []string{"grandparent"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := pparse("", &args)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, args.Grandparent)
|
||||
assert.Nil(t, p.Subcommand())
|
||||
assert.Empty(t, p.SubcommandNames())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubcommandsWithPositionals(t *testing.T) {
|
||||
type listCmd struct {
|
||||
Pattern string `arg:"positional"`
|
||||
|
@ -495,14 +411,3 @@ func TestValForNilStruct(t *testing.T) {
|
|||
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)
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
all:
|
||||
@echo
|
||||
@echo
|
||||
|
||||
test:
|
||||
|
||||
redomod:
|
||||
rm -f go.*
|
||||
GO111MODULE= go mod init
|
||||
GO111MODULE= go mod tidy
|
11
test/go.mod
11
test/go.mod
|
@ -1,11 +0,0 @@
|
|||
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
|
||||
)
|
10
test/go.sum
10
test/go.sum
|
@ -1,10 +0,0 @@
|
|||
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=
|
220
usage.go
220
usage.go
|
@ -3,37 +3,54 @@ package arg
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// the width of the left column
|
||||
const colWidth = 25
|
||||
|
||||
// Fail prints usage information to p.Config.Out and exits with status code 2.
|
||||
// to allow monkey patching in tests
|
||||
var (
|
||||
stdout io.Writer = os.Stdout
|
||||
stderr io.Writer = os.Stderr
|
||||
osExit = os.Exit
|
||||
)
|
||||
|
||||
// Fail prints usage information to stderr and exits with non-zero status
|
||||
func (p *Parser) Fail(msg string) {
|
||||
p.FailSubcommand(msg)
|
||||
p.failWithSubcommand(msg, p.cmd)
|
||||
}
|
||||
|
||||
// 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
|
||||
// FailSubcommand prints usage information for a specified subcommand to stderr,
|
||||
// then exits with non-zero status. 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...)
|
||||
cmd, err := p.lookupCommand(subcommand...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(p.config.Out, "error:", msg)
|
||||
p.config.Exit(2)
|
||||
p.failWithSubcommand(msg, cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
// failWithSubcommand prints usage information for the given subcommand to stderr and exits with non-zero status
|
||||
func (p *Parser) failWithSubcommand(msg string, cmd *command) {
|
||||
p.writeUsageForSubcommand(stderr, cmd)
|
||||
fmt.Fprintln(stderr, "error:", msg)
|
||||
osExit(-1)
|
||||
}
|
||||
|
||||
// WriteUsage writes usage information to the given writer
|
||||
func (p *Parser) WriteUsage(w io.Writer) {
|
||||
p.WriteUsageForSubcommand(w, p.subcommand...)
|
||||
cmd := p.cmd
|
||||
if p.lastCmd != nil {
|
||||
cmd = p.lastCmd
|
||||
}
|
||||
p.writeUsageForSubcommand(w, cmd)
|
||||
}
|
||||
|
||||
// WriteUsageForSubcommand writes the usage information for a specified
|
||||
|
@ -46,7 +63,12 @@ func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) erro
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.writeUsageForSubcommand(w, cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeUsageForSubcommand writes usage information for the given subcommand
|
||||
func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *command) {
|
||||
var positionals, longOptions, shortOptions []*spec
|
||||
for _, spec := range cmd.specs {
|
||||
switch {
|
||||
|
@ -59,10 +81,22 @@ func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) erro
|
|||
}
|
||||
}
|
||||
|
||||
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.Fprintf(w, "Usage: %s", p.cmd.name)
|
||||
for _, s := range subcommand {
|
||||
fmt.Fprint(w, " "+s)
|
||||
fmt.Fprint(w, "Usage:")
|
||||
for i := len(ancestors) - 1; i >= 0; i-- {
|
||||
fmt.Fprint(w, " "+ancestors[i])
|
||||
}
|
||||
|
||||
// write the option component of the usage message
|
||||
|
@ -123,66 +157,47 @@ func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) erro
|
|||
}
|
||||
|
||||
fmt.Fprint(w, "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
func printTwoCols(w io.Writer, left, help string, defaultVal string, envVal string) {
|
||||
lhs := " " + left
|
||||
fmt.Fprint(w, lhs)
|
||||
if description != "" {
|
||||
if help != "" {
|
||||
if len(lhs)+2 < colWidth {
|
||||
fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs)))
|
||||
} else {
|
||||
fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
|
||||
}
|
||||
fmt.Fprint(w, description)
|
||||
fmt.Fprint(w, help)
|
||||
}
|
||||
|
||||
var brack string
|
||||
for _, s := range bracketed {
|
||||
if s != "" {
|
||||
if brack != "" {
|
||||
brack += ", "
|
||||
}
|
||||
brack += s
|
||||
}
|
||||
bracketsContent := []string{}
|
||||
|
||||
if defaultVal != "" {
|
||||
bracketsContent = append(bracketsContent,
|
||||
fmt.Sprintf("default: %s", defaultVal),
|
||||
)
|
||||
}
|
||||
|
||||
if brack != "" {
|
||||
fmt.Fprintf(w, " [%s]", brack)
|
||||
if envVal != "" {
|
||||
bracketsContent = append(bracketsContent,
|
||||
fmt.Sprintf("env: %s", envVal),
|
||||
)
|
||||
}
|
||||
|
||||
if len(bracketsContent) > 0 {
|
||||
fmt.Fprintf(w, " [%s]", strings.Join(bracketsContent, ", "))
|
||||
}
|
||||
fmt.Fprint(w, "\n")
|
||||
}
|
||||
|
||||
func withDefault(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
return "default: " + s
|
||||
}
|
||||
|
||||
func withEnv(env string) string {
|
||||
if env == "" {
|
||||
return ""
|
||||
}
|
||||
return "env: " + env
|
||||
}
|
||||
|
||||
// WriteHelp writes the usage string followed by the full help string for each option
|
||||
func (p *Parser) WriteHelp(w io.Writer) {
|
||||
p.WriteHelpForSubcommand(w, p.subcommand...)
|
||||
cmd := p.cmd
|
||||
if p.lastCmd != nil {
|
||||
cmd = p.lastCmd
|
||||
}
|
||||
p.writeHelpForSubcommand(w, cmd)
|
||||
}
|
||||
|
||||
// WriteHelpForSubcommand writes the usage string followed by the full help
|
||||
|
@ -195,55 +210,34 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.writeHelpForSubcommand(w, cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
var positionals, longOptions, shortOptions, envOnlyOptions []*spec
|
||||
var hasVersionOption bool
|
||||
// writeHelp writes the usage string for the given subcommand
|
||||
func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
|
||||
var positionals, longOptions, shortOptions []*spec
|
||||
for _, spec := range cmd.specs {
|
||||
switch {
|
||||
case spec.positional:
|
||||
positionals = append(positionals, 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)
|
||||
}
|
||||
|
||||
if !hasVersionOption && p.version != "" {
|
||||
fmt.Fprintln(w, p.version)
|
||||
}
|
||||
|
||||
p.WriteUsageForSubcommand(w, subcommand...)
|
||||
p.writeUsageForSubcommand(w, cmd)
|
||||
|
||||
// write the list of positionals
|
||||
if len(positionals) > 0 {
|
||||
fmt.Fprint(w, "\nPositional arguments:\n")
|
||||
for _, spec := range positionals {
|
||||
print(w, spec.placeholder, spec.help, withDefault(spec.defaultString), withEnv(spec.env))
|
||||
printTwoCols(w, spec.placeholder, spec.help, "", "")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -258,6 +252,14 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
|
|||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
if len(globals) > 0 {
|
||||
fmt.Fprint(w, "\nGlobal options:\n")
|
||||
|
@ -273,7 +275,7 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
|
|||
short: "h",
|
||||
help: "display this help and exit",
|
||||
})
|
||||
if !hasVersionOption && p.version != "" {
|
||||
if p.version != "" {
|
||||
p.printOption(w, &spec{
|
||||
cardinality: zero,
|
||||
long: "version",
|
||||
|
@ -281,27 +283,17 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
|
|||
})
|
||||
}
|
||||
|
||||
// write the list of environment only variables
|
||||
if len(envOnlyOptions) > 0 {
|
||||
fmt.Fprint(w, "\nEnvironment variables:\n")
|
||||
for _, spec := range envOnlyOptions {
|
||||
p.printEnvOnlyVar(w, spec)
|
||||
}
|
||||
}
|
||||
|
||||
// write the list of subcommands
|
||||
if len(cmd.subcommands) > 0 {
|
||||
fmt.Fprint(w, "\nCommands:\n")
|
||||
for _, subcmd := range cmd.subcommands {
|
||||
names := append([]string{subcmd.name}, subcmd.aliases...)
|
||||
print(w, strings.Join(names, ", "), subcmd.help)
|
||||
printTwoCols(w, subcmd.name, subcmd.help, "", "")
|
||||
}
|
||||
}
|
||||
|
||||
if p.epilogue != "" {
|
||||
fmt.Fprintln(w, "\n"+p.epilogue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) printOption(w io.Writer, spec *spec) {
|
||||
|
@ -313,30 +305,34 @@ func (p *Parser) printOption(w io.Writer, spec *spec) {
|
|||
ways = append(ways, synopsis(spec, "-"+spec.short))
|
||||
}
|
||||
if len(ways) > 0 {
|
||||
print(w, strings.Join(ways, ", "), spec.help, withDefault(spec.defaultString), withEnv(spec.env))
|
||||
printTwoCols(w, strings.Join(ways, ", "), spec.help, spec.defaultVal, spec.env)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) {
|
||||
ways := make([]string, 0, 2)
|
||||
if spec.required {
|
||||
ways = append(ways, "Required.")
|
||||
} else {
|
||||
ways = append(ways, "Optional.")
|
||||
// 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 {
|
||||
var found *command
|
||||
for _, child := range cmd.subcommands {
|
||||
if child.name == name {
|
||||
found = child
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
|
||||
}
|
||||
cmd = found
|
||||
}
|
||||
|
||||
if spec.help != "" {
|
||||
ways = append(ways, spec.help)
|
||||
}
|
||||
|
||||
print(w, spec.env, strings.Join(ways, " "), withDefault(spec.defaultString))
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func synopsis(spec *spec, form string) string {
|
||||
// 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 == "" {
|
||||
if spec.cardinality == zero {
|
||||
return form
|
||||
}
|
||||
return form + " " + spec.placeholder
|
||||
|
|
|
@ -50,16 +50,12 @@ Options:
|
|||
--optimize OPTIMIZE, -O OPTIMIZE
|
||||
optimization level
|
||||
--ids IDS Ids
|
||||
--values VALUES Values
|
||||
--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
|
||||
|
||||
Environment variables:
|
||||
API_KEY Required. Only via env-var for security reasons
|
||||
TRACE Optional. Record low-level trace
|
||||
`
|
||||
|
||||
var args struct {
|
||||
|
@ -74,12 +70,11 @@ Environment variables:
|
|||
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"`
|
||||
ApiKey string `arg:"required,-,--,env:API_KEY" help:"Only via env-var for security reasons"`
|
||||
Trace bool `arg:"-,--,env" help:"Record low-level trace"`
|
||||
File *NameDotName `arg:"-f" help:"File with mandatory extension"`
|
||||
}
|
||||
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)
|
||||
|
@ -237,7 +232,7 @@ func (versioned) Version() string {
|
|||
}
|
||||
|
||||
func TestUsageWithVersion(t *testing.T) {
|
||||
expectedUsage := "Usage: example"
|
||||
expectedUsage := "example 3.2.1\nUsage: example"
|
||||
|
||||
expectedHelp := `
|
||||
example 3.2.1
|
||||
|
@ -260,233 +255,6 @@ Options:
|
|||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestUsageWithUserDefinedVersionFlag(t *testing.T) {
|
||||
expectedUsage := "Usage: example [--version]"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example [--version]
|
||||
|
||||
Options:
|
||||
--version this is a user-defined version flag
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
|
||||
var args struct {
|
||||
ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
|
||||
}
|
||||
|
||||
os.Args[0] = "example"
|
||||
p, err := NewParser(Config{}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestUsageWithVersionAndUserDefinedVersionFlag(t *testing.T) {
|
||||
expectedUsage := "Usage: example [--version]"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example [--version]
|
||||
|
||||
Options:
|
||||
--version this is a user-defined version flag
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
|
||||
var args struct {
|
||||
versioned
|
||||
ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
|
||||
}
|
||||
|
||||
os.Args[0] = "example"
|
||||
p, err := NewParser(Config{}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
type subcommand struct {
|
||||
Number int `arg:"-n,--number" help:"compute something on the given number"`
|
||||
}
|
||||
|
||||
func TestUsageWithVersionAndSubcommand(t *testing.T) {
|
||||
expectedUsage := "Usage: example <command> [<args>]"
|
||||
|
||||
expectedHelp := `
|
||||
example 3.2.1
|
||||
Usage: example <command> [<args>]
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
--version display version and exit
|
||||
|
||||
Commands:
|
||||
cmd
|
||||
`
|
||||
|
||||
var args struct {
|
||||
versioned
|
||||
Cmd *subcommand `arg:"subcommand"`
|
||||
}
|
||||
|
||||
os.Args[0] = "example"
|
||||
p, err := NewParser(Config{}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
|
||||
expectedUsage = "Usage: example cmd [--number NUMBER]"
|
||||
|
||||
expectedHelp = `
|
||||
example 3.2.1
|
||||
Usage: example cmd [--number NUMBER]
|
||||
|
||||
Options:
|
||||
--number NUMBER, -n NUMBER
|
||||
compute something on the given number
|
||||
--help, -h display this help and exit
|
||||
--version display version and exit
|
||||
`
|
||||
_ = p.Parse([]string{"cmd"})
|
||||
|
||||
help = bytes.Buffer{}
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
usage = bytes.Buffer{}
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestUsageWithUserDefinedVersionFlagAndSubcommand(t *testing.T) {
|
||||
expectedUsage := "Usage: example [--version] <command> [<args>]"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example [--version] <command> [<args>]
|
||||
|
||||
Options:
|
||||
--version this is a user-defined version flag
|
||||
--help, -h display this help and exit
|
||||
|
||||
Commands:
|
||||
cmd
|
||||
`
|
||||
|
||||
var args struct {
|
||||
Cmd *subcommand `arg:"subcommand"`
|
||||
ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
|
||||
}
|
||||
|
||||
os.Args[0] = "example"
|
||||
p, err := NewParser(Config{}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
|
||||
expectedUsage = "Usage: example cmd [--number NUMBER]"
|
||||
|
||||
expectedHelp = `
|
||||
Usage: example cmd [--number NUMBER]
|
||||
|
||||
Options:
|
||||
--number NUMBER, -n NUMBER
|
||||
compute something on the given number
|
||||
|
||||
Global options:
|
||||
--version this is a user-defined version flag
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
_ = p.Parse([]string{"cmd"})
|
||||
|
||||
help = bytes.Buffer{}
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
usage = bytes.Buffer{}
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestUsageWithVersionAndUserDefinedVersionFlagAndSubcommand(t *testing.T) {
|
||||
expectedUsage := "Usage: example [--version] <command> [<args>]"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example [--version] <command> [<args>]
|
||||
|
||||
Options:
|
||||
--version this is a user-defined version flag
|
||||
--help, -h display this help and exit
|
||||
|
||||
Commands:
|
||||
cmd
|
||||
`
|
||||
|
||||
var args struct {
|
||||
versioned
|
||||
Cmd *subcommand `arg:"subcommand"`
|
||||
ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
|
||||
}
|
||||
|
||||
os.Args[0] = "example"
|
||||
p, err := NewParser(Config{}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
|
||||
expectedUsage = "Usage: example cmd [--number NUMBER]"
|
||||
|
||||
expectedHelp = `
|
||||
Usage: example cmd [--number NUMBER]
|
||||
|
||||
Options:
|
||||
--number NUMBER, -n NUMBER
|
||||
compute something on the given number
|
||||
|
||||
Global options:
|
||||
--version this is a user-defined version flag
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
_ = p.Parse([]string{"cmd"})
|
||||
|
||||
help = bytes.Buffer{}
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
usage = bytes.Buffer{}
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
type described struct{}
|
||||
|
||||
// Described returns the description for this program
|
||||
|
@ -642,50 +410,6 @@ Options:
|
|||
assert.Equal(t, expectedUsage, usage.String())
|
||||
}
|
||||
|
||||
func TestUsageWithSubcommands(t *testing.T) {
|
||||
expectedUsage := "Usage: example child [--values VALUES]"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example child [--values VALUES]
|
||||
|
||||
Options:
|
||||
--values VALUES Values
|
||||
|
||||
Global options:
|
||||
--verbose, -v verbosity level
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
|
||||
var args struct {
|
||||
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||
Child *struct {
|
||||
Values []float64 `help:"Values"`
|
||||
} `arg:"subcommand:child"`
|
||||
}
|
||||
|
||||
os.Args[0] = "example"
|
||||
p, err := NewParser(Config{}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
_ = p.Parse([]string{"child"})
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var help2 bytes.Buffer
|
||||
p.WriteHelpForSubcommand(&help2, "child")
|
||||
assert.Equal(t, expectedHelp[1:], help2.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
|
||||
var usage2 bytes.Buffer
|
||||
p.WriteUsageForSubcommand(&usage2, "child")
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String()))
|
||||
}
|
||||
|
||||
func TestUsageWithNestedSubcommands(t *testing.T) {
|
||||
expectedUsage := "Usage: example child nested [--enable] OUTPUT"
|
||||
|
||||
|
@ -721,8 +445,6 @@ Global options:
|
|||
|
||||
_ = p.Parse([]string{"child", "nested", "value"})
|
||||
|
||||
assert.Equal(t, []string{"child", "nested"}, p.SubcommandNames())
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
@ -744,7 +466,7 @@ func TestNonexistentSubcommand(t *testing.T) {
|
|||
var args struct {
|
||||
sub *struct{} `arg:"subcommand"`
|
||||
}
|
||||
p, err := NewParser(Config{Exit: func(int) {}}, &args)
|
||||
p, err := NewParser(Config{}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
var b bytes.Buffer
|
||||
|
@ -784,36 +506,7 @@ Options:
|
|||
ShortOnly2 string `arg:"-b,--,required" help:"some help2"`
|
||||
}
|
||||
p, err := NewParser(Config{Program: "example"}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestUsageWithEmptyPlaceholder(t *testing.T) {
|
||||
expectedUsage := "Usage: example [-a] [--b] [--c]"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example [-a] [--b] [--c]
|
||||
|
||||
Options:
|
||||
-a some help for a
|
||||
--b some help for b
|
||||
--c, -c some help for c
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
ShortOnly string `arg:"-a,--" placeholder:"" help:"some help for a"`
|
||||
LongOnly string `arg:"--b" placeholder:"" help:"some help for b"`
|
||||
Both string `arg:"-c,--c" placeholder:"" help:"some help for c"`
|
||||
}
|
||||
p, err := NewParser(Config{Program: "example"}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
|
@ -860,16 +553,10 @@ Usage: example [-s SHORT]
|
|||
Options:
|
||||
-s SHORT [env: SHORT]
|
||||
--help, -h display this help and exit
|
||||
|
||||
Environment variables:
|
||||
ENVONLY Optional.
|
||||
ENVONLY2 Optional.
|
||||
CUSTOM Optional.
|
||||
`
|
||||
var args struct {
|
||||
Short string `arg:"--,-s,env"`
|
||||
EnvOnly string `arg:"--,env"`
|
||||
EnvOnly2 string `arg:"--,-,env"`
|
||||
EnvOnlyOverriden string `arg:"--,env:CUSTOM"`
|
||||
}
|
||||
|
||||
|
@ -885,39 +572,19 @@ Environment variables:
|
|||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestEnvOnlyArgs(t *testing.T) {
|
||||
expectedUsage := "Usage: example [--arg ARG]"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example [--arg ARG]
|
||||
|
||||
Options:
|
||||
--arg ARG, -a ARG [env: MY_ARG]
|
||||
--help, -h display this help and exit
|
||||
|
||||
Environment variables:
|
||||
AUTH_KEY Required.
|
||||
`
|
||||
var args struct {
|
||||
ArgParam string `arg:"-a,--arg,env:MY_ARG"`
|
||||
AuthKey string `arg:"required,--,env:AUTH_KEY"`
|
||||
}
|
||||
p, err := NewParser(Config{Program: "example"}, &args)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestFail(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
originalStderr := stderr
|
||||
originalExit := osExit
|
||||
defer func() {
|
||||
stderr = originalStderr
|
||||
osExit = originalExit
|
||||
}()
|
||||
|
||||
var b bytes.Buffer
|
||||
stderr = &b
|
||||
|
||||
var exitCode int
|
||||
exit := func(code int) { exitCode = code }
|
||||
osExit = func(code int) { exitCode = code }
|
||||
|
||||
expectedStdout := `
|
||||
Usage: example [--foo FOO]
|
||||
|
@ -927,18 +594,27 @@ error: something went wrong
|
|||
var args struct {
|
||||
Foo int
|
||||
}
|
||||
p, err := NewParser(Config{Program: "example", Exit: exit, Out: &stdout}, &args)
|
||||
p, err := NewParser(Config{Program: "example"}, &args)
|
||||
require.NoError(t, err)
|
||||
p.Fail("something went wrong")
|
||||
|
||||
assert.Equal(t, expectedStdout[1:], stdout.String())
|
||||
assert.Equal(t, 2, exitCode)
|
||||
assert.Equal(t, expectedStdout[1:], b.String())
|
||||
assert.Equal(t, -1, exitCode)
|
||||
}
|
||||
|
||||
func TestFailSubcommand(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
originalStderr := stderr
|
||||
originalExit := osExit
|
||||
defer func() {
|
||||
stderr = originalStderr
|
||||
osExit = originalExit
|
||||
}()
|
||||
|
||||
var b bytes.Buffer
|
||||
stderr = &b
|
||||
|
||||
var exitCode int
|
||||
exit := func(code int) { exitCode = code }
|
||||
osExit = func(code int) { exitCode = code }
|
||||
|
||||
expectedStdout := `
|
||||
Usage: example sub
|
||||
|
@ -948,139 +624,12 @@ error: something went wrong
|
|||
var args struct {
|
||||
Sub *struct{} `arg:"subcommand"`
|
||||
}
|
||||
p, err := NewParser(Config{Program: "example", Exit: exit, Out: &stdout}, &args)
|
||||
p, err := NewParser(Config{Program: "example"}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.FailSubcommand("something went wrong", "sub")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expectedStdout[1:], stdout.String())
|
||||
assert.Equal(t, 2, exitCode)
|
||||
}
|
||||
|
||||
type lengthOf struct {
|
||||
Length int
|
||||
}
|
||||
|
||||
func (p *lengthOf) UnmarshalText(b []byte) error {
|
||||
p.Length = len(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestHelpShowsDefaultValueFromOriginalTag(t *testing.T) {
|
||||
// check that the usage text prints the original string from the default tag, not
|
||||
// the serialization of the parsed value
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example [--test TEST]
|
||||
|
||||
Options:
|
||||
--test TEST [default: some_default_value]
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
|
||||
var args struct {
|
||||
Test *lengthOf `default:"some_default_value"`
|
||||
}
|
||||
p, err := NewParser(Config{Program: "example"}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
}
|
||||
|
||||
func TestHelpShowsSubcommandAliases(t *testing.T) {
|
||||
expectedHelp := `
|
||||
Usage: example <command> [<args>]
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
|
||||
Commands:
|
||||
remove, rm, r remove something from somewhere
|
||||
simple do something simple
|
||||
halt, stop stop now
|
||||
`
|
||||
|
||||
var args struct {
|
||||
Remove *struct{} `arg:"subcommand:remove|rm|r" help:"remove something from somewhere"`
|
||||
Simple *struct{} `arg:"subcommand" help:"do something simple"`
|
||||
Stop *struct{} `arg:"subcommand:halt|stop" help:"stop now"`
|
||||
}
|
||||
p, err := NewParser(Config{Program: "example"}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
}
|
||||
|
||||
func TestHelpShowsPositionalWithDefault(t *testing.T) {
|
||||
expectedHelp := `
|
||||
Usage: example [FOO]
|
||||
|
||||
Positional arguments:
|
||||
FOO this is a positional with a default [default: bar]
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
|
||||
var args struct {
|
||||
Foo string `arg:"positional" default:"bar" help:"this is a positional with a default"`
|
||||
}
|
||||
|
||||
p, err := NewParser(Config{Program: "example"}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
}
|
||||
|
||||
func TestHelpShowsPositionalWithEnv(t *testing.T) {
|
||||
expectedHelp := `
|
||||
Usage: example [FOO]
|
||||
|
||||
Positional arguments:
|
||||
FOO this is a positional with an env variable [env: FOO]
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
|
||||
var args struct {
|
||||
Foo string `arg:"positional,env:FOO" help:"this is a positional with an env variable"`
|
||||
}
|
||||
|
||||
p, err := NewParser(Config{Program: "example"}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
}
|
||||
|
||||
func TestHelpShowsPositionalWithDefaultAndEnv(t *testing.T) {
|
||||
expectedHelp := `
|
||||
Usage: example [FOO]
|
||||
|
||||
Positional arguments:
|
||||
FOO this is a positional with a default and an env variable [default: bar, env: FOO]
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
|
||||
var args struct {
|
||||
Foo string `arg:"positional,env:FOO" default:"bar" help:"this is a positional with a default and an env variable"`
|
||||
}
|
||||
|
||||
p, err := NewParser(Config{Program: "example"}, &args)
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
assert.Equal(t, expectedStdout[1:], b.String())
|
||||
assert.Equal(t, -1, exitCode)
|
||||
}
|
|
@ -0,0 +1,319 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Argument represents a command line argument
|
||||
type Argument struct {
|
||||
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
|
||||
defaultVal string // default value for this option
|
||||
placeholder string // name of the data in help
|
||||
}
|
||||
|
||||
// Command represents a named subcommand, or the top-level command
|
||||
type Command struct {
|
||||
name string
|
||||
help string
|
||||
dest path
|
||||
args []*Argument
|
||||
subcommands []*Command
|
||||
parent *Command
|
||||
}
|
||||
|
||||
// Parser represents a set of command line options with destination values
|
||||
type Parser struct {
|
||||
cmd *Command // the top-level command
|
||||
root reflect.Value // destination struct to fill will values
|
||||
version string // version from the argument struct
|
||||
prologue string // prologue for help text (from the argument struct)
|
||||
epilogue string // epilogue for help text (from the argument struct)
|
||||
|
||||
// the following fields are updated during processing of command line arguments
|
||||
leaf *Command // the subcommand we processed last
|
||||
accessible []*Argument // concatenation of the leaf subcommand's arguments plus all ancestors' arguments
|
||||
seen map[*Argument]bool // the arguments we encountered while processing command line arguments
|
||||
}
|
||||
|
||||
// Versioned is the interface that the destination struct should implement to
|
||||
// make a version string appear at the top of the help message.
|
||||
type Versioned interface {
|
||||
// Version returns the version string that will be printed on a line by itself
|
||||
// at the top of the help message.
|
||||
Version() string
|
||||
}
|
||||
|
||||
// Described is the interface that the destination struct should implement to
|
||||
// make a description string appear at the top of the help message.
|
||||
type Described interface {
|
||||
// Description returns the string that will be printed on a line by itself
|
||||
// at the top of the help message.
|
||||
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
|
||||
}
|
||||
|
||||
// the ParserOption interface matches options for the parser constructor
|
||||
type ParserOption interface {
|
||||
parserOption()
|
||||
}
|
||||
|
||||
type programNameParserOption struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func (programNameParserOption) parserOption() {}
|
||||
|
||||
// WithProgramName overrides the name of the program as displayed in help test
|
||||
func WithProgramName(name string) ParserOption {
|
||||
return programNameParserOption{s: name}
|
||||
}
|
||||
|
||||
// NewParser constructs a parser from a list of destination structs
|
||||
func NewParser(dest interface{}, options ...ParserOption) (*Parser, error) {
|
||||
// check the destination type
|
||||
t := reflect.TypeOf(dest)
|
||||
if t.Kind() != reflect.Ptr {
|
||||
panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", t))
|
||||
}
|
||||
|
||||
// pick a program name for help text and usage output
|
||||
program := "program"
|
||||
if len(os.Args) > 0 {
|
||||
program = filepath.Base(os.Args[0])
|
||||
}
|
||||
|
||||
// apply the options
|
||||
for _, opt := range options {
|
||||
switch opt := opt.(type) {
|
||||
case programNameParserOption:
|
||||
program = opt.s
|
||||
}
|
||||
}
|
||||
|
||||
// build the root command from the struct
|
||||
cmd, err := cmdFromStruct(program, path{}, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// construct the parser
|
||||
p := Parser{
|
||||
seen: make(map[*Argument]bool),
|
||||
root: reflect.ValueOf(dest),
|
||||
cmd: cmd,
|
||||
}
|
||||
// copy the args for the root command into "accessible", which will
|
||||
// grow each time we open up a subcommand
|
||||
p.accessible = make([]*Argument, len(p.cmd.args))
|
||||
copy(p.accessible, p.cmd.args)
|
||||
|
||||
// check for version, prologue, and epilogue
|
||||
if dest, ok := dest.(Versioned); ok {
|
||||
p.version = dest.Version()
|
||||
}
|
||||
if dest, ok := dest.(Described); ok {
|
||||
p.prologue = 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) {
|
||||
// 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",
|
||||
dest, t.Kind())
|
||||
}
|
||||
|
||||
t = t.Elem()
|
||||
if t.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a pointer to %s",
|
||||
dest, t.Kind())
|
||||
}
|
||||
|
||||
cmd := Command{
|
||||
name: name,
|
||||
dest: dest,
|
||||
}
|
||||
|
||||
var errs []string
|
||||
walkFields(t, func(field reflect.StructField, t reflect.Type) bool {
|
||||
// check for the ignore switch in the tag
|
||||
tag := field.Tag.Get("arg")
|
||||
if tag == "-" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// create a new destination path for this field
|
||||
subdest := dest.Child(field)
|
||||
arg := Argument{
|
||||
dest: subdest,
|
||||
field: field,
|
||||
long: strings.ToLower(field.Name),
|
||||
}
|
||||
|
||||
help, exists := field.Tag.Lookup("help")
|
||||
if exists {
|
||||
arg.help = help
|
||||
}
|
||||
|
||||
defaultVal, hasDefault := field.Tag.Lookup("default")
|
||||
if hasDefault {
|
||||
arg.defaultVal = defaultVal
|
||||
}
|
||||
|
||||
// Look at the tag
|
||||
var isSubcommand bool // tracks whether this field is a subcommand
|
||||
for _, key := range strings.Split(tag, ",") {
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
key = strings.TrimLeft(key, " ")
|
||||
var value string
|
||||
if pos := strings.Index(key, ":"); pos != -1 {
|
||||
value = key[pos+1:]
|
||||
key = key[:pos]
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(key, "---"):
|
||||
errs = append(errs, fmt.Sprintf("%s.%s: too many hyphens", t.Name(), field.Name))
|
||||
case strings.HasPrefix(key, "--"):
|
||||
arg.long = key[2:]
|
||||
case strings.HasPrefix(key, "-"):
|
||||
if len(key) != 2 {
|
||||
errs = append(errs, fmt.Sprintf("%s.%s: short arguments must be one character only",
|
||||
t.Name(), field.Name))
|
||||
return false
|
||||
}
|
||||
arg.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
|
||||
}
|
||||
arg.required = true
|
||||
case key == "positional":
|
||||
arg.positional = true
|
||||
case key == "separate":
|
||||
arg.separate = true
|
||||
case key == "env":
|
||||
// Use override name if provided
|
||||
if value != "" {
|
||||
arg.env = value
|
||||
} else {
|
||||
arg.env = strings.ToUpper(field.Name)
|
||||
}
|
||||
case key == "subcommand":
|
||||
// decide on a name for the subcommand
|
||||
cmdname := value
|
||||
if cmdname == "" {
|
||||
cmdname = strings.ToLower(field.Name)
|
||||
}
|
||||
|
||||
// parse the subcommand recursively
|
||||
subcmd, err := cmdFromStruct(cmdname, subdest, field.Type)
|
||||
if err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
subcmd.parent = &cmd
|
||||
subcmd.help = field.Tag.Get("help")
|
||||
|
||||
cmd.subcommands = append(cmd.subcommands, subcmd)
|
||||
isSubcommand = true
|
||||
default:
|
||||
errs = append(errs, fmt.Sprintf("unrecognized tag '%s' on field %s", key, tag))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
placeholder, hasPlaceholder := field.Tag.Lookup("placeholder")
|
||||
if hasPlaceholder {
|
||||
arg.placeholder = placeholder
|
||||
} else if arg.long != "" {
|
||||
arg.placeholder = strings.ToUpper(arg.long)
|
||||
} else {
|
||||
arg.placeholder = strings.ToUpper(arg.field.Name)
|
||||
}
|
||||
|
||||
// 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.args = append(cmd.args, &arg)
|
||||
|
||||
var err error
|
||||
arg.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
|
||||
}
|
||||
if arg.cardinality == multiple && hasDefault {
|
||||
errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields",
|
||||
t.Name(), field.Name))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// if this was an embedded field then we already returned true up above
|
||||
return false
|
||||
})
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, errors.New(strings.Join(errs, "\n"))
|
||||
}
|
||||
|
||||
// check that we don't have both positionals and subcommands
|
||||
var hasPositional bool
|
||||
for _, arg := range cmd.args {
|
||||
if arg.positional {
|
||||
hasPositional = true
|
||||
}
|
||||
}
|
||||
if hasPositional && len(cmd.subcommands) > 0 {
|
||||
return nil, fmt.Errorf("%s cannot have both subcommands and positional arguments", dest)
|
||||
}
|
||||
|
||||
return &cmd, nil
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInvalidTag(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string `arg:"this_is_not_valid"`
|
||||
}
|
||||
_, err := NewParser(&args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUnexportedFieldsSkipped(t *testing.T) {
|
||||
var args struct {
|
||||
unexported struct{}
|
||||
}
|
||||
|
||||
_, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// Package arg parses command line arguments using the fields from a struct.
|
||||
//
|
||||
// For example,
|
||||
//
|
||||
// var args struct {
|
||||
// Iter int
|
||||
// Debug bool
|
||||
// }
|
||||
// arg.MustParse(&args)
|
||||
//
|
||||
// defines two command line arguments, which can be set using any of
|
||||
//
|
||||
// ./example --iter=1 --debug // debug is a boolean flag so its value is set to true
|
||||
// ./example -iter 1 // debug defaults to its zero value (false)
|
||||
// ./example --debug=true // iter defaults to its zero value (zero)
|
||||
//
|
||||
// The fastest way to see how to use go-arg is to read the examples below.
|
||||
//
|
||||
// Fields can be bool, string, any float type, or any signed or unsigned integer type.
|
||||
// They can also be slices of any of the above, or slices of pointers to any of the above.
|
||||
//
|
||||
// Tags can be specified using the `arg` and `help` tag names:
|
||||
//
|
||||
// var args struct {
|
||||
// Input string `arg:"positional"`
|
||||
// Log string `arg:"positional,required"`
|
||||
// Debug bool `arg:"-d" help:"turn on debug mode"`
|
||||
// RealMode bool `arg:"--real"
|
||||
// Wr io.Writer `arg:"-"`
|
||||
// }
|
||||
//
|
||||
// Any tag string that starts with a single hyphen is the short form for an argument
|
||||
// (e.g. `./example -d`), and any tag string that starts with two hyphens is the long
|
||||
// form for the argument (instead of the field name).
|
||||
//
|
||||
// Other valid tag strings are `positional` and `required`.
|
||||
//
|
||||
// Fields can be excluded from processing with `arg:"-"`.
|
||||
package arg
|
|
@ -0,0 +1,507 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func split(s string) []string {
|
||||
return strings.Split(s, " ")
|
||||
}
|
||||
|
||||
// This example demonstrates basic usage
|
||||
func Example() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --foo=hello --bar")
|
||||
|
||||
var args struct {
|
||||
Foo string
|
||||
Bar bool
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println(args.Foo, args.Bar)
|
||||
// output: hello true
|
||||
}
|
||||
|
||||
// This example demonstrates arguments that have default values
|
||||
func Example_defaultValues() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example")
|
||||
|
||||
var args struct {
|
||||
Foo string `default:"abc"`
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println(args.Foo)
|
||||
// output: abc
|
||||
}
|
||||
|
||||
// This example demonstrates arguments that are required
|
||||
func Example_requiredArguments() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --foo=abc --bar")
|
||||
|
||||
var args struct {
|
||||
Foo string `arg:"required"`
|
||||
Bar bool
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println(args.Foo, args.Bar)
|
||||
// output: abc true
|
||||
}
|
||||
|
||||
// This example demonstrates positional arguments
|
||||
func Example_positionalArguments() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example in out1 out2 out3")
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional"`
|
||||
Output []string `arg:"positional"`
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println("In:", args.Input)
|
||||
fmt.Println("Out:", args.Output)
|
||||
// output:
|
||||
// In: in
|
||||
// Out: [out1 out2 out3]
|
||||
}
|
||||
|
||||
// This example demonstrates arguments that have multiple values
|
||||
func Example_multipleValues() {
|
||||
// The args you would pass in on the command line
|
||||
os.Args = split("./example --database localhost --ids 1 2 3")
|
||||
|
||||
var args struct {
|
||||
Database string
|
||||
IDs []int64
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Printf("Fetching the following IDs from %s: %v", args.Database, args.IDs)
|
||||
// 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() {
|
||||
os.Args = split("./example -c cmd1 db1 -f file1 db2 -c cmd2 -f file2 -f file3 db3 -c cmd3")
|
||||
var args struct {
|
||||
Commands []string `arg:"-c,separate"`
|
||||
Files []string `arg:"-f,separate"`
|
||||
Databases []string `arg:"positional"`
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println("Commands:", args.Commands)
|
||||
fmt.Println("Files:", args.Files)
|
||||
fmt.Println("Databases:", args.Databases)
|
||||
|
||||
// output:
|
||||
// Commands: [cmd1 cmd2 cmd3]
|
||||
// Files: [file1 file2 file3]
|
||||
// Databases: [db1 db2 db3]
|
||||
}
|
||||
|
||||
// This example shows the usage string generated by go-arg
|
||||
func Example_helpText() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --help")
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional,required"`
|
||||
Output []string `arg:"positional"`
|
||||
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||
Dataset string `help:"dataset to use"`
|
||||
Optimize int `arg:"-O,--optim" help:"optimization level"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
stdout = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
// Usage: example [--verbose] [--dataset DATASET] [--optim OPTIM] INPUT [OUTPUT [OUTPUT ...]]
|
||||
//
|
||||
// Positional arguments:
|
||||
// INPUT
|
||||
// OUTPUT
|
||||
//
|
||||
// Options:
|
||||
// --verbose, -v verbosity level
|
||||
// --dataset DATASET dataset to use
|
||||
// --optim OPTIM, -O OPTIM
|
||||
// optimization level
|
||||
// --help, -h display this help and exit
|
||||
}
|
||||
|
||||
// This example shows the usage string generated by go-arg with customized placeholders
|
||||
func Example_helpPlaceholder() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --help")
|
||||
|
||||
var args struct {
|
||||
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) {}
|
||||
stdout = 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
|
||||
// --maxjobs N, -j N maximum number of simultaneous jobs
|
||||
// --help, -h display this help and exit
|
||||
}
|
||||
|
||||
// This example shows the usage string generated by go-arg when using subcommands
|
||||
func Example_helpTextWithSubcommand() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --help")
|
||||
|
||||
type getCmd struct {
|
||||
Item string `arg:"positional" 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
|
||||
osExit = func(int) {}
|
||||
stdout = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
// Usage: example [--verbose] <command> [<args>]
|
||||
//
|
||||
// Options:
|
||||
// --verbose
|
||||
// --help, -h display this help and exit
|
||||
//
|
||||
// Commands:
|
||||
// get fetch an item and print it
|
||||
// list list available items
|
||||
}
|
||||
|
||||
// This example shows the usage string generated by go-arg when using subcommands
|
||||
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
|
||||
osExit = func(int) {}
|
||||
stdout = 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")
|
||||
|
||||
type getCmd struct {
|
||||
Item string `arg:"positional" 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
|
||||
osExit = func(int) {}
|
||||
stdout = os.Stdout
|
||||
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
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 list [--format FORMAT] [--limit LIMIT]
|
||||
//
|
||||
// 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
|
||||
osExit = func(int) {}
|
||||
stdout = os.Stdout
|
||||
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
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,required"`
|
||||
Output []string `arg:"positional"`
|
||||
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||
Dataset string `help:"dataset to use"`
|
||||
Optimize int `arg:"-O,help:optimization level"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
stderr = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
// Usage: example [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]]
|
||||
// error: error processing default value for --optimize: strconv.ParseInt: parsing "INVALID": invalid syntax
|
||||
}
|
||||
|
||||
// This example shows the error string generated by go-arg when an invalid option is provided
|
||||
func Example_errorTextForSubcommand() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example get --count INVALID")
|
||||
|
||||
type getCmd struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
var args struct {
|
||||
Get *getCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
stderr = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
// Usage: example get [--count COUNT]
|
||||
// error: error processing default value for --count: strconv.ParseInt: parsing "INVALID": invalid syntax
|
||||
}
|
||||
|
||||
// This example demonstrates use of subcommands
|
||||
func Example_subcommand() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example commit -a -m what-this-commit-is-about")
|
||||
|
||||
type CheckoutCmd struct {
|
||||
Branch string `arg:"positional"`
|
||||
Track bool `arg:"-t"`
|
||||
}
|
||||
type CommitCmd struct {
|
||||
All bool `arg:"-a"`
|
||||
Message string `arg:"-m"`
|
||||
}
|
||||
type PushCmd struct {
|
||||
Remote string `arg:"positional"`
|
||||
Branch string `arg:"positional"`
|
||||
SetUpstream bool `arg:"-u"`
|
||||
}
|
||||
var args struct {
|
||||
Checkout *CheckoutCmd `arg:"subcommand:checkout"`
|
||||
Commit *CommitCmd `arg:"subcommand:commit"`
|
||||
Push *PushCmd `arg:"subcommand:push"`
|
||||
Quiet bool `arg:"-q"` // this flag is global to all subcommands
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
stderr = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
switch {
|
||||
case args.Checkout != nil:
|
||||
fmt.Printf("checkout requested for branch %s\n", args.Checkout.Branch)
|
||||
case args.Commit != nil:
|
||||
fmt.Printf("commit requested with message \"%s\"\n", args.Commit.Message)
|
||||
case args.Push != nil:
|
||||
fmt.Printf("push requested from %s to %s\n", args.Push.Branch, args.Push.Remote)
|
||||
}
|
||||
|
||||
// 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:
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
module github.com/alexflint/go-arg/v2
|
||||
|
||||
require (
|
||||
github.com/alexflint/go-scalar v1.2.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
)
|
||||
|
||||
go 1.13
|
|
@ -0,0 +1,15 @@
|
|||
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
||||
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -0,0 +1,561 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
scalar "github.com/alexflint/go-scalar"
|
||||
)
|
||||
|
||||
// ErrHelp indicates that -h or --help were provided
|
||||
var ErrHelp = errors.New("help requested by user")
|
||||
|
||||
// ErrVersion indicates that --version was provided
|
||||
var ErrVersion = errors.New("version requested by user")
|
||||
|
||||
// MustParse processes command line arguments and exits upon failure
|
||||
func MustParse(dest interface{}) *Parser {
|
||||
p, err := NewParser(dest)
|
||||
if err != nil {
|
||||
fmt.Fprintln(stdout, err)
|
||||
osExit(-1)
|
||||
return nil // just in case osExit was monkey-patched
|
||||
}
|
||||
|
||||
err = p.Parse(os.Args, os.Environ())
|
||||
switch {
|
||||
case err == ErrHelp:
|
||||
p.writeHelpForSubcommand(stdout, p.leaf)
|
||||
osExit(0)
|
||||
case err == ErrVersion:
|
||||
fmt.Fprintln(stdout, p.version)
|
||||
osExit(0)
|
||||
case err != nil:
|
||||
p.failWithSubcommand(err.Error(), p.leaf)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Parse processes command line arguments and stores them in dest
|
||||
func Parse(dest interface{}, options ...ParserOption) error {
|
||||
p, err := NewParser(dest, options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.Parse(os.Args, os.Environ())
|
||||
}
|
||||
|
||||
// Parse processes the given command line option, storing the results in the field
|
||||
// of the structs from which NewParser was constructed
|
||||
func (p *Parser) Parse(args, env []string) error {
|
||||
p.seen = make(map[*Argument]bool)
|
||||
|
||||
// If -h or --help were specified then make sure help text supercedes other errors
|
||||
var help bool
|
||||
for _, arg := range args {
|
||||
if arg == "-h" || arg == "--help" {
|
||||
help = true
|
||||
}
|
||||
if arg == "--" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err := p.ProcessCommandLine(args)
|
||||
if err != nil {
|
||||
if help {
|
||||
return ErrHelp
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
err = p.ProcessEnvironment(env)
|
||||
if err != nil {
|
||||
if help {
|
||||
return ErrHelp
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
err = p.ProcessDefaults()
|
||||
if err != nil {
|
||||
if help {
|
||||
return ErrHelp
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Validate()
|
||||
}
|
||||
|
||||
// ProcessCommandLine scans arguments one-by-one, parses them and assigns
|
||||
// the result to fields of the struct passed to NewParser. It returns
|
||||
// an error if an argument is invalid or unknown, but not if a
|
||||
// required argument is missing. To check that all required arguments
|
||||
// are set, call Validate(). This function ignores the first element
|
||||
// of args, which is assumed to be the program name itself. This function
|
||||
// never overwrites arguments previously seen in a call to any Process*
|
||||
// function.
|
||||
func (p *Parser) ProcessCommandLine(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
positionals, err := p.ProcessOptions(args[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.ProcessPositionals(positionals)
|
||||
}
|
||||
|
||||
// OverwriteWithCommandLine is like ProcessCommandLine but it overwrites
|
||||
// any previously seen values.
|
||||
func (p *Parser) OverwriteWithCommandLine(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
positionals, err := p.OverwriteWithOptions(args[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.OverwriteWithPositionals(positionals)
|
||||
}
|
||||
|
||||
// ProcessOptions processes options but not positionals from the
|
||||
// command line. Positionals are returned and can be passed to
|
||||
// ProcessPositionals. Arguments seen in a previous call to any
|
||||
// Process* or OverwriteWith* functions are ignored.
|
||||
func (p *Parser) ProcessOptions(args []string) ([]string, error) {
|
||||
return p.processOptions(args, false)
|
||||
}
|
||||
|
||||
// OverwriteWithOptions is like ProcessOptions except previously seen
|
||||
// arguments are overwritten
|
||||
func (p *Parser) OverwriteWithOptions(args []string) ([]string, error) {
|
||||
return p.processOptions(args, true)
|
||||
}
|
||||
|
||||
func (p *Parser) processOptions(args []string, overwrite bool) ([]string, error) {
|
||||
// note that p.cmd.args has already been copied into p.accessible in NewParser
|
||||
|
||||
// union of args for the chain of subcommands encountered so far
|
||||
p.leaf = p.cmd
|
||||
var allpositional bool
|
||||
var positionals []string
|
||||
|
||||
// must use explicit for loop, not range, because we manipulate i inside the loop
|
||||
for i := 0; i < len(args); i++ {
|
||||
token := args[i]
|
||||
|
||||
// the "--" token indicates that all further tokens should be treated as positionals
|
||||
if token == "--" {
|
||||
allpositional = true
|
||||
continue
|
||||
}
|
||||
|
||||
// check whether this is a positional argument
|
||||
if !isFlag(token) || allpositional {
|
||||
// each subcommand can have either subcommands or positionals, but not both
|
||||
if len(p.leaf.subcommands) == 0 {
|
||||
positionals = append(positionals, token)
|
||||
continue
|
||||
}
|
||||
|
||||
// if we have a subcommand then make sure it is valid for the current context
|
||||
subcmd := findSubcommand(p.leaf.subcommands, token)
|
||||
if subcmd == nil {
|
||||
return nil, fmt.Errorf("invalid subcommand: %s", token)
|
||||
}
|
||||
|
||||
// instantiate the field to point to a new struct
|
||||
v := p.val(subcmd.dest)
|
||||
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
|
||||
p.accessible = append(p.accessible, subcmd.args...)
|
||||
p.leaf = subcmd
|
||||
continue
|
||||
}
|
||||
|
||||
// check for special --help and --version flags
|
||||
switch token {
|
||||
case "-h", "--help":
|
||||
return nil, ErrHelp
|
||||
case "--version":
|
||||
return nil, ErrVersion
|
||||
}
|
||||
|
||||
// check for an equals sign, as in "--foo=bar"
|
||||
var value string
|
||||
opt := strings.TrimLeft(token, "-")
|
||||
if pos := strings.Index(opt, "="); pos != -1 {
|
||||
value = opt[pos+1:]
|
||||
opt = opt[:pos]
|
||||
}
|
||||
|
||||
// look up the arg for this option (note that the "args" slice changes as
|
||||
// we expand subcommands so it is better not to use a map)
|
||||
arg := findOption(p.accessible, opt)
|
||||
if arg == nil {
|
||||
return nil, fmt.Errorf("unknown argument %s", token)
|
||||
}
|
||||
|
||||
// for the case of multiple values, consume tokens until next --option
|
||||
if arg.cardinality == multiple && !arg.separate {
|
||||
var values []string
|
||||
if value == "" {
|
||||
for i+1 < len(args) && !isFlag(args[i+1]) && args[i+1] != "--" {
|
||||
values = append(values, args[i+1])
|
||||
i++
|
||||
}
|
||||
} else {
|
||||
values = append(values, value)
|
||||
}
|
||||
|
||||
if err := p.processSequence(arg, values, overwrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 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 arg.cardinality == zero && value == "" {
|
||||
value = "true"
|
||||
}
|
||||
|
||||
// if we have something like "--foo" then the value is the next argument
|
||||
if value == "" {
|
||||
if i+1 == len(args) {
|
||||
return nil, fmt.Errorf("missing value for %s", token)
|
||||
}
|
||||
if isFlag(args[i+1]) {
|
||||
return nil, fmt.Errorf("missing value for %s", token)
|
||||
}
|
||||
value = args[i+1]
|
||||
i++
|
||||
}
|
||||
|
||||
// send the value to the argument
|
||||
if err := p.processScalar(arg, value, overwrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return positionals, nil
|
||||
}
|
||||
|
||||
// ProcessPositionals processes a list of positional arguments. If
|
||||
// this list contains tokens that begin with a hyphen they will still be
|
||||
// treated as positional arguments. Arguments seen in a previous call
|
||||
// to any Process* or OverwriteWith* functions are ignored.
|
||||
func (p *Parser) ProcessPositionals(positionals []string) error {
|
||||
return p.processPositionals(positionals, false)
|
||||
}
|
||||
|
||||
// OverwriteWithPositionals is like ProcessPositionals except previously
|
||||
// seen arguments are overwritten.
|
||||
func (p *Parser) OverwriteWithPositionals(positionals []string) error {
|
||||
return p.processPositionals(positionals, true)
|
||||
}
|
||||
|
||||
func (p *Parser) processPositionals(positionals []string, overwrite bool) error {
|
||||
for _, arg := range p.accessible {
|
||||
if !arg.positional {
|
||||
continue
|
||||
}
|
||||
if len(positionals) == 0 {
|
||||
break
|
||||
}
|
||||
if arg.cardinality == multiple {
|
||||
if err := p.processSequence(arg, positionals, overwrite); err != nil {
|
||||
return err
|
||||
}
|
||||
positionals = nil
|
||||
break
|
||||
}
|
||||
|
||||
if err := p.processScalar(arg, positionals[0], overwrite); err != nil {
|
||||
return err
|
||||
}
|
||||
positionals = positionals[1:]
|
||||
}
|
||||
|
||||
if len(positionals) > 0 {
|
||||
return fmt.Errorf("too many positional arguments at '%s'", positionals[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessEnvironment processes environment variables from a list of strings
|
||||
// of the form KEY=VALUE. You can pass in os.Environ(). It
|
||||
// does not overwrite any fields with values already populated.
|
||||
func (p *Parser) ProcessEnvironment(environ []string) error {
|
||||
return p.processEnvironment(environ, false)
|
||||
}
|
||||
|
||||
// OverwriteWithEnvironment processes environment variables from a list
|
||||
// of strings of the form "KEY=VALUE". Any existing values are overwritten.
|
||||
func (p *Parser) OverwriteWithEnvironment(environ []string) error {
|
||||
return p.processEnvironment(environ, true)
|
||||
}
|
||||
|
||||
// ProcessEnvironment processes environment variables from a list of strings
|
||||
// of the form KEY=VALUE. You can pass in os.Environ(). It
|
||||
// overwrites already-populated fields only if overwrite is true.
|
||||
func (p *Parser) processEnvironment(environ []string, overwrite bool) error {
|
||||
// parse the list of KEY=VAL strings in environ
|
||||
env := make(map[string]string)
|
||||
for _, s := range environ {
|
||||
if i := strings.Index(s, "="); i >= 0 {
|
||||
env[s[:i]] = s[i+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// process arguments one-by-one
|
||||
for _, arg := range p.accessible {
|
||||
if arg.env == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
value, found := env[arg.env]
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
if arg.cardinality == multiple && !arg.separate {
|
||||
// expect a CSV string in an environment
|
||||
// variable in the case of multiple values
|
||||
var values []string
|
||||
if len(strings.TrimSpace(value)) > 0 {
|
||||
var err error
|
||||
values, err = csv.NewReader(strings.NewReader(value)).Read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing CSV string from environment variable %s: %v", arg.env, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.processSequence(arg, values, overwrite); err != nil {
|
||||
return fmt.Errorf("error processing environment variable %s: %v", arg.env, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := p.processScalar(arg, value, overwrite); err != nil {
|
||||
return fmt.Errorf("error processing environment variable %s: %v", arg.env, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessDefaults assigns default values to all fields that have default values and
|
||||
// are not already populated.
|
||||
func (p *Parser) ProcessDefaults() error {
|
||||
return p.processDefaults(false)
|
||||
}
|
||||
|
||||
// OverwriteWithDefaults assigns default values to all fields that have default values,
|
||||
// overwriting any previous value
|
||||
func (p *Parser) OverwriteWithDefaults() error {
|
||||
return p.processDefaults(true)
|
||||
}
|
||||
|
||||
// processDefaults assigns default values to all fields in all expanded subcommands.
|
||||
// If overwrite is true then it overwrites existing values.
|
||||
func (p *Parser) processDefaults(overwrite bool) error {
|
||||
for _, arg := range p.accessible {
|
||||
if arg.defaultVal == "" {
|
||||
continue
|
||||
}
|
||||
if err := p.processScalar(arg, arg.defaultVal, overwrite); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processScalar parses a single argument, inserts it into the struct,
|
||||
// and marks the argument as "seen" (unless the argument has been seen
|
||||
// before and overwrite=false, in which case the value is ignored)
|
||||
func (p *Parser) processScalar(arg *Argument, value string, overwrite bool) error {
|
||||
if p.seen[arg] && !overwrite && !arg.separate {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := strings.ToLower(arg.field.Name)
|
||||
if arg.long != "" && !arg.positional {
|
||||
name = "--" + arg.long
|
||||
}
|
||||
|
||||
if arg.cardinality == multiple {
|
||||
err := appendToSliceOrMap(p.val(arg.dest), value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing default value for %s: %v", name, err)
|
||||
}
|
||||
} else {
|
||||
err := scalar.ParseValue(p.val(arg.dest), value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing default value for %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
p.seen[arg] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// processSequence parses a sequence argument, inserts it into the struct,
|
||||
// and marks the argument as "seen" (unless the argument has been seen
|
||||
// before and overwrite=false, in which case the value is ignored)
|
||||
func (p *Parser) processSequence(arg *Argument, values []string, overwrite bool) error {
|
||||
if p.seen[arg] && !overwrite && !arg.separate {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := strings.ToLower(arg.field.Name)
|
||||
if arg.long != "" && !arg.positional {
|
||||
name = "--" + arg.long
|
||||
}
|
||||
|
||||
if arg.cardinality != multiple {
|
||||
panic(fmt.Sprintf("processSequence called for argument %s which has cardinality %v", arg.field.Name, arg.cardinality))
|
||||
}
|
||||
|
||||
err := setSliceOrMap(p.val(arg.dest), values)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing default value for %s: %v", name, err)
|
||||
}
|
||||
|
||||
p.seen[arg] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Missing returns a list of required arguments that were not provided
|
||||
func (p *Parser) Missing() []*Argument {
|
||||
var missing []*Argument
|
||||
for _, arg := range p.accessible {
|
||||
if arg.required && !p.seen[arg] {
|
||||
missing = append(missing, arg)
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
// Validate returns an error if any required arguments were missing
|
||||
func (p *Parser) Validate() error {
|
||||
if missing := p.Missing(); len(missing) > 0 {
|
||||
name := strings.ToLower(missing[0].field.Name)
|
||||
if missing[0].long != "" && !missing[0].positional {
|
||||
name = "--" + missing[0].long
|
||||
}
|
||||
|
||||
if missing[0].env == "" {
|
||||
return fmt.Errorf("%s is required", name)
|
||||
}
|
||||
return fmt.Errorf("%s is required (or environment variable %s)", name, missing[0].env)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// val returns a reflect.Value corresponding to the current value for the
|
||||
// given path
|
||||
func (p *Parser) val(dest path) reflect.Value {
|
||||
v := p.root
|
||||
for _, field := range dest.fields {
|
||||
if v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return reflect.Value{}
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
v = v.FieldByIndex(field.Index)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// path represents a sequence of steps to find the output location for an
|
||||
// argument or subcommand in the final destination struct
|
||||
type path struct {
|
||||
fields []reflect.StructField // sequence of struct fields to traverse
|
||||
}
|
||||
|
||||
// String gets a string representation of the given path
|
||||
func (p path) String() string {
|
||||
s := "args"
|
||||
for _, f := range p.fields {
|
||||
s += "." + f.Name
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Child gets a new path representing a child of this path.
|
||||
func (p path) Child(f reflect.StructField) path {
|
||||
// copy the entire slice of fields to avoid possible slice overwrite
|
||||
subfields := make([]reflect.StructField, len(p.fields)+1)
|
||||
copy(subfields, p.fields)
|
||||
subfields[len(subfields)-1] = f
|
||||
return path{
|
||||
fields: subfields,
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func walkFieldsImpl(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool, path []int) {
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
field.Index = make([]int, len(path)+1)
|
||||
copy(field.Index, append(path, i))
|
||||
expand := visit(field, t)
|
||||
if expand && field.Type.Kind() == reflect.Struct {
|
||||
var subpath []int
|
||||
if field.Anonymous {
|
||||
subpath = append(path, i)
|
||||
}
|
||||
walkFieldsImpl(field.Type, visit, subpath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isFlag returns true if a token is a flag such as "-v" or "--user" but not "-" or "--"
|
||||
func isFlag(s string) bool {
|
||||
return strings.HasPrefix(s, "-") && strings.TrimLeft(s, "-") != ""
|
||||
}
|
||||
|
||||
// findOption finds an option from its name, or returns nil if no arg is found
|
||||
func findOption(args []*Argument, name string) *Argument {
|
||||
for _, arg := range args {
|
||||
if arg.positional {
|
||||
continue
|
||||
}
|
||||
if arg.long == name || arg.short == name {
|
||||
return arg
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findSubcommand finds a subcommand using its name, or returns nil if no subcommand is found
|
||||
func findSubcommand(cmds []*Command, name string) *Command {
|
||||
for _, cmd := range cmds {
|
||||
if cmd.name == name {
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,271 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// this file contains tests related to the precedence rules for:
|
||||
// ProcessCommandLine
|
||||
// ProcessOptions
|
||||
// ProcessPositionals
|
||||
// ProcessEnvironment
|
||||
// ProcessMap
|
||||
// ProcessSingle
|
||||
// ProcessSequence
|
||||
// OverwriteWithCommandLine
|
||||
// OverwriteWithOptions
|
||||
// OverwriteWithPositionals
|
||||
// OverwriteWithEnvironment
|
||||
// OverwriteWithMap
|
||||
//
|
||||
// The Process* functions should not overwrite fields that have
|
||||
// been previously populated, whereas the OverwriteWith* functions
|
||||
// should overwrite fields that have been previously populated.
|
||||
|
||||
// check that we can accumulate "separate" args across env, cmdline, map, and defaults
|
||||
|
||||
// check what happens if we have a required arg with a default value
|
||||
|
||||
// add more tests for combinations of separate and cardinality
|
||||
|
||||
// check what happens if we call ProcessCommandLine multiple times with different subcommands
|
||||
|
||||
func TestProcessOptions(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = p.ProcessOptions([]string{"program", "--arg=hello"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessOptionsDoesNotOverwrite(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessEnvironment([]string{"ARG=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = p.ProcessOptions([]string{"--arg=hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "123", args.Arg)
|
||||
}
|
||||
|
||||
func TestOverwriteWithOptions(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessEnvironment([]string{"ARG=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = p.OverwriteWithOptions([]string{"--arg=hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessPositionals(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"positional"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessPositionals([]string{"hello"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessPositionalsDoesNotOverwrite(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env,positional"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessEnvironment([]string{"ARG=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessPositionals([]string{"hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "123", args.Arg)
|
||||
}
|
||||
|
||||
func TestOverwriteWithPositionals(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env,positional"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessEnvironment([]string{"ARG=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.OverwriteWithPositionals([]string{"hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessCommandLine(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessCommandLine([]string{"program", "--arg=hello"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessCommandLineDoesNotOverwrite(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessEnvironment([]string{"ARG=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessCommandLine([]string{"program", "--arg=hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "123", args.Arg)
|
||||
}
|
||||
|
||||
func TestOverwriteWithCommandLine(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessEnvironment([]string{"ARG=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.OverwriteWithCommandLine([]string{"program", "--arg=hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessEnvironment(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessEnvironment([]string{"ARG=hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessEnvironmentDoesNotOverwrite(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = p.ProcessOptions([]string{"--arg=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessEnvironment([]string{"ARG=hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "123", args.Arg)
|
||||
}
|
||||
|
||||
func TestOverwriteWithEnvironment(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = p.ProcessOptions([]string{"--arg=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.OverwriteWithEnvironment([]string{"ARG=hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessDefaults(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `default:"hello"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessDefaults()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessDefaultsDoesNotOverwrite(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `default:"hello"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = p.ProcessOptions([]string{"--arg=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessDefaults()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "123", args.Arg)
|
||||
}
|
||||
|
||||
func TestOverwriteWithDefaults(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `default:"hello"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = p.ProcessOptions([]string{"--arg=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.OverwriteWithDefaults()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "hello", args.Arg)
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
scalar "github.com/alexflint/go-scalar"
|
||||
)
|
||||
|
||||
var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()
|
||||
|
||||
// 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
|
||||
|
||||
const (
|
||||
zero cardinality = iota
|
||||
one
|
||||
multiple
|
||||
unsupported
|
||||
)
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// 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 can be parsed from a single string
|
||||
func isBoolean(t reflect.Type) bool {
|
||||
switch {
|
||||
case t.Implements(textUnmarshalerType):
|
||||
return false
|
||||
case t.Kind() == reflect.Bool:
|
||||
return true
|
||||
case t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Bool:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 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.Slice || t.Kind() == reflect.Map {
|
||||
return v.IsNil()
|
||||
}
|
||||
if !t.Comparable() {
|
||||
return false
|
||||
}
|
||||
return v.Interface() == reflect.Zero(t).Interface()
|
||||
}
|
|
@ -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,218 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
scalar "github.com/alexflint/go-scalar"
|
||||
)
|
||||
|
||||
// setSliceOrMap parses a sequence of strings into a slice or map. The slice or
|
||||
// map is always cleared first.
|
||||
func setSliceOrMap(dest reflect.Value, values []string) 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)
|
||||
case reflect.Map:
|
||||
return setMap(dest, values)
|
||||
default:
|
||||
return fmt.Errorf("cannot insert multiple values into a %v", t)
|
||||
}
|
||||
}
|
||||
|
||||
// setSlice parses a sequence of strings and inserts them into a slice. The
|
||||
// slice is cleared first.
|
||||
func setSlice(dest reflect.Value, values []string) 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 !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.
|
||||
// The map is always cleared first.
|
||||
func setMap(dest reflect.Value, values []string) 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 !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
|
||||
}
|
||||
|
||||
// appendSliceOrMap parses a string and appends it to an existing slice or map.
|
||||
func appendToSliceOrMap(dest reflect.Value, value string) 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 appendToSlice(dest, value)
|
||||
case reflect.Map:
|
||||
return appendToMap(dest, value)
|
||||
default:
|
||||
return fmt.Errorf("cannot insert multiple values into a %v", t)
|
||||
}
|
||||
}
|
||||
|
||||
// appendSlice parses a string and appends the result into a slice.
|
||||
func appendToSlice(dest reflect.Value, s string) error {
|
||||
var ptr bool
|
||||
elem := dest.Type().Elem()
|
||||
if elem.Kind() == reflect.Ptr && !elem.Implements(textUnmarshalerType) {
|
||||
ptr = true
|
||||
elem = elem.Elem()
|
||||
}
|
||||
|
||||
// parse the value and append
|
||||
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
|
||||
}
|
||||
|
||||
// appendToMap parses a name=value string and inserts it into a map.
|
||||
// If clear is true then any values already in the map are removed.
|
||||
func appendToMap(dest reflect.Value, s string) 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()
|
||||
}
|
||||
|
||||
// allocate the map if it is not allocated
|
||||
if dest.IsNil() {
|
||||
dest.Set(reflect.MakeMap(dest.Type()))
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAppendToSlice(t *testing.T) {
|
||||
xs := []int{10}
|
||||
err := appendToSlice(reflect.ValueOf(&xs).Elem(), "3")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{10, 3}, xs)
|
||||
}
|
||||
|
||||
func TestSetSlice(t *testing.T) {
|
||||
xs := []int{100}
|
||||
entries := []string{"1", "2", "3"}
|
||||
err := setSlice(reflect.ValueOf(&xs).Elem(), entries)
|
||||
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)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetSlicePtr(t *testing.T) {
|
||||
var xs []*int
|
||||
entries := []string{"1", "2", "3"}
|
||||
err := setSlice(reflect.ValueOf(&xs).Elem(), entries)
|
||||
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)
|
||||
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 TestAppendToMap(t *testing.T) {
|
||||
m := map[string]int{"foo": 10}
|
||||
err := appendToMap(reflect.ValueOf(&m).Elem(), "a=1")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m, 2)
|
||||
assert.Equal(t, 1, m["a"])
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
assert.Error(t, err)
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package arg
|
||||
|
||||
// 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
|
||||
// was specified then it returns the top-level arguments struct. If
|
||||
// no command line arguments have been processed by this parser then it
|
||||
// returns nil.
|
||||
func (p *Parser) Subcommand() interface{} {
|
||||
if p.leaf == nil || p.leaf.parent == nil {
|
||||
return nil
|
||||
}
|
||||
return p.val(p.leaf.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.leaf == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// make a list of ancestor commands
|
||||
var ancestors []string
|
||||
cur := p.leaf
|
||||
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
|
||||
}
|
|
@ -0,0 +1,411 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// This file contains tests for parse.go but I decided to put them here
|
||||
// since that file is getting large
|
||||
|
||||
func TestSubcommandNotAPointer(t *testing.T) {
|
||||
var args struct {
|
||||
A string `arg:"subcommand"`
|
||||
}
|
||||
_, err := NewParser(&args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSubcommandNotAPointerToStruct(t *testing.T) {
|
||||
var args struct {
|
||||
A struct{} `arg:"subcommand"`
|
||||
}
|
||||
_, err := NewParser(&args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestPositionalAndSubcommandNotAllowed(t *testing.T) {
|
||||
var args struct {
|
||||
A string `arg:"positional"`
|
||||
B *struct{} `arg:"subcommand"`
|
||||
}
|
||||
_, err := NewParser(&args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMinimalSubcommand(t *testing.T) {
|
||||
type listCmd struct {
|
||||
}
|
||||
var args struct {
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
p, err := parse(&args, "list")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List, p.Subcommand())
|
||||
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(&args)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, p.Subcommand())
|
||||
assert.Nil(t, p.SubcommandNames())
|
||||
}
|
||||
|
||||
func TestNoSuchSubcommand(t *testing.T) {
|
||||
type listCmd struct {
|
||||
}
|
||||
var args struct {
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
_, err := parse(&args, "invalid")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNamedSubcommand(t *testing.T) {
|
||||
type listCmd struct {
|
||||
}
|
||||
var args struct {
|
||||
List *listCmd `arg:"subcommand:ls"`
|
||||
}
|
||||
p, err := parse(&args, "ls")
|
||||
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 {
|
||||
}
|
||||
var args struct {
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
p, err := parse(&args, "")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.List)
|
||||
assert.Nil(t, p.Subcommand())
|
||||
assert.Empty(t, p.SubcommandNames())
|
||||
}
|
||||
|
||||
func TestTwoSubcommands(t *testing.T) {
|
||||
type getCmd struct {
|
||||
}
|
||||
type listCmd struct {
|
||||
}
|
||||
var args struct {
|
||||
Get *getCmd `arg:"subcommand"`
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
p, err := parse(&args, "list")
|
||||
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{"list"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
func TestSubcommandsWithOptions(t *testing.T) {
|
||||
type getCmd struct {
|
||||
Name string
|
||||
}
|
||||
type listCmd struct {
|
||||
Limit int
|
||||
}
|
||||
type cmd struct {
|
||||
Verbose bool
|
||||
Get *getCmd `arg:"subcommand"`
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list --limit 3")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List.Limit, 3)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list --limit 3 --verbose")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List.Limit, 3)
|
||||
assert.True(t, args.Verbose)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list --verbose --limit 3")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List.Limit, 3)
|
||||
assert.True(t, args.Verbose)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "--verbose list --limit 3")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List.Limit, 3)
|
||||
assert.True(t, args.Verbose)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "get")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Nil(t, args.List)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "get --name test")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Nil(t, args.List)
|
||||
assert.Equal(t, args.Get.Name, "test")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
_, err := parse(&args, "list", "LIMIT=123")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.List)
|
||||
assert.Equal(t, 123, args.List.Limit)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list", "LIMIT=not_an_integer")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedSubcommands(t *testing.T) {
|
||||
type child struct{}
|
||||
type parent struct {
|
||||
Child *child `arg:"subcommand"`
|
||||
}
|
||||
type grandparent struct {
|
||||
Parent *parent `arg:"subcommand"`
|
||||
}
|
||||
type root struct {
|
||||
Grandparent *grandparent `arg:"subcommand"`
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := parse(&args, "grandparent parent child")
|
||||
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{"grandparent", "parent", "child"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := parse(&args, "grandparent parent")
|
||||
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", "parent"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := parse(&args, "grandparent")
|
||||
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 := parse(&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"`
|
||||
}
|
||||
type cmd struct {
|
||||
Format string
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "", args.List.Pattern)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list --format json")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "", args.List.Pattern)
|
||||
assert.Equal(t, "json", args.Format)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list somepattern")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "somepattern", args.List.Pattern)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list somepattern --format json")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "somepattern", args.List.Pattern)
|
||||
assert.Equal(t, "json", args.Format)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list --format json somepattern")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "somepattern", args.List.Pattern)
|
||||
assert.Equal(t, "json", args.Format)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "--format json list somepattern")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "somepattern", args.List.Pattern)
|
||||
assert.Equal(t, "json", args.Format)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "--format json")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.List)
|
||||
assert.Equal(t, "json", args.Format)
|
||||
}
|
||||
}
|
||||
func TestSubcommandsWithMultiplePositionals(t *testing.T) {
|
||||
type getCmd struct {
|
||||
Items []string `arg:"positional"`
|
||||
}
|
||||
type cmd struct {
|
||||
Limit int
|
||||
Get *getCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "get")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Empty(t, args.Get.Items)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "get --limit 5")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Empty(t, args.Get.Items)
|
||||
assert.Equal(t, 5, args.Limit)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "get item1")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Equal(t, []string{"item1"}, args.Get.Items)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "get item1 item2 item3")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Equal(t, []string{"item1", "item2", "item3"}, args.Get.Items)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "get item1 --limit 5 item2")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Equal(t, []string{"item1", "item2"}, args.Get.Items)
|
||||
assert.Equal(t, 5, args.Limit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValForNilStruct(t *testing.T) {
|
||||
type subcmd struct{}
|
||||
var cmd struct {
|
||||
Sub *subcmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&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())
|
||||
}
|
|
@ -0,0 +1,331 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// the width of the left column
|
||||
const colWidth = 25
|
||||
|
||||
// to allow monkey patching in tests
|
||||
var (
|
||||
stdout io.Writer = os.Stdout
|
||||
stderr io.Writer = os.Stderr
|
||||
osExit = os.Exit
|
||||
)
|
||||
|
||||
// Fail prints usage information to stderr and exits with non-zero status
|
||||
func (p *Parser) Fail(msg string) {
|
||||
p.failWithSubcommand(msg, p.cmd)
|
||||
}
|
||||
|
||||
// FailSubcommand prints usage information for a specified subcommand to stderr,
|
||||
// then exits with non-zero status. 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 {
|
||||
cmd, err := p.lookupCommand(subcommand...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.failWithSubcommand(msg, cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
// failWithSubcommand prints usage information for the given subcommand to stderr and exits with non-zero status
|
||||
func (p *Parser) failWithSubcommand(msg string, cmd *Command) {
|
||||
p.writeUsageForSubcommand(stderr, cmd)
|
||||
fmt.Fprintln(stderr, "error:", msg)
|
||||
osExit(-1)
|
||||
}
|
||||
|
||||
// WriteUsage writes usage information for the top-level command
|
||||
func (p *Parser) WriteUsage(w io.Writer) {
|
||||
p.writeUsageForSubcommand(w, p.cmd)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
p.writeUsageForSubcommand(w, cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeUsageForSubcommand writes usage information for the given subcommand
|
||||
func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *Command) {
|
||||
var positionals, longOptions, shortOptions []*Argument
|
||||
for _, arg := range cmd.args {
|
||||
switch {
|
||||
case arg.positional:
|
||||
positionals = append(positionals, arg)
|
||||
case arg.long != "":
|
||||
longOptions = append(longOptions, arg)
|
||||
case arg.short != "":
|
||||
shortOptions = append(shortOptions, arg)
|
||||
}
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
// write the option component of the usage message
|
||||
for _, arg := range shortOptions {
|
||||
// prefix with a space
|
||||
fmt.Fprint(w, " ")
|
||||
if !arg.required {
|
||||
fmt.Fprint(w, "[")
|
||||
}
|
||||
fmt.Fprint(w, synopsis(arg, "-"+arg.short))
|
||||
if !arg.required {
|
||||
fmt.Fprint(w, "]")
|
||||
}
|
||||
}
|
||||
|
||||
for _, arg := range longOptions {
|
||||
// prefix with a space
|
||||
fmt.Fprint(w, " ")
|
||||
if !arg.required {
|
||||
fmt.Fprint(w, "[")
|
||||
}
|
||||
fmt.Fprint(w, synopsis(arg, "--"+arg.long))
|
||||
if !arg.required {
|
||||
fmt.Fprint(w, "]")
|
||||
}
|
||||
}
|
||||
|
||||
// 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 _, arg := range positionals {
|
||||
fmt.Fprint(w, " ")
|
||||
if !arg.required {
|
||||
fmt.Fprint(w, "[")
|
||||
closeBrackets += 1
|
||||
}
|
||||
if arg.cardinality == multiple {
|
||||
fmt.Fprintf(w, "%s [%s ...]", arg.placeholder, arg.placeholder)
|
||||
} else {
|
||||
fmt.Fprint(w, arg.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 {
|
||||
fmt.Fprint(w, " <command> [<args>]")
|
||||
}
|
||||
|
||||
fmt.Fprint(w, "\n")
|
||||
}
|
||||
|
||||
func printTwoCols(w io.Writer, left, help string, defaultVal string, envVal string) {
|
||||
lhs := " " + left
|
||||
fmt.Fprint(w, lhs)
|
||||
if help != "" {
|
||||
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)
|
||||
}
|
||||
|
||||
bracketsContent := []string{}
|
||||
|
||||
if defaultVal != "" {
|
||||
bracketsContent = append(bracketsContent,
|
||||
fmt.Sprintf("default: %s", defaultVal),
|
||||
)
|
||||
}
|
||||
|
||||
if envVal != "" {
|
||||
bracketsContent = append(bracketsContent,
|
||||
fmt.Sprintf("env: %s", envVal),
|
||||
)
|
||||
}
|
||||
|
||||
if len(bracketsContent) > 0 {
|
||||
fmt.Fprintf(w, " [%s]", strings.Join(bracketsContent, ", "))
|
||||
}
|
||||
fmt.Fprint(w, "\n")
|
||||
}
|
||||
|
||||
// WriteHelp writes the usage string for the top-level command
|
||||
func (p *Parser) WriteHelp(w io.Writer) {
|
||||
p.writeHelpForSubcommand(w, p.cmd)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
p.writeHelpForSubcommand(w, cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeHelp writes the usage string for the given subcommand
|
||||
func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *Command) {
|
||||
var positionals, longOptions, shortOptions []*Argument
|
||||
for _, arg := range cmd.args {
|
||||
switch {
|
||||
case arg.positional:
|
||||
positionals = append(positionals, arg)
|
||||
case arg.long != "":
|
||||
longOptions = append(longOptions, arg)
|
||||
case arg.short != "":
|
||||
shortOptions = append(shortOptions, arg)
|
||||
}
|
||||
}
|
||||
|
||||
if p.prologue != "" {
|
||||
fmt.Fprintln(w, p.prologue)
|
||||
}
|
||||
p.writeUsageForSubcommand(w, cmd)
|
||||
|
||||
// write the list of positionals
|
||||
if len(positionals) > 0 {
|
||||
fmt.Fprint(w, "\nPositional arguments:\n")
|
||||
for _, arg := range positionals {
|
||||
printTwoCols(w, arg.placeholder, arg.help, "", "")
|
||||
}
|
||||
}
|
||||
|
||||
// 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 _, arg := range shortOptions {
|
||||
p.printOption(w, arg)
|
||||
}
|
||||
for _, arg := range longOptions {
|
||||
p.printOption(w, arg)
|
||||
}
|
||||
}
|
||||
|
||||
// obtain a flattened list of options from all ancestors
|
||||
var globals []*Argument
|
||||
ancestor := cmd.parent
|
||||
for ancestor != nil {
|
||||
globals = append(globals, ancestor.args...)
|
||||
ancestor = ancestor.parent
|
||||
}
|
||||
|
||||
// write the list of global options
|
||||
if len(globals) > 0 {
|
||||
fmt.Fprint(w, "\nGlobal options:\n")
|
||||
for _, arg := range globals {
|
||||
p.printOption(w, arg)
|
||||
}
|
||||
}
|
||||
|
||||
// write the list of built in options
|
||||
p.printOption(w, &Argument{
|
||||
cardinality: zero,
|
||||
long: "help",
|
||||
short: "h",
|
||||
help: "display this help and exit",
|
||||
})
|
||||
if p.version != "" {
|
||||
p.printOption(w, &Argument{
|
||||
cardinality: zero,
|
||||
long: "version",
|
||||
help: "display version and exit",
|
||||
})
|
||||
}
|
||||
|
||||
// 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, "", "")
|
||||
}
|
||||
}
|
||||
|
||||
if p.epilogue != "" {
|
||||
fmt.Fprintln(w, "\n"+p.epilogue)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) printOption(w io.Writer, arg *Argument) {
|
||||
ways := make([]string, 0, 2)
|
||||
if arg.long != "" {
|
||||
ways = append(ways, synopsis(arg, "--"+arg.long))
|
||||
}
|
||||
if arg.short != "" {
|
||||
ways = append(ways, synopsis(arg, "-"+arg.short))
|
||||
}
|
||||
if len(ways) > 0 {
|
||||
printTwoCols(w, strings.Join(ways, ", "), arg.help, arg.defaultVal, arg.env)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
var found *Command
|
||||
for _, child := range cmd.subcommands {
|
||||
if child.name == name {
|
||||
found = child
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
|
||||
}
|
||||
cmd = found
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func synopsis(arg *Argument, form string) string {
|
||||
if arg.cardinality == zero {
|
||||
return form
|
||||
}
|
||||
return form + " " + arg.placeholder
|
||||
}
|
|
@ -0,0 +1,607 @@
|
|||
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 ...]]"
|
||||
|
||||
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
|
||||
--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
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional,required"`
|
||||
Output []string `arg:"positional" help:"list of outputs"`
|
||||
Name string `help:"name to use" default:"Foo Bar"`
|
||||
Value int `help:"secret value" default:"42"`
|
||||
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"`
|
||||
}
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
os.Args[0] = "example"
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
type 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) {
|
||||
expectedUsage := "Usage: example [--label LABEL] [--content CONTENT]"
|
||||
|
||||
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 `default:"cat"`
|
||||
Content string `default:"dog"`
|
||||
}
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
args.Label = "should_ignore_this"
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestUsageLongPositionalWithHelp_legacyForm(t *testing.T) {
|
||||
expectedUsage := "Usage: example [VERYLONGPOSITIONALWITHHELP]"
|
||||
|
||||
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(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestUsageLongPositionalWithHelp_newForm(t *testing.T) {
|
||||
expectedUsage := "Usage: example [VERYLONGPOSITIONALWITHHELP]"
|
||||
|
||||
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(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestUsageWithProgramName(t *testing.T) {
|
||||
expectedUsage := "Usage: myprogram"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: myprogram
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
p, err := NewParser(&struct{}{}, WithProgramName("myprogram"))
|
||||
require.NoError(t, err)
|
||||
|
||||
os.Args[0] = "example"
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
type versioned struct{}
|
||||
|
||||
// Version returns the version for this program
|
||||
func (versioned) Version() string {
|
||||
return "example 3.2.1"
|
||||
}
|
||||
|
||||
func TestUsageWithVersion(t *testing.T) {
|
||||
expectedUsage := "example 3.2.1\nUsage: example"
|
||||
|
||||
expectedHelp := `
|
||||
example 3.2.1
|
||||
Usage: example
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
--version display version and exit
|
||||
`
|
||||
p, err := NewParser(&versioned{}, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
type 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) {
|
||||
expectedUsage := "Usage: example"
|
||||
|
||||
expectedHelp := `
|
||||
this program does this and that
|
||||
Usage: example
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
p, err := NewParser(&described{}, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
type epilogued struct{}
|
||||
|
||||
// Epilogued returns the epilogue for this program
|
||||
func (epilogued) Epilogue() string {
|
||||
return "For more information visit github.com/alexflint/go-arg"
|
||||
}
|
||||
|
||||
func TestUsageWithEpilogue(t *testing.T) {
|
||||
expectedUsage := "Usage: example"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
|
||||
For more information visit github.com/alexflint/go-arg
|
||||
`
|
||||
os.Args[0] = "example"
|
||||
p, err := NewParser(&epilogued{}, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestUsageForRequiredPositionals(t *testing.T) {
|
||||
expectedUsage := "Usage: example REQUIRED1 REQUIRED2\n"
|
||||
var args struct {
|
||||
Required1 string `arg:"positional,required"`
|
||||
Required2 string `arg:"positional,required"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, usage.String())
|
||||
}
|
||||
|
||||
func TestUsageForMixedPositionals(t *testing.T) {
|
||||
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]\n"
|
||||
var args struct {
|
||||
Required1 string `arg:"positional,required"`
|
||||
Required2 string `arg:"positional,required"`
|
||||
Optional1 string `arg:"positional"`
|
||||
Optional2 string `arg:"positional"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, usage.String())
|
||||
}
|
||||
|
||||
func TestUsageForRepeatedPositionals(t *testing.T) {
|
||||
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]\n"
|
||||
var args struct {
|
||||
Required1 string `arg:"positional,required"`
|
||||
Required2 string `arg:"positional,required"`
|
||||
Repeated []string `arg:"positional,required"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, usage.String())
|
||||
}
|
||||
|
||||
func TestUsageForMixedAndRepeatedPositionals(t *testing.T) {
|
||||
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2 [REPEATED [REPEATED ...]]]]\n"
|
||||
var args struct {
|
||||
Required1 string `arg:"positional,required"`
|
||||
Required2 string `arg:"positional,required"`
|
||||
Optional1 string `arg:"positional"`
|
||||
Optional2 string `arg:"positional"`
|
||||
Repeated []string `arg:"positional"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, usage.String())
|
||||
}
|
||||
|
||||
func TestRequiredMultiplePositionals(t *testing.T) {
|
||||
expectedUsage := "Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]\n"
|
||||
|
||||
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(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, usage.String())
|
||||
}
|
||||
|
||||
func TestUsageWithNestedSubcommands(t *testing.T) {
|
||||
expectedUsage := "Usage: example child nested [--enable] OUTPUT"
|
||||
|
||||
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(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
_ = p.Parse([]string{"child", "nested", "value"}, nil)
|
||||
|
||||
var help2 bytes.Buffer
|
||||
p.WriteHelpForSubcommand(&help2, "child", "nested")
|
||||
assert.Equal(t, expectedHelp[1:], help2.String())
|
||||
|
||||
var usage2 bytes.Buffer
|
||||
p.WriteUsageForSubcommand(&usage2, "child", "nested")
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String()))
|
||||
}
|
||||
|
||||
func TestNonexistentSubcommand(t *testing.T) {
|
||||
var args struct {
|
||||
sub *struct{} `arg:"subcommand"`
|
||||
}
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
var b bytes.Buffer
|
||||
|
||||
err = p.WriteUsageForSubcommand(&b, "does_not_exist")
|
||||
assert.Error(t, err)
|
||||
|
||||
err = p.WriteHelpForSubcommand(&b, "does_not_exist")
|
||||
assert.Error(t, err)
|
||||
|
||||
err = p.FailSubcommand("something went wrong", "does_not_exist")
|
||||
assert.Error(t, err)
|
||||
|
||||
err = p.WriteUsageForSubcommand(&b, "sub", "does_not_exist")
|
||||
assert.Error(t, err)
|
||||
|
||||
err = p.WriteHelpForSubcommand(&b, "sub", "does_not_exist")
|
||||
assert.Error(t, err)
|
||||
|
||||
err = p.FailSubcommand("something went wrong", "sub", "does_not_exist")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUsageWithoutLongNames(t *testing.T) {
|
||||
expectedUsage := "Usage: example [-a PLACEHOLDER] -b SHORTONLY2"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example [-a PLACEHOLDER] -b SHORTONLY2
|
||||
|
||||
Options:
|
||||
-a PLACEHOLDER some help [default: some val]
|
||||
-b SHORTONLY2 some help2
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
ShortOnly string `arg:"-a,--" help:"some help" default:"some val" placeholder:"PLACEHOLDER"`
|
||||
ShortOnly2 string `arg:"-b,--,required" help:"some help2"`
|
||||
}
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestUsageWithShortFirst(t *testing.T) {
|
||||
expectedUsage := "Usage: example [-c CAT] [--dog DOG]"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example [-c CAT] [--dog DOG]
|
||||
|
||||
Options:
|
||||
-c CAT
|
||||
--dog DOG
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
Dog string
|
||||
Cat string `arg:"-c,--"`
|
||||
}
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestUsageWithEnvOptions(t *testing.T) {
|
||||
expectedUsage := "Usage: example [-s SHORT]"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example [-s SHORT]
|
||||
|
||||
Options:
|
||||
-s SHORT [env: SHORT]
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
Short string `arg:"--,-s,env"`
|
||||
EnvOnly string `arg:"--,env"`
|
||||
EnvOnlyOverriden string `arg:"--,env:CUSTOM"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestFail(t *testing.T) {
|
||||
originalStderr := stderr
|
||||
originalExit := osExit
|
||||
defer func() {
|
||||
stderr = originalStderr
|
||||
osExit = originalExit
|
||||
}()
|
||||
|
||||
var b bytes.Buffer
|
||||
stderr = &b
|
||||
|
||||
var exitCode int
|
||||
osExit = func(code int) { exitCode = code }
|
||||
|
||||
expectedStdout := `
|
||||
Usage: example [--foo FOO]
|
||||
error: something went wrong
|
||||
`
|
||||
|
||||
var args struct {
|
||||
Foo int
|
||||
}
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
p.Fail("something went wrong")
|
||||
|
||||
assert.Equal(t, expectedStdout[1:], b.String())
|
||||
assert.Equal(t, -1, exitCode)
|
||||
}
|
||||
|
||||
func TestFailSubcommand(t *testing.T) {
|
||||
originalStderr := stderr
|
||||
originalExit := osExit
|
||||
defer func() {
|
||||
stderr = originalStderr
|
||||
osExit = originalExit
|
||||
}()
|
||||
|
||||
var b bytes.Buffer
|
||||
stderr = &b
|
||||
|
||||
var exitCode int
|
||||
osExit = func(code int) { exitCode = code }
|
||||
|
||||
expectedStdout := `
|
||||
Usage: example sub
|
||||
error: something went wrong
|
||||
`
|
||||
|
||||
var args struct {
|
||||
Sub *struct{} `arg:"subcommand"`
|
||||
}
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.FailSubcommand("something went wrong", "sub")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expectedStdout[1:], b.String())
|
||||
assert.Equal(t, -1, exitCode)
|
||||
}
|
Loading…
Reference in New Issue