Compare commits
No commits in common. "register" and "v1.4.0" have entirely different histories.
|
@ -15,17 +15,17 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
go: ['1.17', '1.18', '1.19']
|
go: ['1.13', '1.14', '1.15', '1.16']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: go
|
- id: go
|
||||||
name: Set up Go
|
name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v1
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build -v .
|
run: go build -v .
|
||||||
|
|
23
Makefile
23
Makefile
|
@ -1,23 +0,0 @@
|
||||||
# git remote add gitwit git@git.wit.org:wit/arg.git
|
|
||||||
|
|
||||||
redomod:
|
|
||||||
rm -f go.*
|
|
||||||
GO111MODULE= go mod init
|
|
||||||
GO111MODULE= go mod tidy
|
|
||||||
|
|
||||||
github:
|
|
||||||
git push origin register
|
|
||||||
git push origin devel
|
|
||||||
git push origin jcarr
|
|
||||||
git push origin --tags
|
|
||||||
# git push github register
|
|
||||||
# git push github devel
|
|
||||||
# git push github --tags
|
|
||||||
@echo
|
|
||||||
@echo check https://git.wit.org/wit/arg
|
|
||||||
@echo
|
|
||||||
|
|
||||||
# init-github:
|
|
||||||
# git push -u github master
|
|
||||||
# git push -u github devel
|
|
||||||
# git push github --tags
|
|
77
README.md
77
README.md
|
@ -169,35 +169,6 @@ arg.Foo = "abc"
|
||||||
arg.MustParse(&args)
|
arg.MustParse(&args)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Combining command line options, environment variables, and default values
|
|
||||||
|
|
||||||
You can combine command line arguments, environment variables, and default values. Command line arguments take precedence over environment variables, which take precedence over default values. This means that we check whether a certain option was provided on the command line, then if not, we check for an environment variable (only if an `env` tag was provided), then if none is found, we check for a `default` tag containing a default value.
|
|
||||||
|
|
||||||
```go
|
|
||||||
var args struct {
|
|
||||||
Test string `arg:"-t,env:TEST" default:"something"`
|
|
||||||
}
|
|
||||||
arg.MustParse(&args)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Ignoring environment variables and/or default values
|
|
||||||
|
|
||||||
The values in an existing structure can be kept in-tact by ignoring environment
|
|
||||||
variables and/or default values.
|
|
||||||
|
|
||||||
```go
|
|
||||||
var args struct {
|
|
||||||
Test string `arg:"-t,env:TEST" default:"something"`
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := arg.NewParser(arg.Config{
|
|
||||||
IgnoreEnv: true,
|
|
||||||
IgnoreDefault: true,
|
|
||||||
}, &args)
|
|
||||||
|
|
||||||
err = p.Parse(os.Args)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Arguments with multiple values
|
### Arguments with multiple values
|
||||||
```go
|
```go
|
||||||
var args struct {
|
var args struct {
|
||||||
|
@ -337,22 +308,6 @@ func main() {
|
||||||
|
|
||||||
As usual, any field tagged with `arg:"-"` is ignored.
|
As usual, any field tagged with `arg:"-"` is ignored.
|
||||||
|
|
||||||
### Supported types
|
|
||||||
|
|
||||||
The following types may be used as arguments:
|
|
||||||
- built-in integer types: `int, int8, int16, int32, int64, byte, rune`
|
|
||||||
- built-in floating point types: `float32, float64`
|
|
||||||
- strings
|
|
||||||
- booleans
|
|
||||||
- URLs represented as `url.URL`
|
|
||||||
- time durations represented as `time.Duration`
|
|
||||||
- email addresses represented as `mail.Address`
|
|
||||||
- MAC addresses represented as `net.HardwareAddr`
|
|
||||||
- pointers to any of the above
|
|
||||||
- slices of any of the above
|
|
||||||
- maps using any of the above as keys and values
|
|
||||||
- any type that implements `encoding.TextUnmarshaler`
|
|
||||||
|
|
||||||
### Custom parsing
|
### Custom parsing
|
||||||
|
|
||||||
Implement `encoding.TextUnmarshaler` to define your own parsing logic.
|
Implement `encoding.TextUnmarshaler` to define your own parsing logic.
|
||||||
|
@ -462,9 +417,6 @@ Options:
|
||||||
|
|
||||||
### Description strings
|
### Description strings
|
||||||
|
|
||||||
A descriptive message can be added at the top of the help text by implementing
|
|
||||||
a `Description` function that returns a string.
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type args struct {
|
type args struct {
|
||||||
Foo string
|
Foo string
|
||||||
|
@ -490,35 +442,6 @@ Options:
|
||||||
--help, -h display this help and exit
|
--help, -h display this help and exit
|
||||||
```
|
```
|
||||||
|
|
||||||
Similarly an epilogue can be added at the end of the help text by implementing
|
|
||||||
the `Epilogue` function.
|
|
||||||
|
|
||||||
```go
|
|
||||||
type args struct {
|
|
||||||
Foo string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (args) Epilogue() string {
|
|
||||||
return "For more information visit github.com/alexflint/go-arg"
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var args args
|
|
||||||
arg.MustParse(&args)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ ./example -h
|
|
||||||
Usage: example [--foo FOO]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--foo FOO
|
|
||||||
--help, -h display this help and exit
|
|
||||||
|
|
||||||
For more information visit github.com/alexflint/go-arg
|
|
||||||
```
|
|
||||||
|
|
||||||
### Subcommands
|
### Subcommands
|
||||||
|
|
||||||
*Introduced in version 1.1.0*
|
*Introduced in version 1.1.0*
|
||||||
|
|
205
example_test.go
205
example_test.go
|
@ -2,12 +2,8 @@ package arg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/mail"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func split(s string) []string {
|
func split(s string) []string {
|
||||||
|
@ -154,7 +150,7 @@ func Example_helpText() {
|
||||||
os.Args = split("./example --help")
|
os.Args = split("./example --help")
|
||||||
|
|
||||||
var args struct {
|
var args struct {
|
||||||
Input string `arg:"positional,required"`
|
Input string `arg:"positional"`
|
||||||
Output []string `arg:"positional"`
|
Output []string `arg:"positional"`
|
||||||
Verbose bool `arg:"-v" help:"verbosity level"`
|
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||||
Dataset string `help:"dataset to use"`
|
Dataset string `help:"dataset to use"`
|
||||||
|
@ -162,7 +158,8 @@ func Example_helpText() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is only necessary when running inside golang's runnable example harness
|
// This is only necessary when running inside golang's runnable example harness
|
||||||
mustParseExit = func(int) {}
|
osExit = func(int) {}
|
||||||
|
stdout = os.Stdout
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -187,14 +184,15 @@ func Example_helpPlaceholder() {
|
||||||
os.Args = split("./example --help")
|
os.Args = split("./example --help")
|
||||||
|
|
||||||
var args struct {
|
var args struct {
|
||||||
Input string `arg:"positional,required" placeholder:"SRC"`
|
Input string `arg:"positional" placeholder:"SRC"`
|
||||||
Output []string `arg:"positional" placeholder:"DST"`
|
Output []string `arg:"positional" placeholder:"DST"`
|
||||||
Optimize int `arg:"-O" help:"optimization level" placeholder:"LEVEL"`
|
Optimize int `arg:"-O" help:"optimization level" placeholder:"LEVEL"`
|
||||||
MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" placeholder:"N"`
|
MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" placeholder:"N"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is only necessary when running inside golang's runnable example harness
|
// This is only necessary when running inside golang's runnable example harness
|
||||||
mustParseExit = func(int) {}
|
osExit = func(int) {}
|
||||||
|
stdout = os.Stdout
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -234,7 +232,8 @@ func Example_helpTextWithSubcommand() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is only necessary when running inside golang's runnable example harness
|
// This is only necessary when running inside golang's runnable example harness
|
||||||
mustParseExit = func(int) {}
|
osExit = func(int) {}
|
||||||
|
stdout = os.Stdout
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -251,43 +250,7 @@ func Example_helpTextWithSubcommand() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// This example shows the usage string generated by go-arg when using subcommands
|
// This example shows the usage string generated by go-arg when using subcommands
|
||||||
func Example_helpTextWhenUsingSubcommand() {
|
func Example_helpTextForSubcommand() {
|
||||||
// These are the args you would pass in on the command line
|
|
||||||
os.Args = split("./example get --help")
|
|
||||||
|
|
||||||
type getCmd struct {
|
|
||||||
Item string `arg:"positional,required" help:"item to fetch"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type listCmd struct {
|
|
||||||
Format string `help:"output format"`
|
|
||||||
Limit int
|
|
||||||
}
|
|
||||||
|
|
||||||
var args struct {
|
|
||||||
Verbose bool
|
|
||||||
Get *getCmd `arg:"subcommand" help:"fetch an item and print it"`
|
|
||||||
List *listCmd `arg:"subcommand" help:"list available items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is only necessary when running inside golang's runnable example harness
|
|
||||||
mustParseExit = func(int) {}
|
|
||||||
|
|
||||||
MustParse(&args)
|
|
||||||
|
|
||||||
// output:
|
|
||||||
// Usage: example get ITEM
|
|
||||||
//
|
|
||||||
// Positional arguments:
|
|
||||||
// ITEM item to fetch
|
|
||||||
//
|
|
||||||
// Global options:
|
|
||||||
// --verbose
|
|
||||||
// --help, -h display this help and exit
|
|
||||||
}
|
|
||||||
|
|
||||||
// This example shows how to print help for an explicit subcommand
|
|
||||||
func Example_writeHelpForSubcommand() {
|
|
||||||
// These are the args you would pass in on the command line
|
// These are the args you would pass in on the command line
|
||||||
os.Args = split("./example get --help")
|
os.Args = split("./example get --help")
|
||||||
|
|
||||||
|
@ -307,83 +270,29 @@ func Example_writeHelpForSubcommand() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is only necessary when running inside golang's runnable example harness
|
// This is only necessary when running inside golang's runnable example harness
|
||||||
exit := func(int) {}
|
osExit = func(int) {}
|
||||||
|
stdout = os.Stdout
|
||||||
|
|
||||||
p, err := NewParser(Config{Exit: exit}, &args)
|
MustParse(&args)
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = p.WriteHelpForSubcommand(os.Stdout, "list")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// output:
|
// output:
|
||||||
// Usage: example list [--format FORMAT] [--limit LIMIT]
|
// Usage: example get ITEM
|
||||||
//
|
//
|
||||||
// Options:
|
// Positional arguments:
|
||||||
// --format FORMAT output format
|
// ITEM item to fetch
|
||||||
// --limit LIMIT
|
|
||||||
//
|
//
|
||||||
// Global options:
|
// Global options:
|
||||||
// --verbose
|
// --verbose
|
||||||
// --help, -h display this help and exit
|
// --help, -h display this help and exit
|
||||||
}
|
}
|
||||||
|
|
||||||
// This example shows how to print help for a subcommand that is nested several levels deep
|
|
||||||
func Example_writeHelpForSubcommandNested() {
|
|
||||||
// These are the args you would pass in on the command line
|
|
||||||
os.Args = split("./example get --help")
|
|
||||||
|
|
||||||
type mostNestedCmd struct {
|
|
||||||
Item string
|
|
||||||
}
|
|
||||||
|
|
||||||
type nestedCmd struct {
|
|
||||||
MostNested *mostNestedCmd `arg:"subcommand"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type topLevelCmd struct {
|
|
||||||
Nested *nestedCmd `arg:"subcommand"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var args struct {
|
|
||||||
TopLevel *topLevelCmd `arg:"subcommand"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is only necessary when running inside golang's runnable example harness
|
|
||||||
exit := func(int) {}
|
|
||||||
|
|
||||||
p, err := NewParser(Config{Exit: exit}, &args)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = p.WriteHelpForSubcommand(os.Stdout, "toplevel", "nested", "mostnested")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// output:
|
|
||||||
// Usage: example toplevel nested mostnested [--item ITEM]
|
|
||||||
//
|
|
||||||
// Options:
|
|
||||||
// --item ITEM
|
|
||||||
// --help, -h display this help and exit
|
|
||||||
}
|
|
||||||
|
|
||||||
// This example shows the error string generated by go-arg when an invalid option is provided
|
// This example shows the error string generated by go-arg when an invalid option is provided
|
||||||
func Example_errorText() {
|
func Example_errorText() {
|
||||||
// These are the args you would pass in on the command line
|
// These are the args you would pass in on the command line
|
||||||
os.Args = split("./example --optimize INVALID")
|
os.Args = split("./example --optimize INVALID")
|
||||||
|
|
||||||
var args struct {
|
var args struct {
|
||||||
Input string `arg:"positional,required"`
|
Input string `arg:"positional"`
|
||||||
Output []string `arg:"positional"`
|
Output []string `arg:"positional"`
|
||||||
Verbose bool `arg:"-v" help:"verbosity level"`
|
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||||
Dataset string `help:"dataset to use"`
|
Dataset string `help:"dataset to use"`
|
||||||
|
@ -391,7 +300,8 @@ func Example_errorText() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is only necessary when running inside golang's runnable example harness
|
// This is only necessary when running inside golang's runnable example harness
|
||||||
mustParseExit = func(int) {}
|
osExit = func(int) {}
|
||||||
|
stderr = os.Stdout
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -414,7 +324,8 @@ func Example_errorTextForSubcommand() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is only necessary when running inside golang's runnable example harness
|
// This is only necessary when running inside golang's runnable example harness
|
||||||
mustParseExit = func(int) {}
|
osExit = func(int) {}
|
||||||
|
stderr = os.Stdout
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -449,7 +360,8 @@ func Example_subcommand() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is only necessary when running inside golang's runnable example harness
|
// This is only necessary when running inside golang's runnable example harness
|
||||||
mustParseExit = func(int) {}
|
osExit = func(int) {}
|
||||||
|
stderr = os.Stdout
|
||||||
|
|
||||||
MustParse(&args)
|
MustParse(&args)
|
||||||
|
|
||||||
|
@ -465,76 +377,3 @@ func Example_subcommand() {
|
||||||
// output:
|
// output:
|
||||||
// commit requested with message "what-this-commit-is-about"
|
// commit requested with message "what-this-commit-is-about"
|
||||||
}
|
}
|
||||||
|
|
||||||
func Example_allSupportedTypes() {
|
|
||||||
// These are the args you would pass in on the command line
|
|
||||||
os.Args = []string{}
|
|
||||||
|
|
||||||
var args struct {
|
|
||||||
Bool bool
|
|
||||||
Byte byte
|
|
||||||
Rune rune
|
|
||||||
Int int
|
|
||||||
Int8 int8
|
|
||||||
Int16 int16
|
|
||||||
Int32 int32
|
|
||||||
Int64 int64
|
|
||||||
Float32 float32
|
|
||||||
Float64 float64
|
|
||||||
String string
|
|
||||||
Duration time.Duration
|
|
||||||
URL url.URL
|
|
||||||
Email mail.Address
|
|
||||||
MAC net.HardwareAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
// go-arg supports each of the types above, as well as pointers to any of
|
|
||||||
// the above and slices of any of the above. It also supports any types that
|
|
||||||
// implements encoding.TextUnmarshaler.
|
|
||||||
|
|
||||||
MustParse(&args)
|
|
||||||
|
|
||||||
// output:
|
|
||||||
}
|
|
||||||
|
|
||||||
func Example_envVarOnly() {
|
|
||||||
os.Args = split("./example")
|
|
||||||
_ = os.Setenv("AUTH_KEY", "my_key")
|
|
||||||
|
|
||||||
defer os.Unsetenv("AUTH_KEY")
|
|
||||||
|
|
||||||
var args struct {
|
|
||||||
AuthKey string `arg:"--,env:AUTH_KEY"`
|
|
||||||
}
|
|
||||||
|
|
||||||
MustParse(&args)
|
|
||||||
|
|
||||||
fmt.Println(args.AuthKey)
|
|
||||||
// output: my_key
|
|
||||||
}
|
|
||||||
|
|
||||||
func Example_envVarOnlyShouldIgnoreFlag() {
|
|
||||||
os.Args = split("./example --=my_key")
|
|
||||||
|
|
||||||
var args struct {
|
|
||||||
AuthKey string `arg:"--,env:AUTH_KEY"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Parse(&args)
|
|
||||||
|
|
||||||
fmt.Println(err)
|
|
||||||
// output: unknown argument --=my_key
|
|
||||||
}
|
|
||||||
|
|
||||||
func Example_envVarOnlyShouldIgnoreShortFlag() {
|
|
||||||
os.Args = split("./example -=my_key")
|
|
||||||
|
|
||||||
var args struct {
|
|
||||||
AuthKey string `arg:"--,env:AUTH_KEY"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Parse(&args)
|
|
||||||
|
|
||||||
fmt.Println(err)
|
|
||||||
// output: unknown argument -=my_key
|
|
||||||
}
|
|
||||||
|
|
14
go.mod
14
go.mod
|
@ -1,14 +1,8 @@
|
||||||
module go.wit.com/dev/alexflint/go-arg
|
module github.com/alexflint/go-arg
|
||||||
|
|
||||||
go 1.21.4
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexflint/go-scalar v1.2.0
|
github.com/alexflint/go-scalar v1.0.0
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.2.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
go 1.13
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
|
||||||
|
|
11
go.sum
11
go.sum
|
@ -1,13 +1,8 @@
|
||||||
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi+A70=
|
||||||
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
|
||||||
github.com/stretchr/testify v1.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=
|
|
||||||
|
|
341
parse.go
341
parse.go
|
@ -5,7 +5,6 @@ import (
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
@ -44,25 +43,23 @@ func (p path) Child(f reflect.StructField) path {
|
||||||
|
|
||||||
// spec represents a command line option
|
// spec represents a command line option
|
||||||
type spec struct {
|
type spec struct {
|
||||||
dest path
|
dest path
|
||||||
field reflect.StructField // the struct field from which this option was created
|
field reflect.StructField // the struct field from which this option was created
|
||||||
long string // the --long form for this option, or empty if none
|
long string // the --long form for this option, or empty if none
|
||||||
short string // the -s short form for this option, or empty if none
|
short string // the -s short form for this option, or empty if none
|
||||||
cardinality cardinality // determines how many tokens will be present (possible values: zero, one, multiple)
|
cardinality cardinality // determines how many tokens will be present (possible values: zero, one, multiple)
|
||||||
required bool // if true, this option must be present on the command line
|
required bool // if true, this option must be present on the command line
|
||||||
positional bool // if true, this option will be looked for in the positional flags
|
positional bool // if true, this option will be looked for in the positional flags
|
||||||
separate bool // if true, each slice and map entry will have its own --flag
|
separate bool // if true, each slice and map entry will have its own --flag
|
||||||
help string // the help text for this option
|
help string // the help text for this option
|
||||||
env string // the name of the environment variable for this option, or empty for none
|
env string // the name of the environment variable for this option, or empty for none
|
||||||
defaultValue reflect.Value // default value for this option
|
defaultVal string // default value for this option
|
||||||
defaultString string // default value for this option, in string form to be displayed in help text
|
placeholder string // name of the data in help
|
||||||
placeholder string // name of the data in help
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// command represents a named subcommand, or the top-level command
|
// command represents a named subcommand, or the top-level command
|
||||||
type command struct {
|
type command struct {
|
||||||
name string
|
name string
|
||||||
aliases []string
|
|
||||||
help string
|
help string
|
||||||
dest path
|
dest path
|
||||||
specs []*spec
|
specs []*spec
|
||||||
|
@ -70,55 +67,33 @@ type command struct {
|
||||||
parent *command
|
parent *command
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrHelp indicates that the builtin -h or --help were provided
|
// ErrHelp indicates that -h or --help were provided
|
||||||
var ErrHelp = errors.New("help requested by user")
|
var ErrHelp = errors.New("help requested by user")
|
||||||
|
|
||||||
// ErrVersion indicates that the builtin --version was provided
|
// ErrVersion indicates that --version was provided
|
||||||
var ErrVersion = errors.New("version requested by user")
|
var ErrVersion = errors.New("version requested by user")
|
||||||
|
|
||||||
// for monkey patching in example code
|
|
||||||
var mustParseExit = os.Exit
|
|
||||||
|
|
||||||
// This stores the args sent from modules
|
|
||||||
var register []interface{}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Use this in your packages to register
|
|
||||||
variables with go-arg. Then add this to your init()
|
|
||||||
|
|
||||||
package 'foo'
|
|
||||||
function init() {
|
|
||||||
args.Register(&argsFoo)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
func Register(dest ...interface{}) {
|
|
||||||
register = append(register, dest...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustParse processes command line arguments and exits upon failure
|
// MustParse processes command line arguments and exits upon failure
|
||||||
func MustParse(dest ...interface{}) *Parser {
|
func MustParse(dest ...interface{}) *Parser {
|
||||||
register = append(register, dest...)
|
p, err := NewParser(Config{}, dest...)
|
||||||
return mustParse(Config{Exit: mustParseExit}, register...)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// mustParse is a helper that facilitates testing
|
|
||||||
func mustParse(config Config, dest ...interface{}) *Parser {
|
|
||||||
if config.Exit == nil {
|
|
||||||
config.Exit = os.Exit
|
|
||||||
}
|
|
||||||
if config.Out == nil {
|
|
||||||
config.Out = os.Stdout
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := NewParser(config, dest...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(config.Out, err)
|
fmt.Fprintln(stdout, err)
|
||||||
config.Exit(-1)
|
osExit(-1)
|
||||||
return nil
|
return nil // just in case osExit was monkey-patched
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.Parse(flags())
|
||||||
|
switch {
|
||||||
|
case err == ErrHelp:
|
||||||
|
p.writeHelpForCommand(stdout, p.lastCmd)
|
||||||
|
osExit(0)
|
||||||
|
case err == ErrVersion:
|
||||||
|
fmt.Fprintln(stdout, p.version)
|
||||||
|
osExit(0)
|
||||||
|
case err != nil:
|
||||||
|
p.failWithCommand(err.Error(), p.lastCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.MustParse(flags())
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,20 +121,6 @@ type Config struct {
|
||||||
|
|
||||||
// IgnoreEnv instructs the library not to read environment variables
|
// IgnoreEnv instructs the library not to read environment variables
|
||||||
IgnoreEnv bool
|
IgnoreEnv bool
|
||||||
|
|
||||||
// IgnoreDefault instructs the library not to reset the variables to the
|
|
||||||
// default values, including pointers to sub commands
|
|
||||||
IgnoreDefault bool
|
|
||||||
|
|
||||||
// StrictSubcommands intructs the library not to allow global commands after
|
|
||||||
// subcommand
|
|
||||||
StrictSubcommands bool
|
|
||||||
|
|
||||||
// Exit is called to terminate the process with an error code (defaults to os.Exit)
|
|
||||||
Exit func(int)
|
|
||||||
|
|
||||||
// Out is where help text, usage text, and failure messages are printed (defaults to os.Stdout)
|
|
||||||
Out io.Writer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parser represents a set of command line options with destination values
|
// Parser represents a set of command line options with destination values
|
||||||
|
@ -169,10 +130,9 @@ type Parser struct {
|
||||||
config Config
|
config Config
|
||||||
version string
|
version string
|
||||||
description string
|
description string
|
||||||
epilogue string
|
|
||||||
|
|
||||||
// the following field changes during processing of command line arguments
|
// the following field changes during processing of command line arguments
|
||||||
subcommand []string
|
lastCmd *command
|
||||||
}
|
}
|
||||||
|
|
||||||
// Versioned is the interface that the destination struct should implement to
|
// Versioned is the interface that the destination struct should implement to
|
||||||
|
@ -191,14 +151,6 @@ type Described interface {
|
||||||
Description() string
|
Description() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Epilogued is the interface that the destination struct should implement to
|
|
||||||
// add an epilogue string at the bottom of the help message.
|
|
||||||
type Epilogued interface {
|
|
||||||
// Epilogue returns the string that will be printed on a line by itself
|
|
||||||
// at the end of the help message.
|
|
||||||
Epilogue() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// walkFields calls a function for each field of a struct, recursively expanding struct fields.
|
// walkFields calls a function for each field of a struct, recursively expanding struct fields.
|
||||||
func walkFields(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool) {
|
func walkFields(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool) {
|
||||||
walkFieldsImpl(t, visit, nil)
|
walkFieldsImpl(t, visit, nil)
|
||||||
|
@ -222,14 +174,6 @@ func walkFieldsImpl(t reflect.Type, visit func(field reflect.StructField, owner
|
||||||
|
|
||||||
// NewParser constructs a parser from a list of destination structs
|
// NewParser constructs a parser from a list of destination structs
|
||||||
func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
||||||
// fill in defaults
|
|
||||||
if config.Exit == nil {
|
|
||||||
config.Exit = os.Exit
|
|
||||||
}
|
|
||||||
if config.Out == nil {
|
|
||||||
config.Out = os.Stdout
|
|
||||||
}
|
|
||||||
|
|
||||||
// first pick a name for the command for use in the usage text
|
// first pick a name for the command for use in the usage text
|
||||||
var name string
|
var name string
|
||||||
switch {
|
switch {
|
||||||
|
@ -264,31 +208,18 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// for backwards compatibility, add nonzero field values as defaults
|
// add nonzero field values as defaults
|
||||||
// this applies only to the top-level command, not to subcommands (this inconsistency
|
|
||||||
// is the reason that this method for setting default values was deprecated)
|
|
||||||
for _, spec := range cmd.specs {
|
for _, spec := range cmd.specs {
|
||||||
// get the value
|
if v := p.val(spec.dest); v.IsValid() && !isZero(v) {
|
||||||
v := p.val(spec.dest)
|
if defaultVal, ok := v.Interface().(encoding.TextMarshaler); ok {
|
||||||
|
str, err := defaultVal.MarshalText()
|
||||||
// if the value is the "zero value" (e.g. nil pointer, empty struct) then ignore
|
if err != nil {
|
||||||
if isZero(v) {
|
return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
|
||||||
continue
|
}
|
||||||
}
|
spec.defaultVal = string(str)
|
||||||
|
} else {
|
||||||
// store as a default
|
spec.defaultVal = fmt.Sprintf("%v", v)
|
||||||
spec.defaultValue = v
|
|
||||||
|
|
||||||
// we need a string to display in help text
|
|
||||||
// if MarshalText is implemented then use that
|
|
||||||
if m, ok := v.Interface().(encoding.TextMarshaler); ok {
|
|
||||||
s, err := m.MarshalText()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
|
|
||||||
}
|
}
|
||||||
spec.defaultString = string(s)
|
|
||||||
} else {
|
|
||||||
spec.defaultString = fmt.Sprintf("%v", v)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,9 +232,6 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
||||||
if dest, ok := dest.(Described); ok {
|
if dest, ok := dest.(Described); ok {
|
||||||
p.description = dest.Description()
|
p.description = dest.Description()
|
||||||
}
|
}
|
||||||
if dest, ok := dest.(Epilogued); ok {
|
|
||||||
p.epilogue = dest.Epilogue()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &p, nil
|
return &p, nil
|
||||||
|
@ -329,24 +257,17 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
||||||
|
|
||||||
var errs []string
|
var errs []string
|
||||||
walkFields(t, func(field reflect.StructField, t reflect.Type) bool {
|
walkFields(t, func(field reflect.StructField, t reflect.Type) bool {
|
||||||
// check for the ignore switch in the tag
|
// Check for the ignore switch in the tag
|
||||||
tag := field.Tag.Get("arg")
|
tag := field.Tag.Get("arg")
|
||||||
if tag == "-" {
|
if tag == "-" || !isExported(field.Name) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// if this is an embedded struct then recurse into its fields, even if
|
// If this is an embedded struct then recurse into its fields
|
||||||
// it is unexported, because exported fields on unexported embedded
|
|
||||||
// structs are still writable
|
|
||||||
if field.Anonymous && field.Type.Kind() == reflect.Struct {
|
if field.Anonymous && field.Type.Kind() == reflect.Struct {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore any other unexported field
|
|
||||||
if !isExported(field.Name) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// duplicate the entire path to avoid slice overwrites
|
// duplicate the entire path to avoid slice overwrites
|
||||||
subdest := dest.Child(field)
|
subdest := dest.Child(field)
|
||||||
spec := spec{
|
spec := spec{
|
||||||
|
@ -360,9 +281,13 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
||||||
spec.help = help
|
spec.help = help
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultVal, hasDefault := field.Tag.Lookup("default")
|
||||||
|
if hasDefault {
|
||||||
|
spec.defaultVal = defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
// Look at the tag
|
// Look at the tag
|
||||||
var isSubcommand bool // tracks whether this field is a subcommand
|
var isSubcommand bool // tracks whether this field is a subcommand
|
||||||
|
|
||||||
for _, key := range strings.Split(tag, ",") {
|
for _, key := range strings.Split(tag, ",") {
|
||||||
if key == "" {
|
if key == "" {
|
||||||
continue
|
continue
|
||||||
|
@ -380,13 +305,18 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
||||||
case strings.HasPrefix(key, "--"):
|
case strings.HasPrefix(key, "--"):
|
||||||
spec.long = key[2:]
|
spec.long = key[2:]
|
||||||
case strings.HasPrefix(key, "-"):
|
case strings.HasPrefix(key, "-"):
|
||||||
if len(key) > 2 {
|
if len(key) != 2 {
|
||||||
errs = append(errs, fmt.Sprintf("%s.%s: short arguments must be one character only",
|
errs = append(errs, fmt.Sprintf("%s.%s: short arguments must be one character only",
|
||||||
t.Name(), field.Name))
|
t.Name(), field.Name))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
spec.short = key[1:]
|
spec.short = key[1:]
|
||||||
case key == "required":
|
case key == "required":
|
||||||
|
if hasDefault {
|
||||||
|
errs = append(errs, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified",
|
||||||
|
t.Name(), field.Name))
|
||||||
|
return false
|
||||||
|
}
|
||||||
spec.required = true
|
spec.required = true
|
||||||
case key == "positional":
|
case key == "positional":
|
||||||
spec.positional = true
|
spec.positional = true
|
||||||
|
@ -403,24 +333,18 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
||||||
}
|
}
|
||||||
case key == "subcommand":
|
case key == "subcommand":
|
||||||
// decide on a name for the subcommand
|
// decide on a name for the subcommand
|
||||||
var cmdnames []string
|
cmdname := value
|
||||||
if value == "" {
|
if cmdname == "" {
|
||||||
cmdnames = []string{strings.ToLower(field.Name)}
|
cmdname = strings.ToLower(field.Name)
|
||||||
} else {
|
|
||||||
cmdnames = strings.Split(value, "|")
|
|
||||||
}
|
|
||||||
for i := range cmdnames {
|
|
||||||
cmdnames[i] = strings.TrimSpace(cmdnames[i])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse the subcommand recursively
|
// parse the subcommand recursively
|
||||||
subcmd, err := cmdFromStruct(cmdnames[0], subdest, field.Type)
|
subcmd, err := cmdFromStruct(cmdname, subdest, field.Type)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err.Error())
|
errs = append(errs, err.Error())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
subcmd.aliases = cmdnames[1:]
|
|
||||||
subcmd.parent = &cmd
|
subcmd.parent = &cmd
|
||||||
subcmd.help = field.Tag.Get("help")
|
subcmd.help = field.Tag.Get("help")
|
||||||
|
|
||||||
|
@ -441,60 +365,27 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
||||||
spec.placeholder = strings.ToUpper(spec.field.Name)
|
spec.placeholder = strings.ToUpper(spec.field.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if this is a subcommand then we've done everything we need to do
|
// Check whether this field is supported. It's good to do this here rather than
|
||||||
if isSubcommand {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// check whether this field is supported. It's good to do this here rather than
|
|
||||||
// wait until ParseValue because it means that a program with invalid argument
|
// wait until ParseValue because it means that a program with invalid argument
|
||||||
// fields will always fail regardless of whether the arguments it received
|
// fields will always fail regardless of whether the arguments it received
|
||||||
// exercised those fields.
|
// exercised those fields.
|
||||||
var err error
|
if !isSubcommand {
|
||||||
spec.cardinality, err = cardinalityOf(field.Type)
|
cmd.specs = append(cmd.specs, &spec)
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported",
|
|
||||||
t.Name(), field.Name, field.Type.String()))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultString, hasDefault := field.Tag.Lookup("default")
|
var err error
|
||||||
if hasDefault {
|
spec.cardinality, err = cardinalityOf(field.Type)
|
||||||
// we do not support default values for maps and slices
|
if err != nil {
|
||||||
if spec.cardinality == multiple {
|
errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported",
|
||||||
|
t.Name(), field.Name, field.Type.String()))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if spec.cardinality == multiple && hasDefault {
|
||||||
errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields",
|
errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields",
|
||||||
t.Name(), field.Name))
|
t.Name(), field.Name))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// a required field cannot also have a default value
|
|
||||||
if spec.required {
|
|
||||||
errs = append(errs, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified",
|
|
||||||
t.Name(), field.Name))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse the default value
|
|
||||||
spec.defaultString = defaultString
|
|
||||||
if field.Type.Kind() == reflect.Ptr {
|
|
||||||
// here we have a field of type *T and we create a new T, no need to dereference
|
|
||||||
// in order for the value to be settable
|
|
||||||
spec.defaultValue = reflect.New(field.Type.Elem())
|
|
||||||
} else {
|
|
||||||
// here we have a field of type T and we create a new T and then dereference it
|
|
||||||
// so that the resulting value is settable
|
|
||||||
spec.defaultValue = reflect.New(field.Type).Elem()
|
|
||||||
}
|
|
||||||
err := scalar.ParseValue(spec.defaultValue, defaultString)
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, fmt.Sprintf("%s.%s: error processing default value: %v", t.Name(), field.Name, err))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the spec to the list of specs
|
|
||||||
cmd.specs = append(cmd.specs, &spec)
|
|
||||||
|
|
||||||
// if this was an embedded field then we already returned true up above
|
// if this was an embedded field then we already returned true up above
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
@ -535,20 +426,6 @@ func (p *Parser) Parse(args []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) MustParse(args []string) {
|
|
||||||
err := p.Parse(args)
|
|
||||||
switch {
|
|
||||||
case err == ErrHelp:
|
|
||||||
p.WriteHelpForSubcommand(p.config.Out, p.subcommand...)
|
|
||||||
p.config.Exit(0)
|
|
||||||
case err == ErrVersion:
|
|
||||||
fmt.Fprintln(p.config.Out, p.version)
|
|
||||||
p.config.Exit(0)
|
|
||||||
case err != nil:
|
|
||||||
p.FailSubcommand(err.Error(), p.subcommand...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// process environment vars for the given arguments
|
// process environment vars for the given arguments
|
||||||
func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error {
|
func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error {
|
||||||
for _, spec := range specs {
|
for _, spec := range specs {
|
||||||
|
@ -564,17 +441,13 @@ func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error
|
||||||
if spec.cardinality == multiple {
|
if spec.cardinality == multiple {
|
||||||
// expect a CSV string in an environment
|
// expect a CSV string in an environment
|
||||||
// variable in the case of multiple values
|
// variable in the case of multiple values
|
||||||
var values []string
|
values, err := csv.NewReader(strings.NewReader(value)).Read()
|
||||||
var err error
|
if err != nil {
|
||||||
if len(strings.TrimSpace(value)) > 0 {
|
return fmt.Errorf(
|
||||||
values, err = csv.NewReader(strings.NewReader(value)).Read()
|
"error reading a CSV string from environment variable %s with multiple values: %v",
|
||||||
if err != nil {
|
spec.env,
|
||||||
return fmt.Errorf(
|
err,
|
||||||
"error reading a CSV string from environment variable %s with multiple values: %v",
|
)
|
||||||
spec.env,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if err = setSliceOrMap(p.val(spec.dest), values, !spec.separate); err != nil {
|
if err = setSliceOrMap(p.val(spec.dest), values, !spec.separate); err != nil {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
|
@ -602,7 +475,7 @@ func (p *Parser) process(args []string) error {
|
||||||
|
|
||||||
// union of specs for the chain of subcommands encountered so far
|
// union of specs for the chain of subcommands encountered so far
|
||||||
curCmd := p.cmd
|
curCmd := p.cmd
|
||||||
p.subcommand = nil
|
p.lastCmd = curCmd
|
||||||
|
|
||||||
// make a copy of the specs because we will add to this list each time we expand a subcommand
|
// make a copy of the specs because we will add to this list each time we expand a subcommand
|
||||||
specs := make([]*spec, len(curCmd.specs))
|
specs := make([]*spec, len(curCmd.specs))
|
||||||
|
@ -616,15 +489,6 @@ func (p *Parser) process(args []string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// determine if the current command has a version option spec
|
|
||||||
var hasVersionOption bool
|
|
||||||
for _, spec := range curCmd.specs {
|
|
||||||
if spec.long == "version" {
|
|
||||||
hasVersionOption = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// process each string from the command line
|
// process each string from the command line
|
||||||
var allpositional bool
|
var allpositional bool
|
||||||
var positionals []string
|
var positionals []string
|
||||||
|
@ -652,17 +516,10 @@ func (p *Parser) process(args []string) error {
|
||||||
|
|
||||||
// instantiate the field to point to a new struct
|
// instantiate the field to point to a new struct
|
||||||
v := p.val(subcmd.dest)
|
v := p.val(subcmd.dest)
|
||||||
if v.IsNil() {
|
v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers
|
||||||
v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the new options to the set of allowed options
|
// add the new options to the set of allowed options
|
||||||
if p.config.StrictSubcommands {
|
specs = append(specs, subcmd.specs...)
|
||||||
specs = make([]*spec, len(subcmd.specs))
|
|
||||||
copy(specs, subcmd.specs)
|
|
||||||
} else {
|
|
||||||
specs = append(specs, subcmd.specs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// capture environment vars for these new options
|
// capture environment vars for these new options
|
||||||
if !p.config.IgnoreEnv {
|
if !p.config.IgnoreEnv {
|
||||||
|
@ -673,7 +530,7 @@ func (p *Parser) process(args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
curCmd = subcmd
|
curCmd = subcmd
|
||||||
p.subcommand = append(p.subcommand, arg)
|
p.lastCmd = curCmd
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -682,9 +539,7 @@ func (p *Parser) process(args []string) error {
|
||||||
case "-h", "--help":
|
case "-h", "--help":
|
||||||
return ErrHelp
|
return ErrHelp
|
||||||
case "--version":
|
case "--version":
|
||||||
if !hasVersionOption && p.version != "" {
|
return ErrVersion
|
||||||
return ErrVersion
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for an equals sign, as in "--foo=bar"
|
// check for an equals sign, as in "--foo=bar"
|
||||||
|
@ -698,7 +553,7 @@ func (p *Parser) process(args []string) error {
|
||||||
// lookup the spec for this option (note that the "specs" slice changes as
|
// lookup the spec for this option (note that the "specs" slice changes as
|
||||||
// we expand subcommands so it is better not to use a map)
|
// we expand subcommands so it is better not to use a map)
|
||||||
spec := findOption(specs, opt)
|
spec := findOption(specs, opt)
|
||||||
if spec == nil || opt == "" {
|
if spec == nil {
|
||||||
return fmt.Errorf("unknown argument %s", arg)
|
return fmt.Errorf("unknown argument %s", arg)
|
||||||
}
|
}
|
||||||
wasPresent[spec] = true
|
wasPresent[spec] = true
|
||||||
|
@ -787,26 +642,13 @@ func (p *Parser) process(args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if spec.required {
|
if spec.required {
|
||||||
if spec.short == "" && spec.long == "" {
|
return fmt.Errorf("%s is required", name)
|
||||||
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.defaultVal != "" {
|
||||||
if spec.defaultValue.IsValid() && !p.config.IgnoreDefault {
|
err := scalar.ParseValue(p.val(spec.dest), spec.defaultVal)
|
||||||
// One issue here is that if the user now modifies the value then
|
if err != nil {
|
||||||
// the default value stored in the spec will be corrupted. There
|
return fmt.Errorf("error processing default value for %s: %v", name, err)
|
||||||
// is no general way to "deep-copy" values in Go, and we still
|
}
|
||||||
// support the old-style method for specifying defaults as
|
|
||||||
// Go values assigned directly to the struct field, so we are stuck.
|
|
||||||
p.val(spec.dest).Set(spec.defaultValue)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -867,11 +709,6 @@ func findSubcommand(cmds []*command, name string) *command {
|
||||||
if cmd.name == name {
|
if cmd.name == name {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
for _, alias := range cmd.aliases {
|
|
||||||
if alias == name {
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
441
parse_test.go
441
parse_test.go
|
@ -2,11 +2,9 @@ package arg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -96,21 +94,6 @@ func TestInt(t *testing.T) {
|
||||||
assert.EqualValues(t, 8, *args.Ptr)
|
assert.EqualValues(t, 8, *args.Ptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHexOctBin(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
Hex int
|
|
||||||
Oct int
|
|
||||||
Bin int
|
|
||||||
Underscored int
|
|
||||||
}
|
|
||||||
err := parse("--hex 0xA --oct 0o10 --bin 0b101 --underscored 123_456", &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.EqualValues(t, 10, args.Hex)
|
|
||||||
assert.EqualValues(t, 8, args.Oct)
|
|
||||||
assert.EqualValues(t, 5, args.Bin)
|
|
||||||
assert.EqualValues(t, 123456, args.Underscored)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNegativeInt(t *testing.T) {
|
func TestNegativeInt(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo int
|
Foo int
|
||||||
|
@ -219,22 +202,6 @@ func TestRequired(t *testing.T) {
|
||||||
require.Error(t, err, "--foo is required")
|
require.Error(t, err, "--foo is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRequiredWithEnv(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
Foo string `arg:"required,env:FOO"`
|
|
||||||
}
|
|
||||||
err := parse("", &args)
|
|
||||||
require.Error(t, err, "--foo is required (or environment variable FOO)")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRequiredWithEnvOnly(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
Foo string `arg:"required,--,-,env:FOO"`
|
|
||||||
}
|
|
||||||
_, err := parseWithEnv("", []string{}, &args)
|
|
||||||
require.Error(t, err, "environment variable FOO is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShortFlag(t *testing.T) {
|
func TestShortFlag(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo string `arg:"-f"`
|
Foo string `arg:"-f"`
|
||||||
|
@ -754,15 +721,6 @@ func TestEnvironmentVariableSliceArgumentString(t *testing.T) {
|
||||||
assert.Equal(t, []string{"bar", "baz, qux"}, args.Foo)
|
assert.Equal(t, []string{"bar", "baz, qux"}, args.Foo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEnvironmentVariableSliceEmpty(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
Foo []string `arg:"env"`
|
|
||||||
}
|
|
||||||
_, err := parseWithEnv("", []string{`FOO=`}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Len(t, args.Foo, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnvironmentVariableSliceArgumentInteger(t *testing.T) {
|
func TestEnvironmentVariableSliceArgumentInteger(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo []int `arg:"env"`
|
Foo []int `arg:"env"`
|
||||||
|
@ -817,15 +775,6 @@ func TestEnvironmentVariableMap(t *testing.T) {
|
||||||
assert.Equal(t, "ninetynine", args.Foo[99])
|
assert.Equal(t, "ninetynine", args.Foo[99])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEnvironmentVariableEmptyMap(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
Foo map[int]string `arg:"env"`
|
|
||||||
}
|
|
||||||
_, err := parseWithEnv("", []string{`FOO=`}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Len(t, args.Foo, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnvironmentVariableIgnored(t *testing.T) {
|
func TestEnvironmentVariableIgnored(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo string `arg:"env"`
|
Foo string `arg:"env"`
|
||||||
|
@ -840,37 +789,6 @@ func TestEnvironmentVariableIgnored(t *testing.T) {
|
||||||
assert.Equal(t, "", args.Foo)
|
assert.Equal(t, "", args.Foo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultValuesIgnored(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
Foo string `default:"bad"`
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := NewParser(Config{IgnoreDefault: true}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = p.Parse(nil)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, "", args.Foo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRequiredEnvironmentOnlyVariableIsMissing(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
Foo string `arg:"required,--,env:FOO"`
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := parseWithEnv("", []string{""}, &args)
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOptionalEnvironmentOnlyVariable(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
Foo string `arg:"env:FOO"`
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := parseWithEnv("", []string{}, &args)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
|
func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Sub *struct {
|
Sub *struct {
|
||||||
|
@ -883,51 +801,10 @@ func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = p.Parse([]string{"sub"})
|
err = p.Parse([]string{"sub"})
|
||||||
require.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
require.NotNil(t, args.Sub)
|
|
||||||
assert.Equal(t, "", args.Sub.Foo)
|
assert.Equal(t, "", args.Sub.Foo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParserMustParseEmptyArgs(t *testing.T) {
|
|
||||||
// this mirrors TestEmptyArgs
|
|
||||||
p, err := NewParser(Config{}, &struct{}{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, p)
|
|
||||||
p.MustParse(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParserMustParse(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args versioned
|
|
||||||
cmdLine []string
|
|
||||||
code int
|
|
||||||
output string
|
|
||||||
}{
|
|
||||||
{name: "help", args: struct{}{}, cmdLine: []string{"--help"}, code: 0, output: "display this help and exit"},
|
|
||||||
{name: "version", args: versioned{}, cmdLine: []string{"--version"}, code: 0, output: "example 3.2.1"},
|
|
||||||
{name: "invalid", args: struct{}{}, cmdLine: []string{"invalid"}, code: -1, output: ""},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
tt := tt
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
var exitCode int
|
|
||||||
var stdout bytes.Buffer
|
|
||||||
exit := func(code int) { exitCode = code }
|
|
||||||
|
|
||||||
p, err := NewParser(Config{Exit: exit, Out: &stdout}, &tt.args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, p)
|
|
||||||
|
|
||||||
p.MustParse(tt.cmdLine)
|
|
||||||
assert.NotNil(t, exitCode)
|
|
||||||
assert.Equal(t, tt.code, exitCode)
|
|
||||||
assert.Contains(t, stdout.String(), tt.output)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type textUnmarshaler struct {
|
type textUnmarshaler struct {
|
||||||
val int
|
val int
|
||||||
}
|
}
|
||||||
|
@ -1066,24 +943,6 @@ func TestPtrToIP(t *testing.T) {
|
||||||
assert.Equal(t, "192.168.0.1", args.Host.String())
|
assert.Equal(t, "192.168.0.1", args.Host.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestURL(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
URL url.URL
|
|
||||||
}
|
|
||||||
err := parse("--url https://example.com/get?item=xyz", &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "https://example.com/get?item=xyz", args.URL.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPtrToURL(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
URL *url.URL
|
|
||||||
}
|
|
||||||
err := parse("--url http://example.com/#xyz", &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "http://example.com/#xyz", args.URL.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIPSlice(t *testing.T) {
|
func TestIPSlice(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Host []net.IP
|
Host []net.IP
|
||||||
|
@ -1218,29 +1077,6 @@ func TestEmbeddedWithDuplicateField2(t *testing.T) {
|
||||||
assert.Equal(t, "", args.U.A)
|
assert.Equal(t, "", args.U.A)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnexportedEmbedded(t *testing.T) {
|
|
||||||
type embeddedArgs struct {
|
|
||||||
Foo string
|
|
||||||
}
|
|
||||||
var args struct {
|
|
||||||
embeddedArgs
|
|
||||||
}
|
|
||||||
err := parse("--foo bar", &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "bar", args.Foo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIgnoredEmbedded(t *testing.T) {
|
|
||||||
type embeddedArgs struct {
|
|
||||||
Foo string
|
|
||||||
}
|
|
||||||
var args struct {
|
|
||||||
embeddedArgs `arg:"-"`
|
|
||||||
}
|
|
||||||
err := parse("--foo bar", &args)
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmptyArgs(t *testing.T) {
|
func TestEmptyArgs(t *testing.T) {
|
||||||
origArgs := os.Args
|
origArgs := os.Args
|
||||||
|
|
||||||
|
@ -1381,55 +1217,11 @@ func TestReuseParser(t *testing.T) {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNoVersion(t *testing.T) {
|
func TestVersion(t *testing.T) {
|
||||||
var args struct{}
|
var args struct{}
|
||||||
|
err := parse("--version", &args)
|
||||||
p, err := NewParser(Config{}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = p.Parse([]string{"--version"})
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.NotEqual(t, ErrVersion, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuiltinVersion(t *testing.T) {
|
|
||||||
var args struct{}
|
|
||||||
|
|
||||||
p, err := NewParser(Config{}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
p.version = "example 3.2.1"
|
|
||||||
|
|
||||||
err = p.Parse([]string{"--version"})
|
|
||||||
assert.Equal(t, ErrVersion, err)
|
assert.Equal(t, ErrVersion, err)
|
||||||
}
|
|
||||||
|
|
||||||
func TestArgsVersion(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
Version bool `arg:"--version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := NewParser(Config{}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = p.Parse([]string{"--version"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, args.Version, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestArgsAndBuiltinVersion(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
Version bool `arg:"--version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := NewParser(Config{}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
p.version = "example 3.2.1"
|
|
||||||
|
|
||||||
err = p.Parse([]string{"--version"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, args.Version, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultipleTerminates(t *testing.T) {
|
func TestMultipleTerminates(t *testing.T) {
|
||||||
|
@ -1460,21 +1252,13 @@ func TestDefaultOptionValues(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, 123, args.A)
|
assert.Equal(t, 123, args.A)
|
||||||
if assert.NotNil(t, args.B) {
|
assert.Equal(t, 123, *args.B)
|
||||||
assert.Equal(t, 123, *args.B)
|
|
||||||
}
|
|
||||||
assert.Equal(t, "xyz", args.C)
|
assert.Equal(t, "xyz", args.C)
|
||||||
if assert.NotNil(t, args.D) {
|
assert.Equal(t, "abc", *args.D)
|
||||||
assert.Equal(t, "abc", *args.D)
|
|
||||||
}
|
|
||||||
assert.Equal(t, 4.56, args.E)
|
assert.Equal(t, 4.56, args.E)
|
||||||
if assert.NotNil(t, args.F) {
|
assert.Equal(t, 1.23, *args.F)
|
||||||
assert.Equal(t, 1.23, *args.F)
|
assert.True(t, args.G)
|
||||||
}
|
|
||||||
assert.True(t, args.G)
|
assert.True(t, args.G)
|
||||||
if assert.NotNil(t, args.H) {
|
|
||||||
assert.True(t, *args.H)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultUnparseable(t *testing.T) {
|
func TestDefaultUnparseable(t *testing.T) {
|
||||||
|
@ -1483,7 +1267,7 @@ func TestDefaultUnparseable(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err := parse("", &args)
|
err := parse("", &args)
|
||||||
assert.EqualError(t, err, `.A: error processing default value: strconv.ParseInt: parsing "x": invalid syntax`)
|
assert.EqualError(t, err, `error processing default value for --a: strconv.ParseInt: parsing "x": invalid syntax`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultPositionalValues(t *testing.T) {
|
func TestDefaultPositionalValues(t *testing.T) {
|
||||||
|
@ -1502,21 +1286,13 @@ func TestDefaultPositionalValues(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, 456, args.A)
|
assert.Equal(t, 456, args.A)
|
||||||
if assert.NotNil(t, args.B) {
|
assert.Equal(t, 789, *args.B)
|
||||||
assert.Equal(t, 789, *args.B)
|
|
||||||
}
|
|
||||||
assert.Equal(t, "abc", args.C)
|
assert.Equal(t, "abc", args.C)
|
||||||
if assert.NotNil(t, args.D) {
|
assert.Equal(t, "abc", *args.D)
|
||||||
assert.Equal(t, "abc", *args.D)
|
|
||||||
}
|
|
||||||
assert.Equal(t, 1.23, args.E)
|
assert.Equal(t, 1.23, args.E)
|
||||||
if assert.NotNil(t, args.F) {
|
assert.Equal(t, 1.23, *args.F)
|
||||||
assert.Equal(t, 1.23, *args.F)
|
assert.True(t, args.G)
|
||||||
}
|
|
||||||
assert.True(t, args.G)
|
assert.True(t, args.G)
|
||||||
if assert.NotNil(t, args.H) {
|
|
||||||
assert.True(t, *args.H)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultValuesNotAllowedWithRequired(t *testing.T) {
|
func TestDefaultValuesNotAllowedWithRequired(t *testing.T) {
|
||||||
|
@ -1530,7 +1306,7 @@ func TestDefaultValuesNotAllowedWithRequired(t *testing.T) {
|
||||||
|
|
||||||
func TestDefaultValuesNotAllowedWithSlice(t *testing.T) {
|
func TestDefaultValuesNotAllowedWithSlice(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
A []int `default:"invalid"` // default values not allowed with slices
|
A []int `default:"123"` // required not allowed with default!
|
||||||
}
|
}
|
||||||
|
|
||||||
err := parse("", &args)
|
err := parse("", &args)
|
||||||
|
@ -1547,193 +1323,68 @@ func TestUnexportedFieldsSkipped(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMustParseInvalidParser(t *testing.T) {
|
func TestMustParseInvalidParser(t *testing.T) {
|
||||||
|
originalExit := osExit
|
||||||
|
originalStdout := stdout
|
||||||
|
defer func() {
|
||||||
|
osExit = originalExit
|
||||||
|
stdout = originalStdout
|
||||||
|
}()
|
||||||
|
|
||||||
var exitCode int
|
var exitCode int
|
||||||
var stdout bytes.Buffer
|
osExit = func(code int) { exitCode = code }
|
||||||
exit := func(code int) { exitCode = code }
|
stdout = &bytes.Buffer{}
|
||||||
|
|
||||||
var args struct {
|
var args struct {
|
||||||
CannotParse struct{}
|
CannotParse struct{}
|
||||||
}
|
}
|
||||||
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
|
parser := MustParse(&args)
|
||||||
assert.Nil(t, parser)
|
assert.Nil(t, parser)
|
||||||
assert.Equal(t, -1, exitCode)
|
assert.Equal(t, -1, exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMustParsePrintsHelp(t *testing.T) {
|
func TestMustParsePrintsHelp(t *testing.T) {
|
||||||
|
originalExit := osExit
|
||||||
|
originalStdout := stdout
|
||||||
originalArgs := os.Args
|
originalArgs := os.Args
|
||||||
defer func() {
|
defer func() {
|
||||||
|
osExit = originalExit
|
||||||
|
stdout = originalStdout
|
||||||
os.Args = originalArgs
|
os.Args = originalArgs
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
var exitCode *int
|
||||||
|
osExit = func(code int) { exitCode = &code }
|
||||||
os.Args = []string{"someprogram", "--help"}
|
os.Args = []string{"someprogram", "--help"}
|
||||||
|
stdout = &bytes.Buffer{}
|
||||||
var exitCode int
|
|
||||||
var stdout bytes.Buffer
|
|
||||||
exit := func(code int) { exitCode = code }
|
|
||||||
|
|
||||||
var args struct{}
|
var args struct{}
|
||||||
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
|
parser := MustParse(&args)
|
||||||
assert.NotNil(t, parser)
|
assert.NotNil(t, parser)
|
||||||
assert.Equal(t, 0, exitCode)
|
require.NotNil(t, exitCode)
|
||||||
|
assert.Equal(t, 0, *exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMustParsePrintsVersion(t *testing.T) {
|
func TestMustParsePrintsVersion(t *testing.T) {
|
||||||
|
originalExit := osExit
|
||||||
|
originalStdout := stdout
|
||||||
originalArgs := os.Args
|
originalArgs := os.Args
|
||||||
defer func() {
|
defer func() {
|
||||||
|
osExit = originalExit
|
||||||
|
stdout = originalStdout
|
||||||
os.Args = originalArgs
|
os.Args = originalArgs
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var exitCode int
|
var exitCode *int
|
||||||
var stdout bytes.Buffer
|
osExit = func(code int) { exitCode = &code }
|
||||||
exit := func(code int) { exitCode = code }
|
|
||||||
|
|
||||||
os.Args = []string{"someprogram", "--version"}
|
os.Args = []string{"someprogram", "--version"}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
stdout = &b
|
||||||
|
|
||||||
var args versioned
|
var args versioned
|
||||||
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
|
parser := MustParse(&args)
|
||||||
require.NotNil(t, parser)
|
require.NotNil(t, parser)
|
||||||
assert.Equal(t, 0, exitCode)
|
require.NotNil(t, exitCode)
|
||||||
assert.Equal(t, "example 3.2.1\n", stdout.String())
|
assert.Equal(t, 0, *exitCode)
|
||||||
}
|
assert.Equal(t, "example 3.2.1\n", b.String())
|
||||||
|
|
||||||
type mapWithUnmarshalText struct {
|
|
||||||
val map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *mapWithUnmarshalText) UnmarshalText(data []byte) error {
|
|
||||||
return json.Unmarshal(data, &v.val)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTextUnmarshalerEmpty(t *testing.T) {
|
|
||||||
// based on https://github.com/alexflint/go-arg/issues/184
|
|
||||||
var args struct {
|
|
||||||
Config mapWithUnmarshalText `arg:"--config"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := parse("", &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Empty(t, args.Config)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTextUnmarshalerEmptyPointer(t *testing.T) {
|
|
||||||
// a slight variant on https://github.com/alexflint/go-arg/issues/184
|
|
||||||
var args struct {
|
|
||||||
Config *mapWithUnmarshalText `arg:"--config"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := parse("", &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Nil(t, args.Config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// similar to the above but also implements MarshalText
|
|
||||||
type mapWithMarshalText struct {
|
|
||||||
val map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *mapWithMarshalText) MarshalText(data []byte) error {
|
|
||||||
return json.Unmarshal(data, &v.val)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *mapWithMarshalText) UnmarshalText(data []byte) error {
|
|
||||||
return json.Unmarshal(data, &v.val)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTextMarshalerUnmarshalerEmpty(t *testing.T) {
|
|
||||||
// based on https://github.com/alexflint/go-arg/issues/184
|
|
||||||
var args struct {
|
|
||||||
Config mapWithMarshalText `arg:"--config"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := parse("", &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Empty(t, args.Config)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTextMarshalerUnmarshalerEmptyPointer(t *testing.T) {
|
|
||||||
// a slight variant on https://github.com/alexflint/go-arg/issues/184
|
|
||||||
var args struct {
|
|
||||||
Config *mapWithMarshalText `arg:"--config"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := parse("", &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Nil(t, args.Config)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubcommandGlobalFlag_Before(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
Global bool `arg:"-g"`
|
|
||||||
Sub *struct {
|
|
||||||
} `arg:"subcommand"`
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := NewParser(Config{StrictSubcommands: false}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = p.Parse([]string{"-g", "sub"})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.True(t, args.Global)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubcommandGlobalFlag_InCommand(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
Global bool `arg:"-g"`
|
|
||||||
Sub *struct {
|
|
||||||
} `arg:"subcommand"`
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := NewParser(Config{StrictSubcommands: false}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = p.Parse([]string{"sub", "-g"})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.True(t, args.Global)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubcommandGlobalFlag_Before_Strict(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
Global bool `arg:"-g"`
|
|
||||||
Sub *struct {
|
|
||||||
} `arg:"subcommand"`
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := NewParser(Config{StrictSubcommands: true}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = p.Parse([]string{"-g", "sub"})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.True(t, args.Global)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubcommandGlobalFlag_InCommand_Strict(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
Global bool `arg:"-g"`
|
|
||||||
Sub *struct {
|
|
||||||
} `arg:"subcommand"`
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := NewParser(Config{StrictSubcommands: true}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = p.Parse([]string{"sub", "-g"})
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubcommandGlobalFlag_InCommand_Strict_Inner(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
Global bool `arg:"-g"`
|
|
||||||
Sub *struct {
|
|
||||||
Guard bool `arg:"-g"`
|
|
||||||
} `arg:"subcommand"`
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := NewParser(Config{StrictSubcommands: true}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = p.Parse([]string{"sub", "-g"})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.False(t, args.Global)
|
|
||||||
require.NotNil(t, args.Sub)
|
|
||||||
assert.True(t, args.Sub.Guard)
|
|
||||||
}
|
}
|
||||||
|
|
17
reflect.go
17
reflect.go
|
@ -13,9 +13,9 @@ import (
|
||||||
var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()
|
var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()
|
||||||
|
|
||||||
// cardinality tracks how many tokens are expected for a given spec
|
// cardinality tracks how many tokens are expected for a given spec
|
||||||
// - zero is a boolean, which does to expect any value
|
// - zero is a boolean, which does to expect any value
|
||||||
// - one is an ordinary option that will be parsed from a single token
|
// - one is an ordinary option that will be parsed from a single token
|
||||||
// - multiple is a slice or map that can accept zero or more tokens
|
// - multiple is a slice or map that can accept zero or more tokens
|
||||||
type cardinality int
|
type cardinality int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -74,10 +74,10 @@ func cardinalityOf(t reflect.Type) (cardinality, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// isBoolean returns true if the type is a boolean or a pointer to a boolean
|
// isBoolean returns true if the type can be parsed from a single string
|
||||||
func isBoolean(t reflect.Type) bool {
|
func isBoolean(t reflect.Type) bool {
|
||||||
switch {
|
switch {
|
||||||
case isTextUnmarshaler(t):
|
case t.Implements(textUnmarshalerType):
|
||||||
return false
|
return false
|
||||||
case t.Kind() == reflect.Bool:
|
case t.Kind() == reflect.Bool:
|
||||||
return true
|
return true
|
||||||
|
@ -88,11 +88,6 @@ func isBoolean(t reflect.Type) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// isTextUnmarshaler returns true if the type or its pointer implements encoding.TextUnmarshaler
|
|
||||||
func isTextUnmarshaler(t reflect.Type) bool {
|
|
||||||
return t.Implements(textUnmarshalerType) || reflect.PtrTo(t).Implements(textUnmarshalerType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// isExported returns true if the struct field name is exported
|
// isExported returns true if the struct field name is exported
|
||||||
func isExported(field string) bool {
|
func isExported(field string) bool {
|
||||||
r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8
|
r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8
|
||||||
|
@ -102,7 +97,7 @@ func isExported(field string) bool {
|
||||||
// isZero returns true if v contains the zero value for its type
|
// isZero returns true if v contains the zero value for its type
|
||||||
func isZero(v reflect.Value) bool {
|
func isZero(v reflect.Value) bool {
|
||||||
t := v.Type()
|
t := v.Type()
|
||||||
if t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice || t.Kind() == reflect.Map || t.Kind() == reflect.Chan || t.Kind() == reflect.Interface {
|
if t.Kind() == reflect.Slice || t.Kind() == reflect.Map {
|
||||||
return v.IsNil()
|
return v.IsNil()
|
||||||
}
|
}
|
||||||
if !t.Comparable() {
|
if !t.Comparable() {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package arg
|
package arg
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// Subcommand returns the user struct for the subcommand selected by
|
// Subcommand returns the user struct for the subcommand selected by
|
||||||
// the command line arguments most recently processed by the parser.
|
// the command line arguments most recently processed by the parser.
|
||||||
// The return value is always a pointer to a struct. If no subcommand
|
// The return value is always a pointer to a struct. If no subcommand
|
||||||
|
@ -9,35 +7,31 @@ import "fmt"
|
||||||
// no command line arguments have been processed by this parser then it
|
// no command line arguments have been processed by this parser then it
|
||||||
// returns nil.
|
// returns nil.
|
||||||
func (p *Parser) Subcommand() interface{} {
|
func (p *Parser) Subcommand() interface{} {
|
||||||
if len(p.subcommand) == 0 {
|
if p.lastCmd == nil || p.lastCmd.parent == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
cmd, err := p.lookupCommand(p.subcommand...)
|
return p.val(p.lastCmd.dest).Interface()
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return p.val(cmd.dest).Interface()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubcommandNames returns the sequence of subcommands specified by the
|
// SubcommandNames returns the sequence of subcommands specified by the
|
||||||
// user. If no subcommands were given then it returns an empty slice.
|
// user. If no subcommands were given then it returns an empty slice.
|
||||||
func (p *Parser) SubcommandNames() []string {
|
func (p *Parser) SubcommandNames() []string {
|
||||||
return p.subcommand
|
if p.lastCmd == nil {
|
||||||
}
|
return nil
|
||||||
|
|
||||||
// lookupCommand finds a subcommand based on a sequence of subcommand names. The
|
|
||||||
// first string should be a top-level subcommand, the next should be a child
|
|
||||||
// subcommand of that subcommand, and so on. If no strings are given then the
|
|
||||||
// root command is returned. If no such subcommand exists then an error is
|
|
||||||
// returned.
|
|
||||||
func (p *Parser) lookupCommand(path ...string) (*command, error) {
|
|
||||||
cmd := p.cmd
|
|
||||||
for _, name := range path {
|
|
||||||
found := findSubcommand(cmd.subcommands, name)
|
|
||||||
if found == nil {
|
|
||||||
return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
|
|
||||||
}
|
|
||||||
cmd = found
|
|
||||||
}
|
}
|
||||||
return cmd, nil
|
|
||||||
|
// make a list of ancestor commands
|
||||||
|
var ancestors []string
|
||||||
|
cur := p.lastCmd
|
||||||
|
for cur.parent != nil { // we want to exclude the root
|
||||||
|
ancestors = append(ancestors, cur.name)
|
||||||
|
cur = cur.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// reverse the list
|
||||||
|
out := make([]string, len(ancestors))
|
||||||
|
for i := 0; i < len(ancestors); i++ {
|
||||||
|
out[i] = ancestors[len(ancestors)-i-1]
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,19 +83,6 @@ func TestNamedSubcommand(t *testing.T) {
|
||||||
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
|
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSubcommandAliases(t *testing.T) {
|
|
||||||
type listCmd struct {
|
|
||||||
}
|
|
||||||
var args struct {
|
|
||||||
List *listCmd `arg:"subcommand:list|ls"`
|
|
||||||
}
|
|
||||||
p, err := pparse("ls", &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, args.List)
|
|
||||||
assert.Equal(t, args.List, p.Subcommand())
|
|
||||||
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmptySubcommand(t *testing.T) {
|
func TestEmptySubcommand(t *testing.T) {
|
||||||
type listCmd struct {
|
type listCmd struct {
|
||||||
}
|
}
|
||||||
|
@ -126,23 +113,6 @@ func TestTwoSubcommands(t *testing.T) {
|
||||||
assert.Equal(t, []string{"list"}, p.SubcommandNames())
|
assert.Equal(t, []string{"list"}, p.SubcommandNames())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTwoSubcommandsWithAliases(t *testing.T) {
|
|
||||||
type getCmd struct {
|
|
||||||
}
|
|
||||||
type listCmd struct {
|
|
||||||
}
|
|
||||||
var args struct {
|
|
||||||
Get *getCmd `arg:"subcommand:get|g"`
|
|
||||||
List *listCmd `arg:"subcommand:list|ls"`
|
|
||||||
}
|
|
||||||
p, err := pparse("ls", &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Nil(t, args.Get)
|
|
||||||
assert.NotNil(t, args.List)
|
|
||||||
assert.Equal(t, args.List, p.Subcommand())
|
|
||||||
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubcommandsWithOptions(t *testing.T) {
|
func TestSubcommandsWithOptions(t *testing.T) {
|
||||||
type getCmd struct {
|
type getCmd struct {
|
||||||
Name string
|
Name string
|
||||||
|
@ -305,60 +275,6 @@ func TestNestedSubcommands(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNestedSubcommandsWithAliases(t *testing.T) {
|
|
||||||
type child struct{}
|
|
||||||
type parent struct {
|
|
||||||
Child *child `arg:"subcommand:child|ch"`
|
|
||||||
}
|
|
||||||
type grandparent struct {
|
|
||||||
Parent *parent `arg:"subcommand:parent|pa"`
|
|
||||||
}
|
|
||||||
type root struct {
|
|
||||||
Grandparent *grandparent `arg:"subcommand:grandparent|gp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
var args root
|
|
||||||
p, err := pparse("gp parent child", &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, args.Grandparent)
|
|
||||||
require.NotNil(t, args.Grandparent.Parent)
|
|
||||||
require.NotNil(t, args.Grandparent.Parent.Child)
|
|
||||||
assert.Equal(t, args.Grandparent.Parent.Child, p.Subcommand())
|
|
||||||
assert.Equal(t, []string{"gp", "parent", "child"}, p.SubcommandNames())
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
var args root
|
|
||||||
p, err := pparse("grandparent pa", &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, args.Grandparent)
|
|
||||||
require.NotNil(t, args.Grandparent.Parent)
|
|
||||||
require.Nil(t, args.Grandparent.Parent.Child)
|
|
||||||
assert.Equal(t, args.Grandparent.Parent, p.Subcommand())
|
|
||||||
assert.Equal(t, []string{"grandparent", "pa"}, p.SubcommandNames())
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
var args root
|
|
||||||
p, err := pparse("grandparent", &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, args.Grandparent)
|
|
||||||
require.Nil(t, args.Grandparent.Parent)
|
|
||||||
assert.Equal(t, args.Grandparent, p.Subcommand())
|
|
||||||
assert.Equal(t, []string{"grandparent"}, p.SubcommandNames())
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
var args root
|
|
||||||
p, err := pparse("", &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Nil(t, args.Grandparent)
|
|
||||||
assert.Nil(t, p.Subcommand())
|
|
||||||
assert.Empty(t, p.SubcommandNames())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubcommandsWithPositionals(t *testing.T) {
|
func TestSubcommandsWithPositionals(t *testing.T) {
|
||||||
type listCmd struct {
|
type listCmd struct {
|
||||||
Pattern string `arg:"positional"`
|
Pattern string `arg:"positional"`
|
||||||
|
@ -495,14 +411,3 @@ func TestValForNilStruct(t *testing.T) {
|
||||||
v := p.val(path{fields: []reflect.StructField{subField, subField}})
|
v := p.val(path{fields: []reflect.StructField{subField, subField}})
|
||||||
assert.False(t, v.IsValid())
|
assert.False(t, v.IsValid())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSubcommandInvalidInternal(t *testing.T) {
|
|
||||||
// this situation should never arise in practice but still good to test for it
|
|
||||||
var cmd struct{}
|
|
||||||
p, err := NewParser(Config{}, &cmd)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
p.subcommand = []string{"should", "never", "happen"}
|
|
||||||
sub := p.Subcommand()
|
|
||||||
assert.Nil(t, sub)
|
|
||||||
}
|
|
||||||
|
|
217
usage.go
217
usage.go
|
@ -3,50 +3,43 @@ package arg
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// the width of the left column
|
// the width of the left column
|
||||||
const colWidth = 25
|
const colWidth = 25
|
||||||
|
|
||||||
|
// to allow monkey patching in tests
|
||||||
|
var (
|
||||||
|
stdout io.Writer = os.Stdout
|
||||||
|
stderr io.Writer = os.Stderr
|
||||||
|
osExit = os.Exit
|
||||||
|
)
|
||||||
|
|
||||||
// Fail prints usage information to stderr and exits with non-zero status
|
// Fail prints usage information to stderr and exits with non-zero status
|
||||||
func (p *Parser) Fail(msg string) {
|
func (p *Parser) Fail(msg string) {
|
||||||
p.FailSubcommand(msg)
|
p.failWithCommand(msg, p.cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FailSubcommand prints usage information for a specified subcommand to stderr,
|
// failWithCommand prints usage information for the given subcommand to stderr and exits with non-zero status
|
||||||
// then exits with non-zero status. To write usage information for a top-level
|
func (p *Parser) failWithCommand(msg string, cmd *command) {
|
||||||
// subcommand, provide just the name of that subcommand. To write usage
|
p.writeUsageForCommand(stderr, cmd)
|
||||||
// information for a subcommand that is nested under another subcommand, provide
|
fmt.Fprintln(stderr, "error:", msg)
|
||||||
// a sequence of subcommand names starting with the top-level subcommand and so
|
osExit(-1)
|
||||||
// on down the tree.
|
|
||||||
func (p *Parser) FailSubcommand(msg string, subcommand ...string) error {
|
|
||||||
err := p.WriteUsageForSubcommand(p.config.Out, subcommand...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(p.config.Out, "error:", msg)
|
|
||||||
p.config.Exit(-1)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteUsage writes usage information to the given writer
|
// WriteUsage writes usage information to the given writer
|
||||||
func (p *Parser) WriteUsage(w io.Writer) {
|
func (p *Parser) WriteUsage(w io.Writer) {
|
||||||
p.WriteUsageForSubcommand(w, p.subcommand...)
|
cmd := p.cmd
|
||||||
|
if p.lastCmd != nil {
|
||||||
|
cmd = p.lastCmd
|
||||||
|
}
|
||||||
|
p.writeUsageForCommand(w, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteUsageForSubcommand writes the usage information for a specified
|
// writeUsageForCommand writes usage information for the given subcommand
|
||||||
// subcommand. To write usage information for a top-level subcommand, provide
|
func (p *Parser) writeUsageForCommand(w io.Writer, cmd *command) {
|
||||||
// just the name of that subcommand. To write usage information for a subcommand
|
|
||||||
// that is nested under another subcommand, provide a sequence of subcommand
|
|
||||||
// names starting with the top-level subcommand and so on down the tree.
|
|
||||||
func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) error {
|
|
||||||
cmd, err := p.lookupCommand(subcommand...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var positionals, longOptions, shortOptions []*spec
|
var positionals, longOptions, shortOptions []*spec
|
||||||
for _, spec := range cmd.specs {
|
for _, spec := range cmd.specs {
|
||||||
switch {
|
switch {
|
||||||
|
@ -63,10 +56,18 @@ func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) erro
|
||||||
fmt.Fprintln(w, p.version)
|
fmt.Fprintln(w, p.version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make a list of ancestor commands so that we print with full context
|
||||||
|
var ancestors []string
|
||||||
|
ancestor := cmd
|
||||||
|
for ancestor != nil {
|
||||||
|
ancestors = append(ancestors, ancestor.name)
|
||||||
|
ancestor = ancestor.parent
|
||||||
|
}
|
||||||
|
|
||||||
// print the beginning of the usage string
|
// print the beginning of the usage string
|
||||||
fmt.Fprintf(w, "Usage: %s", p.cmd.name)
|
fmt.Fprint(w, "Usage:")
|
||||||
for _, s := range subcommand {
|
for i := len(ancestors) - 1; i >= 0; i-- {
|
||||||
fmt.Fprint(w, " "+s)
|
fmt.Fprint(w, " "+ancestors[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
// write the option component of the usage message
|
// write the option component of the usage message
|
||||||
|
@ -94,32 +95,22 @@ func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) erro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// When we parse positionals, we check that:
|
// write the positional component of the usage message
|
||||||
// 1. required positionals come before non-required positionals
|
|
||||||
// 2. there is at most one multiple-value positional
|
|
||||||
// 3. if there is a multiple-value positional then it comes after all other positionals
|
|
||||||
// Here we merely print the usage string, so we do not explicitly re-enforce those rules
|
|
||||||
|
|
||||||
// write the positionals in following form:
|
|
||||||
// REQUIRED1 REQUIRED2
|
|
||||||
// REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]
|
|
||||||
// REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]
|
|
||||||
// REQUIRED1 REQUIRED2 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]
|
|
||||||
// REQUIRED1 REQUIRED2 [OPTIONAL1 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]]
|
|
||||||
var closeBrackets int
|
|
||||||
for _, spec := range positionals {
|
for _, spec := range positionals {
|
||||||
|
// prefix with a space
|
||||||
fmt.Fprint(w, " ")
|
fmt.Fprint(w, " ")
|
||||||
if !spec.required {
|
|
||||||
fmt.Fprint(w, "[")
|
|
||||||
closeBrackets += 1
|
|
||||||
}
|
|
||||||
if spec.cardinality == multiple {
|
if spec.cardinality == multiple {
|
||||||
|
if !spec.required {
|
||||||
|
fmt.Fprint(w, "[")
|
||||||
|
}
|
||||||
fmt.Fprintf(w, "%s [%s ...]", spec.placeholder, spec.placeholder)
|
fmt.Fprintf(w, "%s [%s ...]", spec.placeholder, spec.placeholder)
|
||||||
|
if !spec.required {
|
||||||
|
fmt.Fprint(w, "]")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprint(w, spec.placeholder)
|
fmt.Fprint(w, spec.placeholder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Fprint(w, strings.Repeat("]", closeBrackets))
|
|
||||||
|
|
||||||
// if the program supports subcommands, give a hint to the user about their existence
|
// if the program supports subcommands, give a hint to the user about their existence
|
||||||
if len(cmd.subcommands) > 0 {
|
if len(cmd.subcommands) > 0 {
|
||||||
|
@ -127,81 +118,52 @@ func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) erro
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprint(w, "\n")
|
fmt.Fprint(w, "\n")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// print prints a line like this:
|
func printTwoCols(w io.Writer, left, help string, defaultVal string, envVal string) {
|
||||||
//
|
lhs := " " + left
|
||||||
// --option FOO A description of the option [default: 123]
|
|
||||||
//
|
|
||||||
// If the text on the left is longer than a certain threshold, the description is moved to the next line:
|
|
||||||
//
|
|
||||||
// --verylongoptionoption VERY_LONG_VARIABLE
|
|
||||||
// A description of the option [default: 123]
|
|
||||||
//
|
|
||||||
// If multiple "extras" are provided then they are put inside a single set of square brackets:
|
|
||||||
//
|
|
||||||
// --option FOO A description of the option [default: 123, env: FOO]
|
|
||||||
func print(w io.Writer, item, description string, bracketed ...string) {
|
|
||||||
lhs := " " + item
|
|
||||||
fmt.Fprint(w, lhs)
|
fmt.Fprint(w, lhs)
|
||||||
if description != "" {
|
if help != "" {
|
||||||
if len(lhs)+2 < colWidth {
|
if len(lhs)+2 < colWidth {
|
||||||
fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs)))
|
fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs)))
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
|
fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
|
||||||
}
|
}
|
||||||
fmt.Fprint(w, description)
|
fmt.Fprint(w, help)
|
||||||
}
|
}
|
||||||
|
|
||||||
var brack string
|
bracketsContent := []string{}
|
||||||
for _, s := range bracketed {
|
|
||||||
if s != "" {
|
if defaultVal != "" {
|
||||||
if brack != "" {
|
bracketsContent = append(bracketsContent,
|
||||||
brack += ", "
|
fmt.Sprintf("default: %s", defaultVal),
|
||||||
}
|
)
|
||||||
brack += s
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if brack != "" {
|
if envVal != "" {
|
||||||
fmt.Fprintf(w, " [%s]", brack)
|
bracketsContent = append(bracketsContent,
|
||||||
|
fmt.Sprintf("env: %s", envVal),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bracketsContent) > 0 {
|
||||||
|
fmt.Fprintf(w, " [%s]", strings.Join(bracketsContent, ", "))
|
||||||
}
|
}
|
||||||
fmt.Fprint(w, "\n")
|
fmt.Fprint(w, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func withDefault(s string) string {
|
|
||||||
if s == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return "default: " + s
|
|
||||||
}
|
|
||||||
|
|
||||||
func withEnv(env string) string {
|
|
||||||
if env == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return "env: " + env
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteHelp writes the usage string followed by the full help string for each option
|
// WriteHelp writes the usage string followed by the full help string for each option
|
||||||
func (p *Parser) WriteHelp(w io.Writer) {
|
func (p *Parser) WriteHelp(w io.Writer) {
|
||||||
p.WriteHelpForSubcommand(w, p.subcommand...)
|
cmd := p.cmd
|
||||||
|
if p.lastCmd != nil {
|
||||||
|
cmd = p.lastCmd
|
||||||
|
}
|
||||||
|
p.writeHelpForCommand(w, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteHelpForSubcommand writes the usage string followed by the full help
|
// writeHelp writes the usage string for the given subcommand
|
||||||
// string for a specified subcommand. To write help for a top-level subcommand,
|
func (p *Parser) writeHelpForCommand(w io.Writer, cmd *command) {
|
||||||
// provide just the name of that subcommand. To write help for a subcommand that
|
var positionals, longOptions, shortOptions []*spec
|
||||||
// is nested under another subcommand, provide a sequence of subcommand names
|
|
||||||
// starting with the top-level subcommand and so on down the tree.
|
|
||||||
func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error {
|
|
||||||
cmd, err := p.lookupCommand(subcommand...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var positionals, longOptions, shortOptions, envOnlyOptions []*spec
|
|
||||||
var hasVersionOption bool
|
|
||||||
for _, spec := range cmd.specs {
|
for _, spec := range cmd.specs {
|
||||||
switch {
|
switch {
|
||||||
case spec.positional:
|
case spec.positional:
|
||||||
|
@ -210,21 +172,19 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
|
||||||
longOptions = append(longOptions, spec)
|
longOptions = append(longOptions, spec)
|
||||||
case spec.short != "":
|
case spec.short != "":
|
||||||
shortOptions = append(shortOptions, spec)
|
shortOptions = append(shortOptions, spec)
|
||||||
case spec.short == "" && spec.long == "":
|
|
||||||
envOnlyOptions = append(envOnlyOptions, spec)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.description != "" {
|
if p.description != "" {
|
||||||
fmt.Fprintln(w, p.description)
|
fmt.Fprintln(w, p.description)
|
||||||
}
|
}
|
||||||
p.WriteUsageForSubcommand(w, subcommand...)
|
p.writeUsageForCommand(w, cmd)
|
||||||
|
|
||||||
// write the list of positionals
|
// write the list of positionals
|
||||||
if len(positionals) > 0 {
|
if len(positionals) > 0 {
|
||||||
fmt.Fprint(w, "\nPositional arguments:\n")
|
fmt.Fprint(w, "\nPositional arguments:\n")
|
||||||
for _, spec := range positionals {
|
for _, spec := range positionals {
|
||||||
print(w, spec.placeholder, spec.help)
|
printTwoCols(w, spec.placeholder, spec.help, "", "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,9 +196,6 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
|
||||||
}
|
}
|
||||||
for _, spec := range longOptions {
|
for _, spec := range longOptions {
|
||||||
p.printOption(w, spec)
|
p.printOption(w, spec)
|
||||||
if spec.long == "version" {
|
|
||||||
hasVersionOption = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,9 +212,6 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
|
||||||
fmt.Fprint(w, "\nGlobal options:\n")
|
fmt.Fprint(w, "\nGlobal options:\n")
|
||||||
for _, spec := range globals {
|
for _, spec := range globals {
|
||||||
p.printOption(w, spec)
|
p.printOption(w, spec)
|
||||||
if spec.long == "version" {
|
|
||||||
hasVersionOption = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,7 +222,7 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
|
||||||
short: "h",
|
short: "h",
|
||||||
help: "display this help and exit",
|
help: "display this help and exit",
|
||||||
})
|
})
|
||||||
if !hasVersionOption && p.version != "" {
|
if p.version != "" {
|
||||||
p.printOption(w, &spec{
|
p.printOption(w, &spec{
|
||||||
cardinality: zero,
|
cardinality: zero,
|
||||||
long: "version",
|
long: "version",
|
||||||
|
@ -276,27 +230,13 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// write the list of environment only variables
|
|
||||||
if len(envOnlyOptions) > 0 {
|
|
||||||
fmt.Fprint(w, "\nEnvironment variables:\n")
|
|
||||||
for _, spec := range envOnlyOptions {
|
|
||||||
p.printEnvOnlyVar(w, spec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// write the list of subcommands
|
// write the list of subcommands
|
||||||
if len(cmd.subcommands) > 0 {
|
if len(cmd.subcommands) > 0 {
|
||||||
fmt.Fprint(w, "\nCommands:\n")
|
fmt.Fprint(w, "\nCommands:\n")
|
||||||
for _, subcmd := range cmd.subcommands {
|
for _, subcmd := range cmd.subcommands {
|
||||||
names := append([]string{subcmd.name}, subcmd.aliases...)
|
printTwoCols(w, subcmd.name, subcmd.help, "", "")
|
||||||
print(w, strings.Join(names, ", "), subcmd.help)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.epilogue != "" {
|
|
||||||
fmt.Fprintln(w, "\n"+p.epilogue)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) printOption(w io.Writer, spec *spec) {
|
func (p *Parser) printOption(w io.Writer, spec *spec) {
|
||||||
|
@ -308,25 +248,10 @@ func (p *Parser) printOption(w io.Writer, spec *spec) {
|
||||||
ways = append(ways, synopsis(spec, "-"+spec.short))
|
ways = append(ways, synopsis(spec, "-"+spec.short))
|
||||||
}
|
}
|
||||||
if len(ways) > 0 {
|
if len(ways) > 0 {
|
||||||
print(w, strings.Join(ways, ", "), spec.help, withDefault(spec.defaultString), withEnv(spec.env))
|
printTwoCols(w, strings.Join(ways, ", "), spec.help, spec.defaultVal, spec.env)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) {
|
|
||||||
ways := make([]string, 0, 2)
|
|
||||||
if spec.required {
|
|
||||||
ways = append(ways, "Required.")
|
|
||||||
} else {
|
|
||||||
ways = append(ways, "Optional.")
|
|
||||||
}
|
|
||||||
|
|
||||||
if spec.help != "" {
|
|
||||||
ways = append(ways, spec.help)
|
|
||||||
}
|
|
||||||
|
|
||||||
print(w, spec.env, strings.Join(ways, " "), withDefault(spec.defaultString))
|
|
||||||
}
|
|
||||||
|
|
||||||
func synopsis(spec *spec, form string) string {
|
func synopsis(spec *spec, form string) string {
|
||||||
if spec.cardinality == zero {
|
if spec.cardinality == zero {
|
||||||
return form
|
return form
|
||||||
|
|
295
usage_test.go
295
usage_test.go
|
@ -50,20 +50,16 @@ Options:
|
||||||
--optimize OPTIMIZE, -O OPTIMIZE
|
--optimize OPTIMIZE, -O OPTIMIZE
|
||||||
optimization level
|
optimization level
|
||||||
--ids IDS Ids
|
--ids IDS Ids
|
||||||
--values VALUES Values
|
--values VALUES Values [default: [3.14 42 256]]
|
||||||
--workers WORKERS, -w WORKERS
|
--workers WORKERS, -w WORKERS
|
||||||
number of workers to start [default: 10, env: WORKERS]
|
number of workers to start [default: 10, env: WORKERS]
|
||||||
--testenv TESTENV, -a TESTENV [env: TEST_ENV]
|
--testenv TESTENV, -a TESTENV [env: TEST_ENV]
|
||||||
--file FILE, -f FILE File with mandatory extension [default: scratch.txt]
|
--file FILE, -f FILE File with mandatory extension [default: scratch.txt]
|
||||||
--help, -h display this help and exit
|
--help, -h display this help and exit
|
||||||
|
|
||||||
Environment variables:
|
|
||||||
API_KEY Required. Only via env-var for security reasons
|
|
||||||
TRACE Optional. Record low-level trace
|
|
||||||
`
|
`
|
||||||
|
|
||||||
var args struct {
|
var args struct {
|
||||||
Input string `arg:"positional,required"`
|
Input string `arg:"positional"`
|
||||||
Output []string `arg:"positional" help:"list of outputs"`
|
Output []string `arg:"positional" help:"list of outputs"`
|
||||||
Name string `help:"name to use"`
|
Name string `help:"name to use"`
|
||||||
Value int `help:"secret value"`
|
Value int `help:"secret value"`
|
||||||
|
@ -74,12 +70,11 @@ Environment variables:
|
||||||
Values []float64 `help:"Values"`
|
Values []float64 `help:"Values"`
|
||||||
Workers int `arg:"-w,env:WORKERS" help:"number of workers to start" default:"10"`
|
Workers int `arg:"-w,env:WORKERS" help:"number of workers to start" default:"10"`
|
||||||
TestEnv string `arg:"-a,env:TEST_ENV"`
|
TestEnv string `arg:"-a,env:TEST_ENV"`
|
||||||
ApiKey string `arg:"required,-,--,env:API_KEY" help:"Only via env-var for security reasons"`
|
|
||||||
Trace bool `arg:"-,--,env" help:"Record low-level trace"`
|
|
||||||
File *NameDotName `arg:"-f" help:"File with mandatory extension"`
|
File *NameDotName `arg:"-f" help:"File with mandatory extension"`
|
||||||
}
|
}
|
||||||
args.Name = "Foo Bar"
|
args.Name = "Foo Bar"
|
||||||
args.Value = 42
|
args.Value = 42
|
||||||
|
args.Values = []float64{3.14, 42, 256}
|
||||||
args.File = &NameDotName{"scratch", "txt"}
|
args.File = &NameDotName{"scratch", "txt"}
|
||||||
p, err := NewParser(Config{Program: "example"}, &args)
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -146,10 +141,10 @@ func TestUsageCannotMarshalToString(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUsageLongPositionalWithHelp_legacyForm(t *testing.T) {
|
func TestUsageLongPositionalWithHelp_legacyForm(t *testing.T) {
|
||||||
expectedUsage := "Usage: example [VERYLONGPOSITIONALWITHHELP]"
|
expectedUsage := "Usage: example VERYLONGPOSITIONALWITHHELP"
|
||||||
|
|
||||||
expectedHelp := `
|
expectedHelp := `
|
||||||
Usage: example [VERYLONGPOSITIONALWITHHELP]
|
Usage: example VERYLONGPOSITIONALWITHHELP
|
||||||
|
|
||||||
Positional arguments:
|
Positional arguments:
|
||||||
VERYLONGPOSITIONALWITHHELP
|
VERYLONGPOSITIONALWITHHELP
|
||||||
|
@ -175,10 +170,10 @@ Options:
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUsageLongPositionalWithHelp_newForm(t *testing.T) {
|
func TestUsageLongPositionalWithHelp_newForm(t *testing.T) {
|
||||||
expectedUsage := "Usage: example [VERYLONGPOSITIONALWITHHELP]"
|
expectedUsage := "Usage: example VERYLONGPOSITIONALWITHHELP"
|
||||||
|
|
||||||
expectedHelp := `
|
expectedHelp := `
|
||||||
Usage: example [VERYLONGPOSITIONALWITHHELP]
|
Usage: example VERYLONGPOSITIONALWITHHELP
|
||||||
|
|
||||||
Positional arguments:
|
Positional arguments:
|
||||||
VERYLONGPOSITIONALWITHHELP
|
VERYLONGPOSITIONALWITHHELP
|
||||||
|
@ -290,105 +285,8 @@ Options:
|
||||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
type epilogued struct{}
|
|
||||||
|
|
||||||
// Epilogued returns the epilogue for this program
|
|
||||||
func (epilogued) Epilogue() string {
|
|
||||||
return "For more information visit github.com/alexflint/go-arg"
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsageWithEpilogue(t *testing.T) {
|
|
||||||
expectedUsage := "Usage: example"
|
|
||||||
|
|
||||||
expectedHelp := `
|
|
||||||
Usage: example
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--help, -h display this help and exit
|
|
||||||
|
|
||||||
For more information visit github.com/alexflint/go-arg
|
|
||||||
`
|
|
||||||
os.Args[0] = "example"
|
|
||||||
p, err := NewParser(Config{}, &epilogued{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var help bytes.Buffer
|
|
||||||
p.WriteHelp(&help)
|
|
||||||
assert.Equal(t, expectedHelp[1:], help.String())
|
|
||||||
|
|
||||||
var usage bytes.Buffer
|
|
||||||
p.WriteUsage(&usage)
|
|
||||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsageForRequiredPositionals(t *testing.T) {
|
|
||||||
expectedUsage := "Usage: example REQUIRED1 REQUIRED2\n"
|
|
||||||
var args struct {
|
|
||||||
Required1 string `arg:"positional,required"`
|
|
||||||
Required2 string `arg:"positional,required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := NewParser(Config{Program: "example"}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var usage bytes.Buffer
|
|
||||||
p.WriteUsage(&usage)
|
|
||||||
assert.Equal(t, expectedUsage, usage.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsageForMixedPositionals(t *testing.T) {
|
|
||||||
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]\n"
|
|
||||||
var args struct {
|
|
||||||
Required1 string `arg:"positional,required"`
|
|
||||||
Required2 string `arg:"positional,required"`
|
|
||||||
Optional1 string `arg:"positional"`
|
|
||||||
Optional2 string `arg:"positional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := NewParser(Config{Program: "example"}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var usage bytes.Buffer
|
|
||||||
p.WriteUsage(&usage)
|
|
||||||
assert.Equal(t, expectedUsage, usage.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsageForRepeatedPositionals(t *testing.T) {
|
|
||||||
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]\n"
|
|
||||||
var args struct {
|
|
||||||
Required1 string `arg:"positional,required"`
|
|
||||||
Required2 string `arg:"positional,required"`
|
|
||||||
Repeated []string `arg:"positional,required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := NewParser(Config{Program: "example"}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var usage bytes.Buffer
|
|
||||||
p.WriteUsage(&usage)
|
|
||||||
assert.Equal(t, expectedUsage, usage.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsageForMixedAndRepeatedPositionals(t *testing.T) {
|
|
||||||
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2 [REPEATED [REPEATED ...]]]]\n"
|
|
||||||
var args struct {
|
|
||||||
Required1 string `arg:"positional,required"`
|
|
||||||
Required2 string `arg:"positional,required"`
|
|
||||||
Optional1 string `arg:"positional"`
|
|
||||||
Optional2 string `arg:"positional"`
|
|
||||||
Repeated []string `arg:"positional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := NewParser(Config{Program: "example"}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var usage bytes.Buffer
|
|
||||||
p.WriteUsage(&usage)
|
|
||||||
assert.Equal(t, expectedUsage, usage.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRequiredMultiplePositionals(t *testing.T) {
|
func TestRequiredMultiplePositionals(t *testing.T) {
|
||||||
expectedUsage := "Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]\n"
|
expectedUsage := "Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]"
|
||||||
|
|
||||||
expectedHelp := `
|
expectedHelp := `
|
||||||
Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]
|
Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]
|
||||||
|
@ -403,7 +301,7 @@ Options:
|
||||||
RequiredMultiple []string `arg:"positional,required" help:"required multiple positional"`
|
RequiredMultiple []string `arg:"positional,required" help:"required multiple positional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
p, err := NewParser(Config{Program: "example"}, &args)
|
p, err := NewParser(Config{}, &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var help bytes.Buffer
|
var help bytes.Buffer
|
||||||
|
@ -412,7 +310,7 @@ Options:
|
||||||
|
|
||||||
var usage bytes.Buffer
|
var usage bytes.Buffer
|
||||||
p.WriteUsage(&usage)
|
p.WriteUsage(&usage)
|
||||||
assert.Equal(t, expectedUsage, usage.String())
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUsageWithNestedSubcommands(t *testing.T) {
|
func TestUsageWithNestedSubcommands(t *testing.T) {
|
||||||
|
@ -450,51 +348,13 @@ Global options:
|
||||||
|
|
||||||
_ = p.Parse([]string{"child", "nested", "value"})
|
_ = p.Parse([]string{"child", "nested", "value"})
|
||||||
|
|
||||||
assert.Equal(t, []string{"child", "nested"}, p.SubcommandNames())
|
|
||||||
|
|
||||||
var help bytes.Buffer
|
var help bytes.Buffer
|
||||||
p.WriteHelp(&help)
|
p.WriteHelp(&help)
|
||||||
assert.Equal(t, expectedHelp[1:], help.String())
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
var help2 bytes.Buffer
|
|
||||||
p.WriteHelpForSubcommand(&help2, "child", "nested")
|
|
||||||
assert.Equal(t, expectedHelp[1:], help2.String())
|
|
||||||
|
|
||||||
var usage bytes.Buffer
|
var usage bytes.Buffer
|
||||||
p.WriteUsage(&usage)
|
p.WriteUsage(&usage)
|
||||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
|
||||||
var usage2 bytes.Buffer
|
|
||||||
p.WriteUsageForSubcommand(&usage2, "child", "nested")
|
|
||||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNonexistentSubcommand(t *testing.T) {
|
|
||||||
var args struct {
|
|
||||||
sub *struct{} `arg:"subcommand"`
|
|
||||||
}
|
|
||||||
p, err := NewParser(Config{Exit: func(int) {}}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var b bytes.Buffer
|
|
||||||
|
|
||||||
err = p.WriteUsageForSubcommand(&b, "does_not_exist")
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
err = p.WriteHelpForSubcommand(&b, "does_not_exist")
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
err = p.FailSubcommand("something went wrong", "does_not_exist")
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
err = p.WriteUsageForSubcommand(&b, "sub", "does_not_exist")
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
err = p.WriteHelpForSubcommand(&b, "sub", "does_not_exist")
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
err = p.FailSubcommand("something went wrong", "sub", "does_not_exist")
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUsageWithoutLongNames(t *testing.T) {
|
func TestUsageWithoutLongNames(t *testing.T) {
|
||||||
|
@ -513,7 +373,7 @@ Options:
|
||||||
ShortOnly2 string `arg:"-b,--,required" help:"some help2"`
|
ShortOnly2 string `arg:"-b,--,required" help:"some help2"`
|
||||||
}
|
}
|
||||||
p, err := NewParser(Config{Program: "example"}, &args)
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
require.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
var help bytes.Buffer
|
var help bytes.Buffer
|
||||||
p.WriteHelp(&help)
|
p.WriteHelp(&help)
|
||||||
|
@ -560,16 +420,10 @@ Usage: example [-s SHORT]
|
||||||
Options:
|
Options:
|
||||||
-s SHORT [env: SHORT]
|
-s SHORT [env: SHORT]
|
||||||
--help, -h display this help and exit
|
--help, -h display this help and exit
|
||||||
|
|
||||||
Environment variables:
|
|
||||||
ENVONLY Optional.
|
|
||||||
ENVONLY2 Optional.
|
|
||||||
CUSTOM Optional.
|
|
||||||
`
|
`
|
||||||
var args struct {
|
var args struct {
|
||||||
Short string `arg:"--,-s,env"`
|
Short string `arg:"--,-s,env"`
|
||||||
EnvOnly string `arg:"--,env"`
|
EnvOnly string `arg:"--,env"`
|
||||||
EnvOnly2 string `arg:"--,-,env"`
|
|
||||||
EnvOnlyOverriden string `arg:"--,env:CUSTOM"`
|
EnvOnlyOverriden string `arg:"--,env:CUSTOM"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -585,39 +439,19 @@ Environment variables:
|
||||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEnvOnlyArgs(t *testing.T) {
|
|
||||||
expectedUsage := "Usage: example [--arg ARG]"
|
|
||||||
|
|
||||||
expectedHelp := `
|
|
||||||
Usage: example [--arg ARG]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--arg ARG, -a ARG [env: MY_ARG]
|
|
||||||
--help, -h display this help and exit
|
|
||||||
|
|
||||||
Environment variables:
|
|
||||||
AUTH_KEY Required.
|
|
||||||
`
|
|
||||||
var args struct {
|
|
||||||
ArgParam string `arg:"-a,--arg,env:MY_ARG"`
|
|
||||||
AuthKey string `arg:"required,--,env:AUTH_KEY"`
|
|
||||||
}
|
|
||||||
p, err := NewParser(Config{Program: "example"}, &args)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
var help bytes.Buffer
|
|
||||||
p.WriteHelp(&help)
|
|
||||||
assert.Equal(t, expectedHelp[1:], help.String())
|
|
||||||
|
|
||||||
var usage bytes.Buffer
|
|
||||||
p.WriteUsage(&usage)
|
|
||||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFail(t *testing.T) {
|
func TestFail(t *testing.T) {
|
||||||
var stdout bytes.Buffer
|
originalStderr := stderr
|
||||||
|
originalExit := osExit
|
||||||
|
defer func() {
|
||||||
|
stderr = originalStderr
|
||||||
|
osExit = originalExit
|
||||||
|
}()
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
stderr = &b
|
||||||
|
|
||||||
var exitCode int
|
var exitCode int
|
||||||
exit := func(code int) { exitCode = code }
|
osExit = func(code int) { exitCode = code }
|
||||||
|
|
||||||
expectedStdout := `
|
expectedStdout := `
|
||||||
Usage: example [--foo FOO]
|
Usage: example [--foo FOO]
|
||||||
|
@ -627,91 +461,10 @@ error: something went wrong
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo int
|
Foo int
|
||||||
}
|
}
|
||||||
p, err := NewParser(Config{Program: "example", Exit: exit, Out: &stdout}, &args)
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
p.Fail("something went wrong")
|
p.Fail("something went wrong")
|
||||||
|
|
||||||
assert.Equal(t, expectedStdout[1:], stdout.String())
|
assert.Equal(t, expectedStdout[1:], b.String())
|
||||||
assert.Equal(t, -1, exitCode)
|
assert.Equal(t, -1, exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFailSubcommand(t *testing.T) {
|
|
||||||
var stdout bytes.Buffer
|
|
||||||
var exitCode int
|
|
||||||
exit := func(code int) { exitCode = code }
|
|
||||||
|
|
||||||
expectedStdout := `
|
|
||||||
Usage: example sub
|
|
||||||
error: something went wrong
|
|
||||||
`
|
|
||||||
|
|
||||||
var args struct {
|
|
||||||
Sub *struct{} `arg:"subcommand"`
|
|
||||||
}
|
|
||||||
p, err := NewParser(Config{Program: "example", Exit: exit, Out: &stdout}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = p.FailSubcommand("something went wrong", "sub")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, expectedStdout[1:], stdout.String())
|
|
||||||
assert.Equal(t, -1, exitCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
type lengthOf struct {
|
|
||||||
Length int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *lengthOf) UnmarshalText(b []byte) error {
|
|
||||||
p.Length = len(b)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHelpShowsDefaultValueFromOriginalTag(t *testing.T) {
|
|
||||||
// check that the usage text prints the original string from the default tag, not
|
|
||||||
// the serialization of the parsed value
|
|
||||||
|
|
||||||
expectedHelp := `
|
|
||||||
Usage: example [--test TEST]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--test TEST [default: some_default_value]
|
|
||||||
--help, -h display this help and exit
|
|
||||||
`
|
|
||||||
|
|
||||||
var args struct {
|
|
||||||
Test *lengthOf `default:"some_default_value"`
|
|
||||||
}
|
|
||||||
p, err := NewParser(Config{Program: "example"}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var help bytes.Buffer
|
|
||||||
p.WriteHelp(&help)
|
|
||||||
assert.Equal(t, expectedHelp[1:], help.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHelpShowsSubcommandAliases(t *testing.T) {
|
|
||||||
expectedHelp := `
|
|
||||||
Usage: example <command> [<args>]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--help, -h display this help and exit
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
remove, rm, r remove something from somewhere
|
|
||||||
simple do something simple
|
|
||||||
halt, stop stop now
|
|
||||||
`
|
|
||||||
|
|
||||||
var args struct {
|
|
||||||
Remove *struct{} `arg:"subcommand:remove|rm|r" help:"remove something from somewhere"`
|
|
||||||
Simple *struct{} `arg:"subcommand" help:"do something simple"`
|
|
||||||
Stop *struct{} `arg:"subcommand:halt|stop" help:"stop now"`
|
|
||||||
}
|
|
||||||
p, err := NewParser(Config{Program: "example"}, &args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var help bytes.Buffer
|
|
||||||
p.WriteHelp(&help)
|
|
||||||
assert.Equal(t, expectedHelp[1:], help.String())
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue