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