Compare commits

..

2 Commits

Author SHA1 Message Date
Jeff Carr 0af6f25365 add register()
Signed-off-by: Jeff Carr <jcarr@wit.com>
2024-01-14 14:26:47 -06:00
Jeff Carr 530fcb84d4 isolate tests
Signed-off-by: Jeff Carr <jcarr@wit.com>
2024-01-14 14:25:54 -06:00
19 changed files with 206 additions and 928 deletions

1
.github/FUNDING.yml vendored
View File

@ -1 +0,0 @@
github: [alexflint]

View File

@ -15,17 +15,17 @@ jobs:
strategy:
fail-fast: false
matrix:
go: ['1.20', '1.21', '1.22']
go: ['1.17', '1.18', '1.19']
steps:
- id: go
name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go }}
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Build
run: go build -v .

View File

@ -1,19 +1,10 @@
all: goimports vet
all:
@echo
@echo
clean:
rm -f go.*
test:
redomod:
rm -f go.*
GO111MODULE= go mod init
GO111MODULE= go mod tidy
test:
go test
vet:
@GO111MODULE=off go vet
@echo this go binary package builds okay
goimports:
goimports -w *.go

300
README.md
View File

@ -64,7 +64,7 @@ fmt.Println("Input:", args.Input)
fmt.Println("Output:", args.Output)
```
```shell
```
$ ./example src.txt x.out y.out z.out
Input: src.txt
Output: [x.out y.out z.out]
@ -80,12 +80,12 @@ arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```shell
```
$ WORKERS=4 ./example
Workers: 4
```
```shell
```
$ WORKERS=4 ./example --workers=6
Workers: 6
```
@ -100,12 +100,12 @@ arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```shell
```
$ NUM_WORKERS=4 ./example
Workers: 4
```
You can provide multiple values in environment variables using commas:
You can provide multiple values using the CSV (RFC 4180) format:
```go
var args struct {
@ -115,50 +115,12 @@ arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```shell
```
$ WORKERS='1,99' ./example
Workers: [1 99]
```
Command line arguments take precedence over environment variables:
```go
var args struct {
Workers int `arg:"--count,env:NUM_WORKERS"`
}
arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```shell
$ NUM_WORKERS=6 ./example
Workers: 6
$ NUM_WORKERS=6 ./example --count 4
Workers: 4
```
Configuring a global environment variable name prefix is also possible:
```go
var args struct {
Workers int `arg:"--count,env:NUM_WORKERS"`
}
p, err := arg.NewParser(arg.Config{
EnvPrefix: "MYAPP_",
}, &args)
p.MustParse(os.Args[1:])
fmt.Println("Workers:", args.Workers)
```
```shell
$ MYAPP_NUM_WORKERS=6 ./example
Workers: 6
```
### Usage strings
```go
var args struct {
Input string `arg:"positional"`
@ -196,7 +158,20 @@ var args struct {
arg.MustParse(&args)
```
Command line arguments take precedence over environment variables, which take precedence over default values. This means that we check whether a certain option was provided on the command line, then if not, we check for an environment variable (only if an `env` tag was provided), then if none is found, we check for a `default` tag containing a default value.
### Default values (before v1.2)
```go
var args struct {
Foo string
Bar bool
}
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 {
@ -207,6 +182,9 @@ 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"`
@ -217,11 +195,10 @@ p, err := arg.NewParser(arg.Config{
IgnoreDefault: true,
}, &args)
err = p.Parse(os.Args[1:])
err = p.Parse(os.Args)
```
### Arguments with multiple values
```go
var args struct {
Database string
@ -237,7 +214,6 @@ Fetching the following IDs from foo: [1 2 3]
```
### Arguments that can be specified multiple times, mixed with positionals
```go
var args struct {
Commands []string `arg:"-c,separate"`
@ -255,7 +231,6 @@ Databases [db1 db2 db3]
```
### Arguments with keys and values
```go
var args struct {
UserIDs map[string]int
@ -269,6 +244,24 @@ fmt.Println(args.UserIDs)
map[john:123 mary:456]
```
### Custom validation
```go
var args struct {
Foo string
Bar string
}
p := arg.MustParse(&args)
if args.Foo == "" && args.Bar == "" {
p.Fail("you must provide either --foo or --bar")
}
```
```shell
./example
Usage: samples [--foo FOO] [--bar BAR]
error: you must provide either --foo or --bar
```
### Version strings
```go
@ -291,28 +284,6 @@ $ ./example --version
someprogram 4.3.0
```
> **Note**
> If a `--version` flag is defined in `args` or any subcommand, it overrides the built-in versioning.
### Custom validation
```go
var args struct {
Foo string
Bar string
}
p := arg.MustParse(&args)
if args.Foo == "" && args.Bar == "" {
p.Fail("you must provide either --foo or --bar")
}
```
```shell
./example
Usage: samples [--foo FOO] [--bar BAR]
error: you must provide either --foo or --bar
```
### Overriding option names
```go
@ -337,11 +308,13 @@ Options:
--help, -h display this help and exit
```
### Embedded structs
The fields of embedded structs are treated just like regular fields:
```go
type DatabaseOptions struct {
Host string
Username string
@ -409,7 +382,6 @@ func main() {
fmt.Printf("%#v\n", args.Name)
}
```
```shell
$ ./example --name=foo.bar
main.NameDotName{Head:"foo", Tail:"bar"}
@ -446,7 +418,6 @@ func main() {
fmt.Printf("%#v\n", args.Name)
}
```
```shell
$ ./example --help
Usage: test [--name NAME]
@ -461,6 +432,8 @@ main.NameDotName{Head:"file", Tail:"txt"}
### Custom placeholders
*Introduced in version 1.3.0*
Use the `placeholder` tag to control which placeholder text is used in the usage text.
```go
@ -472,7 +445,6 @@ var args struct {
}
arg.MustParse(&args)
```
```shell
$ ./example -h
Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
@ -549,6 +521,8 @@ For more information visit github.com/alexflint/go-arg
### Subcommands
*Introduced in version 1.1.0*
Subcommands are commonly used in tools that wish to group multiple functions into a single program. An example is the `git` tool:
```shell
$ git checkout [arguments specific to checking out code]
@ -609,187 +583,15 @@ if p.Subcommand() == nil {
}
```
### Custom handling of --help and --version
The following reproduces the internal logic of `MustParse` for the simple case where
you are not using subcommands or --version. This allows you to respond
programatically to --help, and to any errors that come up.
```go
var args struct {
Something string
}
p, err := arg.NewParser(arg.Config{}, &args)
if err != nil {
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
}
err = p.Parse(os.Args[1:])
switch {
case err == arg.ErrHelp: // indicates that user wrote "--help" on command line
p.WriteHelp(os.Stdout)
os.Exit(0)
case err != nil:
fmt.Printf("error: %v\n", err)
p.WriteUsage(os.Stdout)
os.Exit(1)
}
```
```shell
$ go run ./example --help
Usage: ./example --something SOMETHING
Options:
--something SOMETHING
--help, -h display this help and exit
$ ./example --wrong
error: unknown argument --wrong
Usage: ./example --something SOMETHING
$ ./example
error: --something is required
Usage: ./example --something SOMETHING
```
To also handle --version programatically, use the following:
```go
type args struct {
Something string
}
func (args) Version() string {
return "1.2.3"
}
func main() {
var args args
p, err := arg.NewParser(arg.Config{}, &args)
if err != nil {
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
}
err = p.Parse(os.Args[1:])
switch {
case err == arg.ErrHelp: // found "--help" on command line
p.WriteHelp(os.Stdout)
os.Exit(0)
case err == arg.ErrVersion: // found "--version" on command line
fmt.Println(args.Version())
os.Exit(0)
case err != nil:
fmt.Printf("error: %v\n", err)
p.WriteUsage(os.Stdout)
os.Exit(1)
}
fmt.Printf("got %q\n", args.Something)
}
```
```shell
$ ./example --version
1.2.3
$ go run ./example --help
1.2.3
Usage: example --something SOMETHING
Options:
--something SOMETHING
--help, -h display this help and exit
$ ./example --wrong
1.2.3
error: unknown argument --wrong
Usage: example --something SOMETHING
$ ./example
error: --something is required
Usage: example --something SOMETHING
```
To generate subcommand-specific help messages, use the following most general version
(this also works in absence of subcommands but is a bit more complex):
```go
type fetchCmd struct {
Count int
}
type args struct {
Something string
Fetch *fetchCmd `arg:"subcommand"`
}
func (args) Version() string {
return "1.2.3"
}
func main() {
var args args
p, err := arg.NewParser(arg.Config{}, &args)
if err != nil {
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
}
err = p.Parse(os.Args[1:])
switch {
case err == arg.ErrHelp: // found "--help" on command line
p.WriteHelpForSubcommand(os.Stdout, p.SubcommandNames()...)
os.Exit(0)
case err == arg.ErrVersion: // found "--version" on command line
fmt.Println(args.Version())
os.Exit(0)
case err != nil:
fmt.Printf("error: %v\n", err)
p.WriteUsageForSubcommand(os.Stdout, p.SubcommandNames()...)
os.Exit(1)
}
}
```
```shell
$ ./example --version
1.2.3
$ ./example --help
1.2.3
Usage: example [--something SOMETHING] <command> [<args>]
Options:
--something SOMETHING
--help, -h display this help and exit
--version display version and exit
Commands:
fetch
$ ./example fetch --help
1.2.3
Usage: example fetch [--count COUNT]
Options:
--count COUNT
Global options:
--something SOMETHING
--help, -h display this help and exit
--version display version and exit
```
### API Documentation
https://pkg.go.dev/github.com/alexflint/go-arg
https://godoc.org/github.com/alexflint/go-arg
### Rationale
There are many command line argument parsing libraries for Go, including one in the standard library, so why build another?
The `flag` library that ships in the standard library seems awkward to me. Positional arguments must precede options, so `./prog x --foo=1` does what you expect but `./prog --foo=1 x` does not. It also does not allow arguments to have both long (`--foo`) and short (`-f`) forms.
The `flag` library that ships in the standard library seems awkward to me. Positional arguments must preceed options, so `./prog x --foo=1` does what you expect but `./prog --foo=1 x` does not. It also does not allow arguments to have both long (`--foo`) and short (`-f`) forms.
Many third-party argument parsing libraries are great for writing sophisticated command line interfaces, but feel to me like overkill for a simple script with a few flags.
@ -797,4 +599,4 @@ The idea behind `go-arg` is that Go already has an excellent way to describe dat
### Backward compatibility notes
Earlier versions of this library required the help text to be part of the `arg` tag. This is still supported but is now deprecated. Instead, you should use a separate `help` tag, described above, which makes it possible to include commas inside help text.
Earlier versions of this library required the help text to be part of the `arg` tag. This is still supported but is now deprecated. Instead, you should use a separate `help` tag, described above, which removes most of the limits on the text you can write. In particular, you will need to use the new `help` tag if your help text includes any commas.

13
go.mod
View File

@ -1,14 +1,3 @@
module go.wit.com/dev/alexflint/arg
require (
github.com/alexflint/go-scalar v1.2.0
github.com/stretchr/testify v1.7.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0 // indirect
)
go 1.18
go 1.21.4

16
go.sum
View File

@ -1,16 +0,0 @@
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

124
parse.go
View File

@ -11,7 +11,7 @@ import (
"reflect"
"strings"
scalar "github.com/alexflint/go-scalar"
"go.wit.com/dev/alexflint/scalar"
)
// path represents a sequence of steps to find the output location for an
@ -56,7 +56,7 @@ type spec struct {
env string // the name of the environment variable for this option, or empty for none
defaultValue reflect.Value // default value for this option
defaultString string // default value for this option, in string form to be displayed in help text
placeholder string // placeholder string in help
placeholder string // name of the data in help
}
// command represents a named subcommand, or the top-level command
@ -76,22 +76,21 @@ var ErrHelp = errors.New("help requested by user")
// ErrVersion indicates that the builtin --version was provided
var ErrVersion = errors.New("version requested by user")
// for monkey patching in example and test code
// for monkey patching in example code
var mustParseExit = os.Exit
var mustParseOut io.Writer = os.Stdout
// This stores the args sent from modules
var register []interface{}
/*
This allows you to have common arg values defined in a GO package
Use this in your packages to register
variables with go-arg. Then add this to your init()
package 'foo'
function init() {
args.Register(&argsFoo)
}
*/
// This stores the args sent from the GO packages
var register []interface{}
func Register(dest ...interface{}) {
register = append(register, dest...)
}
@ -99,15 +98,23 @@ func Register(dest ...interface{}) {
// MustParse processes command line arguments and exits upon failure
func MustParse(dest ...interface{}) *Parser {
register = append(register, dest...)
return mustParse(Config{Exit: mustParseExit, Out: mustParseOut}, register...)
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 {
fmt.Fprintln(config.Out, err)
config.Exit(2)
config.Exit(-1)
return nil
}
@ -124,24 +131,8 @@ func Parse(dest ...interface{}) error {
return p.Parse(flags())
}
// pass in a "pretend" os.Args. Used for bash autocomplete
func ParseFlags(flags []string, dest ...interface{}) (*Parser, error) {
p, err := NewParser(Config{}, dest...)
if err != nil {
return p, err
}
overrideFlags = append(overrideFlags, flags...)
err = p.Parse(flags)
return p, err
}
var overrideFlags []string
// flags gets all command line arguments other than the first (program name)
func flags() []string {
if len(overrideFlags) > 0 {
return overrideFlags
}
if len(os.Args) == 0 { // os.Args could be empty
return nil
}
@ -164,9 +155,6 @@ type Config struct {
// subcommand
StrictSubcommands bool
// EnvPrefix instructs the library to use a name prefix when reading environment variables.
EnvPrefix string
// Exit is called to terminate the process with an error code (defaults to os.Exit)
Exit func(int)
@ -271,7 +259,7 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", t))
}
cmd, err := cmdFromStruct(name, path{root: i}, t, config.EnvPrefix)
cmd, err := cmdFromStruct(name, path{root: i}, t)
if err != nil {
return nil, err
}
@ -318,17 +306,10 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
}
}
// Set the parent of the subcommands to be the top-level command
// to make sure that global options work when there is more than one
// dest supplied.
for _, subcommand := range p.cmd.subcommands {
subcommand.parent = p.cmd
}
return &p, nil
}
func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*command, error) {
func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
// commands can only be created from pointers to structs
if t.Kind() != reflect.Ptr {
return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a %s",
@ -379,8 +360,9 @@ func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*c
spec.help = help
}
// process each comma-separated part of the tag
var isSubcommand bool
// Look at the tag
var isSubcommand bool // tracks whether this field is a subcommand
for _, key := range strings.Split(tag, ",") {
if key == "" {
continue
@ -415,9 +397,9 @@ func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*c
case key == "env":
// Use override name if provided
if value != "" {
spec.env = envPrefix + value
spec.env = value
} else {
spec.env = envPrefix + strings.ToUpper(field.Name)
spec.env = strings.ToUpper(field.Name)
}
case key == "subcommand":
// decide on a name for the subcommand
@ -432,7 +414,7 @@ func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*c
}
// parse the subcommand recursively
subcmd, err := cmdFromStruct(cmdnames[0], subdest, field.Type, envPrefix)
subcmd, err := cmdFromStruct(cmdnames[0], subdest, field.Type)
if err != nil {
errs = append(errs, err.Error())
return false
@ -450,7 +432,6 @@ func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*c
}
}
// placeholder is the string used in the help text like this: "--somearg PLACEHOLDER"
placeholder, hasPlaceholder := field.Tag.Lookup("placeholder")
if hasPlaceholder {
spec.placeholder = placeholder
@ -536,15 +517,8 @@ func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*c
return &cmd, nil
}
// Parse processes the given command line option, storing the results in the fields
// of the structs from which NewParser was constructed.
//
// It returns ErrHelp if "--help" is one of the command line args and ErrVersion if
// "--version" is one of the command line args (the latter only applies if the
// destination struct passed to NewParser implements Versioned.)
//
// To respond to --help and --version in the way that MustParse does, see examples
// in the README under "Custom handling of --help and --version".
// Parse processes the given command line option, storing the results in the field
// of the structs from which NewParser was constructed
func (p *Parser) Parse(args []string) error {
err := p.process(args)
if err != nil {
@ -658,7 +632,7 @@ func (p *Parser) process(args []string) error {
// must use explicit for loop, not range, because we manipulate i inside the loop
for i := 0; i < len(args); i++ {
arg := args[i]
if arg == "--" && !allpositional {
if arg == "--" {
allpositional = true
continue
}
@ -733,7 +707,7 @@ func (p *Parser) process(args []string) error {
if spec.cardinality == multiple {
var values []string
if value == "" {
for i+1 < len(args) && isValue(args[i+1], spec.field.Type, specs) && args[i+1] != "--" {
for i+1 < len(args) && !isFlag(args[i+1]) && args[i+1] != "--" {
values = append(values, args[i+1])
i++
if spec.separate {
@ -761,7 +735,7 @@ func (p *Parser) process(args []string) error {
if i+1 == len(args) {
return fmt.Errorf("missing value for %s", arg)
}
if !isValue(args[i+1], spec.field.Type, specs) {
if !nextIsNumeric(spec.field.Type, args[i+1]) && isFlag(args[i+1]) {
return fmt.Errorf("missing value for %s", arg)
}
value = args[i+1]
@ -786,13 +760,13 @@ func (p *Parser) process(args []string) error {
if spec.cardinality == multiple {
err := setSliceOrMap(p.val(spec.dest), positionals, true)
if err != nil {
return fmt.Errorf("error processing %s: %v", spec.placeholder, err)
return fmt.Errorf("error processing %s: %v", spec.field.Name, err)
}
positionals = nil
} else {
err := scalar.ParseValue(p.val(spec.dest), positionals[0])
if err != nil {
return fmt.Errorf("error processing %s: %v", spec.placeholder, err)
return fmt.Errorf("error processing %s: %v", spec.field.Name, err)
}
positionals = positionals[1:]
}
@ -807,13 +781,18 @@ func (p *Parser) process(args []string) error {
continue
}
name := strings.ToLower(spec.field.Name)
if spec.long != "" && !spec.positional {
name = "--" + spec.long
}
if spec.required {
if spec.short == "" && spec.long == "" {
msg := fmt.Sprintf("environment variable %s is required", spec.env)
return errors.New(msg)
}
msg := fmt.Sprintf("%s is required", spec.placeholder)
msg := fmt.Sprintf("%s is required", name)
if spec.env != "" {
msg += " (or environment variable " + spec.env + ")"
}
@ -834,29 +813,22 @@ func (p *Parser) process(args []string) error {
return nil
}
// isFlag returns true if a token is a flag such as "-v" or "--user" but not "-" or "--"
func isFlag(s string) bool {
return strings.HasPrefix(s, "-") && strings.TrimLeft(s, "-") != ""
}
// isValue returns true if a token should be consumed as a value for a flag of type t. This
// is almost always the inverse of isFlag. The one exception is for negative numbers, in which
// case we check the list of active options and return true if its not present there.
func isValue(s string, t reflect.Type, specs []*spec) bool {
func nextIsNumeric(t reflect.Type, s string) bool {
switch t.Kind() {
case reflect.Ptr, reflect.Slice:
return isValue(s, t.Elem(), specs)
case reflect.Ptr:
return nextIsNumeric(t.Elem(), s)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
v := reflect.New(t)
err := scalar.ParseValue(v, s)
// if value can be parsed and is not an explicit option declared elsewhere, then use it as a value
if err == nil && (!strings.HasPrefix(s, "-") || findOption(specs, strings.TrimPrefix(s, "-")) == nil) {
return true
return err == nil
default:
return false
}
}
// default case that is used in all cases other than negative numbers: inverse of isFlag
return !isFlag(s)
// isFlag returns true if a token is a flag such as "-v" or "--user" but not "-" or "--"
func isFlag(s string) bool {
return strings.HasPrefix(s, "-") && strings.TrimLeft(s, "-") != ""
}
// val returns a reflect.Value corresponding to the current value for the

View File

@ -7,7 +7,7 @@ import (
"unicode"
"unicode/utf8"
scalar "github.com/alexflint/go-scalar"
"go.wit.com/dev/alexflint/scalar"
)
var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()

View File

@ -5,7 +5,7 @@ import (
"reflect"
"strings"
scalar "github.com/alexflint/go-scalar"
"go.wit.com/dev/alexflint/scalar"
)
// setSliceOrMap parses a sequence of strings into a slice or map. If clear is

10
test/Makefile Normal file
View File

@ -0,0 +1,10 @@
all:
@echo
@echo
test:
redomod:
rm -f go.*
GO111MODULE= go mod init
GO111MODULE= go mod tidy

View File

@ -163,7 +163,6 @@ func Example_helpText() {
// This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
@ -196,17 +195,17 @@ func Example_helpPlaceholder() {
// This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
// output:
// Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
//
// Positional arguments:
// SRC
// DST
//
// Options:
// --optimize LEVEL, -O LEVEL
// optimization level
@ -236,7 +235,6 @@ func Example_helpTextWithSubcommand() {
// This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
@ -274,7 +272,6 @@ func Example_helpTextWhenUsingSubcommand() {
// This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
@ -395,7 +392,6 @@ func Example_errorText() {
// This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
@ -419,7 +415,6 @@ func Example_errorTextForSubcommand() {
// This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
@ -455,7 +450,6 @@ func Example_subcommand() {
// This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)

11
test/go.mod Normal file
View File

@ -0,0 +1,11 @@
module go.wit.com/dev/alexflint/arg/test
go 1.21.4
require github.com/stretchr/testify v1.8.4
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

10
test/go.sum Normal file
View File

@ -0,0 +1,10 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -28,11 +28,11 @@ func parse(cmdline string, dest interface{}) error {
}
func pparse(cmdline string, dest interface{}) (*Parser, error) {
return parseWithEnv(Config{}, cmdline, nil, dest)
return parseWithEnv(cmdline, nil, dest)
}
func parseWithEnv(config Config, cmdline string, env []string, dest interface{}) (*Parser, error) {
p, err := NewParser(config, dest)
func parseWithEnv(cmdline string, env []string, dest interface{}) (*Parser, error) {
p, err := NewParser(Config{}, dest)
if err != nil {
return nil, err
}
@ -120,91 +120,17 @@ func TestNegativeInt(t *testing.T) {
assert.EqualValues(t, args.Foo, -100)
}
func TestNegativeFloat(t *testing.T) {
var args struct {
Foo float64
}
err := parse("-foo -99", &args)
require.NoError(t, err)
assert.EqualValues(t, args.Foo, -99)
}
func TestNumericFlag(t *testing.T) {
var args struct {
UseIPv6 bool `arg:"-6"`
Foo int
}
err := parse("-6", &args)
require.NoError(t, err)
assert.EqualValues(t, args.UseIPv6, true)
}
func TestNumericFlagTakesPrecedence(t *testing.T) {
var args struct {
UseIPv6 bool `arg:"-6"`
Foo int
}
err := parse("-foo -6", &args)
require.Error(t, err)
}
func TestRepeatedNegativeInts(t *testing.T) {
var args struct {
Ints []int `arg:"--numbers"`
}
err := parse("--numbers -1 -2 -6", &args)
require.NoError(t, err)
assert.EqualValues(t, args.Ints, []int{-1, -2, -6})
}
func TestRepeatedNegativeFloats(t *testing.T) {
var args struct {
Floats []float32 `arg:"--numbers"`
}
err := parse("--numbers -1 -2 -6", &args)
require.NoError(t, err)
assert.EqualValues(t, args.Floats, []float32{-1, -2, -6})
}
func TestRepeatedNegativeFloatsThenNumericFlag(t *testing.T) {
var args struct {
Floats []float32 `arg:"--numbers"`
UseIPv6 bool `arg:"-6"`
}
err := parse("--numbers -1 -2 -6", &args)
require.NoError(t, err)
assert.EqualValues(t, args.Floats, []float32{-1, -2})
assert.True(t, args.UseIPv6)
}
func TestRepeatedNegativeFloatsThenNonexistentFlag(t *testing.T) {
var args struct {
Floats []float32 `arg:"--numbers"`
UseIPv6 bool `arg:"-6"`
}
err := parse("--numbers -1 -2 -n", &args)
require.Error(t, err, "unknown argument -n")
}
func TestRepeatedNegativeIntsThenFloat(t *testing.T) {
var args struct {
Ints []int `arg:"--numbers"`
}
err := parse("--numbers -1 -2 -0.1", &args)
require.Error(t, err, "unknown argument -0.1")
}
func TestNegativeIntAndFloatAndTricks(t *testing.T) {
var args struct {
Foo int
Bar float64
N int `arg:"--100"`
}
err := parse("-foo -99 -bar -60.14 -100 -101", &args)
err := parse("-foo -100 -bar -60.14 -100 -100", &args)
require.NoError(t, err)
assert.EqualValues(t, args.Foo, -99)
assert.EqualValues(t, args.Foo, -100)
assert.EqualValues(t, args.Bar, -60.14)
assert.EqualValues(t, args.N, -101)
assert.EqualValues(t, args.N, -100)
}
func TestUint(t *testing.T) {
@ -305,7 +231,7 @@ func TestRequiredWithEnvOnly(t *testing.T) {
var args struct {
Foo string `arg:"required,--,-,env:FOO"`
}
_, err := parseWithEnv(Config{}, "", []string{}, &args)
_, err := parseWithEnv("", []string{}, &args)
require.Error(t, err, "environment variable FOO is required")
}
@ -599,6 +525,15 @@ func TestMissingValueInMiddle(t *testing.T) {
assert.Error(t, err)
}
func TestNegativeValue(t *testing.T) {
var args struct {
Foo int
}
err := parse("--foo -123", &args)
require.NoError(t, err)
assert.Equal(t, -123, args.Foo)
}
func TestInvalidInt(t *testing.T) {
var args struct {
Foo int
@ -674,15 +609,6 @@ func TestNoMoreOptionsBeforeHelp(t *testing.T) {
assert.NotEqual(t, ErrHelp, err)
}
func TestNoMoreOptionsTwice(t *testing.T) {
var args struct {
X []string `arg:"positional"`
}
err := parse("-- --", &args)
require.NoError(t, err)
assert.Equal(t, []string{"--"}, args.X)
}
func TestHelpFlag(t *testing.T) {
var args struct {
Foo string
@ -766,26 +692,11 @@ func TestMustParse(t *testing.T) {
assert.NotNil(t, parser)
}
func TestMustParseError(t *testing.T) {
var args struct {
Foo []string `default:""`
}
var exitCode int
var stdout bytes.Buffer
mustParseExit = func(code int) { exitCode = code }
mustParseOut = &stdout
os.Args = []string{"example"}
parser := MustParse(&args)
assert.Nil(t, parser)
assert.Equal(t, 2, exitCode)
assert.Contains(t, stdout.String(), "default values are not supported for slice or map fields")
}
func TestEnvironmentVariable(t *testing.T) {
var args struct {
Foo string `arg:"env"`
}
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
_, err := parseWithEnv("", []string{"FOO=bar"}, &args)
require.NoError(t, err)
assert.Equal(t, "bar", args.Foo)
}
@ -794,7 +705,7 @@ func TestEnvironmentVariableNotPresent(t *testing.T) {
var args struct {
NotPresent string `arg:"env"`
}
_, err := parseWithEnv(Config{}, "", nil, &args)
_, err := parseWithEnv("", nil, &args)
require.NoError(t, err)
assert.Equal(t, "", args.NotPresent)
}
@ -803,7 +714,7 @@ func TestEnvironmentVariableOverrideName(t *testing.T) {
var args struct {
Foo string `arg:"env:BAZ"`
}
_, err := parseWithEnv(Config{}, "", []string{"BAZ=bar"}, &args)
_, err := parseWithEnv("", []string{"BAZ=bar"}, &args)
require.NoError(t, err)
assert.Equal(t, "bar", args.Foo)
}
@ -812,7 +723,7 @@ func TestEnvironmentVariableOverrideArgument(t *testing.T) {
var args struct {
Foo string `arg:"env"`
}
_, err := parseWithEnv(Config{}, "--foo zzz", []string{"FOO=bar"}, &args)
_, err := parseWithEnv("--foo zzz", []string{"FOO=bar"}, &args)
require.NoError(t, err)
assert.Equal(t, "zzz", args.Foo)
}
@ -821,7 +732,7 @@ func TestEnvironmentVariableError(t *testing.T) {
var args struct {
Foo int `arg:"env"`
}
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
_, err := parseWithEnv("", []string{"FOO=bar"}, &args)
assert.Error(t, err)
}
@ -829,7 +740,7 @@ func TestEnvironmentVariableRequired(t *testing.T) {
var args struct {
Foo string `arg:"env,required"`
}
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
_, err := parseWithEnv("", []string{"FOO=bar"}, &args)
require.NoError(t, err)
assert.Equal(t, "bar", args.Foo)
}
@ -838,7 +749,7 @@ func TestEnvironmentVariableSliceArgumentString(t *testing.T) {
var args struct {
Foo []string `arg:"env"`
}
_, err := parseWithEnv(Config{}, "", []string{`FOO=bar,"baz, qux"`}, &args)
_, err := parseWithEnv("", []string{`FOO=bar,"baz, qux"`}, &args)
require.NoError(t, err)
assert.Equal(t, []string{"bar", "baz, qux"}, args.Foo)
}
@ -847,7 +758,7 @@ func TestEnvironmentVariableSliceEmpty(t *testing.T) {
var args struct {
Foo []string `arg:"env"`
}
_, err := parseWithEnv(Config{}, "", []string{`FOO=`}, &args)
_, err := parseWithEnv("", []string{`FOO=`}, &args)
require.NoError(t, err)
assert.Len(t, args.Foo, 0)
}
@ -856,7 +767,7 @@ func TestEnvironmentVariableSliceArgumentInteger(t *testing.T) {
var args struct {
Foo []int `arg:"env"`
}
_, err := parseWithEnv(Config{}, "", []string{`FOO=1,99`}, &args)
_, err := parseWithEnv("", []string{`FOO=1,99`}, &args)
require.NoError(t, err)
assert.Equal(t, []int{1, 99}, args.Foo)
}
@ -865,7 +776,7 @@ func TestEnvironmentVariableSliceArgumentFloat(t *testing.T) {
var args struct {
Foo []float32 `arg:"env"`
}
_, err := parseWithEnv(Config{}, "", []string{`FOO=1.1,99.9`}, &args)
_, err := parseWithEnv("", []string{`FOO=1.1,99.9`}, &args)
require.NoError(t, err)
assert.Equal(t, []float32{1.1, 99.9}, args.Foo)
}
@ -874,7 +785,7 @@ func TestEnvironmentVariableSliceArgumentBool(t *testing.T) {
var args struct {
Foo []bool `arg:"env"`
}
_, err := parseWithEnv(Config{}, "", []string{`FOO=true,false,0,1`}, &args)
_, err := parseWithEnv("", []string{`FOO=true,false,0,1`}, &args)
require.NoError(t, err)
assert.Equal(t, []bool{true, false, false, true}, args.Foo)
}
@ -883,7 +794,7 @@ func TestEnvironmentVariableSliceArgumentWrongCsv(t *testing.T) {
var args struct {
Foo []int `arg:"env"`
}
_, err := parseWithEnv(Config{}, "", []string{`FOO=1,99\"`}, &args)
_, err := parseWithEnv("", []string{`FOO=1,99\"`}, &args)
assert.Error(t, err)
}
@ -891,7 +802,7 @@ func TestEnvironmentVariableSliceArgumentWrongType(t *testing.T) {
var args struct {
Foo []bool `arg:"env"`
}
_, err := parseWithEnv(Config{}, "", []string{`FOO=one,two`}, &args)
_, err := parseWithEnv("", []string{`FOO=one,two`}, &args)
assert.Error(t, err)
}
@ -899,7 +810,7 @@ func TestEnvironmentVariableMap(t *testing.T) {
var args struct {
Foo map[int]string `arg:"env"`
}
_, err := parseWithEnv(Config{}, "", []string{`FOO=1=one,99=ninetynine`}, &args)
_, err := parseWithEnv("", []string{`FOO=1=one,99=ninetynine`}, &args)
require.NoError(t, err)
assert.Len(t, args.Foo, 2)
assert.Equal(t, "one", args.Foo[1])
@ -910,21 +821,11 @@ func TestEnvironmentVariableEmptyMap(t *testing.T) {
var args struct {
Foo map[int]string `arg:"env"`
}
_, err := parseWithEnv(Config{}, "", []string{`FOO=`}, &args)
_, err := parseWithEnv("", []string{`FOO=`}, &args)
require.NoError(t, err)
assert.Len(t, args.Foo, 0)
}
func TestEnvironmentVariableWithPrefix(t *testing.T) {
var args struct {
Foo string `arg:"env"`
}
_, err := parseWithEnv(Config{EnvPrefix: "MYAPP_"}, "", []string{"MYAPP_FOO=bar"}, &args)
require.NoError(t, err)
assert.Equal(t, "bar", args.Foo)
}
func TestEnvironmentVariableIgnored(t *testing.T) {
var args struct {
Foo string `arg:"env"`
@ -957,7 +858,7 @@ func TestRequiredEnvironmentOnlyVariableIsMissing(t *testing.T) {
Foo string `arg:"required,--,env:FOO"`
}
_, err := parseWithEnv(Config{}, "", []string{""}, &args)
_, err := parseWithEnv("", []string{""}, &args)
assert.Error(t, err)
}
@ -966,7 +867,7 @@ func TestOptionalEnvironmentOnlyVariable(t *testing.T) {
Foo string `arg:"env:FOO"`
}
_, err := parseWithEnv(Config{}, "", []string{}, &args)
_, err := parseWithEnv("", []string{}, &args)
assert.NoError(t, err)
}
@ -1005,7 +906,7 @@ func TestParserMustParse(t *testing.T) {
}{
{name: "help", args: struct{}{}, cmdLine: []string{"--help"}, code: 0, output: "display this help and exit"},
{name: "version", args: versioned{}, cmdLine: []string{"--version"}, code: 0, output: "example 3.2.1"},
{name: "invalid", args: struct{}{}, cmdLine: []string{"invalid"}, code: 2, output: ""},
{name: "invalid", args: struct{}{}, cmdLine: []string{"invalid"}, code: -1, output: ""},
}
for _, tt := range tests {
@ -1655,7 +1556,7 @@ func TestMustParseInvalidParser(t *testing.T) {
}
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
assert.Nil(t, parser)
assert.Equal(t, 2, exitCode)
assert.Equal(t, -1, exitCode)
}
func TestMustParsePrintsHelp(t *testing.T) {
@ -1836,11 +1737,3 @@ func TestSubcommandGlobalFlag_InCommand_Strict_Inner(t *testing.T) {
require.NotNil(t, args.Sub)
assert.True(t, args.Sub.Guard)
}
func TestExitFunctionAndOutStreamGetFilledIn(t *testing.T) {
var args struct{}
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
assert.NotNil(t, p.config.Exit) // go prohibits function pointer comparison
assert.Equal(t, p.config.Out, os.Stdout)
}

View File

@ -237,7 +237,7 @@ func (versioned) Version() string {
}
func TestUsageWithVersion(t *testing.T) {
expectedUsage := "Usage: example"
expectedUsage := "example 3.2.1\nUsage: example"
expectedHelp := `
example 3.2.1
@ -260,233 +260,6 @@ Options:
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestUsageWithUserDefinedVersionFlag(t *testing.T) {
expectedUsage := "Usage: example [--version]"
expectedHelp := `
Usage: example [--version]
Options:
--version this is a user-defined version flag
--help, -h display this help and exit
`
var args struct {
ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
}
os.Args[0] = "example"
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestUsageWithVersionAndUserDefinedVersionFlag(t *testing.T) {
expectedUsage := "Usage: example [--version]"
expectedHelp := `
Usage: example [--version]
Options:
--version this is a user-defined version flag
--help, -h display this help and exit
`
var args struct {
versioned
ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
}
os.Args[0] = "example"
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
type subcommand struct {
Number int `arg:"-n,--number" help:"compute something on the given number"`
}
func TestUsageWithVersionAndSubcommand(t *testing.T) {
expectedUsage := "Usage: example <command> [<args>]"
expectedHelp := `
example 3.2.1
Usage: example <command> [<args>]
Options:
--help, -h display this help and exit
--version display version and exit
Commands:
cmd
`
var args struct {
versioned
Cmd *subcommand `arg:"subcommand"`
}
os.Args[0] = "example"
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
expectedUsage = "Usage: example cmd [--number NUMBER]"
expectedHelp = `
example 3.2.1
Usage: example cmd [--number NUMBER]
Options:
--number NUMBER, -n NUMBER
compute something on the given number
--help, -h display this help and exit
--version display version and exit
`
_ = p.Parse([]string{"cmd"})
help = bytes.Buffer{}
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
usage = bytes.Buffer{}
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestUsageWithUserDefinedVersionFlagAndSubcommand(t *testing.T) {
expectedUsage := "Usage: example [--version] <command> [<args>]"
expectedHelp := `
Usage: example [--version] <command> [<args>]
Options:
--version this is a user-defined version flag
--help, -h display this help and exit
Commands:
cmd
`
var args struct {
Cmd *subcommand `arg:"subcommand"`
ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
}
os.Args[0] = "example"
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
expectedUsage = "Usage: example cmd [--number NUMBER]"
expectedHelp = `
Usage: example cmd [--number NUMBER]
Options:
--number NUMBER, -n NUMBER
compute something on the given number
Global options:
--version this is a user-defined version flag
--help, -h display this help and exit
`
_ = p.Parse([]string{"cmd"})
help = bytes.Buffer{}
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
usage = bytes.Buffer{}
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestUsageWithVersionAndUserDefinedVersionFlagAndSubcommand(t *testing.T) {
expectedUsage := "Usage: example [--version] <command> [<args>]"
expectedHelp := `
Usage: example [--version] <command> [<args>]
Options:
--version this is a user-defined version flag
--help, -h display this help and exit
Commands:
cmd
`
var args struct {
versioned
Cmd *subcommand `arg:"subcommand"`
ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
}
os.Args[0] = "example"
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
expectedUsage = "Usage: example cmd [--number NUMBER]"
expectedHelp = `
Usage: example cmd [--number NUMBER]
Options:
--number NUMBER, -n NUMBER
compute something on the given number
Global options:
--version this is a user-defined version flag
--help, -h display this help and exit
`
_ = p.Parse([]string{"cmd"})
help = bytes.Buffer{}
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
usage = bytes.Buffer{}
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
type described struct{}
// Described returns the description for this program
@ -642,50 +415,6 @@ Options:
assert.Equal(t, expectedUsage, usage.String())
}
func TestUsageWithSubcommands(t *testing.T) {
expectedUsage := "Usage: example child [--values VALUES]"
expectedHelp := `
Usage: example child [--values VALUES]
Options:
--values VALUES Values
Global options:
--verbose, -v verbosity level
--help, -h display this help and exit
`
var args struct {
Verbose bool `arg:"-v" help:"verbosity level"`
Child *struct {
Values []float64 `help:"Values"`
} `arg:"subcommand:child"`
}
os.Args[0] = "example"
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
_ = p.Parse([]string{"child"})
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var help2 bytes.Buffer
p.WriteHelpForSubcommand(&help2, "child")
assert.Equal(t, expectedHelp[1:], help2.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
var usage2 bytes.Buffer
p.WriteUsageForSubcommand(&usage2, "child")
assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String()))
}
func TestUsageWithNestedSubcommands(t *testing.T) {
expectedUsage := "Usage: example child nested [--enable] OUTPUT"
@ -795,35 +524,6 @@ Options:
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestUsageWithEmptyPlaceholder(t *testing.T) {
expectedUsage := "Usage: example [-a] [--b] [--c]"
expectedHelp := `
Usage: example [-a] [--b] [--c]
Options:
-a some help for a
--b some help for b
--c, -c some help for c
--help, -h display this help and exit
`
var args struct {
ShortOnly string `arg:"-a,--" placeholder:"" help:"some help for a"`
LongOnly string `arg:"--b" placeholder:"" help:"some help for b"`
Both string `arg:"-c,--c" placeholder:"" help:"some help for c"`
}
p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestUsageWithShortFirst(t *testing.T) {
expectedUsage := "Usage: example [-c CAT] [--dog DOG]"
@ -932,7 +632,7 @@ error: something went wrong
p.Fail("something went wrong")
assert.Equal(t, expectedStdout[1:], stdout.String())
assert.Equal(t, 2, exitCode)
assert.Equal(t, -1, exitCode)
}
func TestFailSubcommand(t *testing.T) {
@ -955,7 +655,7 @@ error: something went wrong
require.NoError(t, err)
assert.Equal(t, expectedStdout[1:], stdout.String())
assert.Equal(t, 2, exitCode)
assert.Equal(t, -1, exitCode)
}
type lengthOf struct {
@ -1015,72 +715,3 @@ Commands:
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
}
func TestHelpShowsPositionalWithDefault(t *testing.T) {
expectedHelp := `
Usage: example [FOO]
Positional arguments:
FOO this is a positional with a default [default: bar]
Options:
--help, -h display this help and exit
`
var args struct {
Foo string `arg:"positional" default:"bar" help:"this is a positional with a default"`
}
p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
}
func TestHelpShowsPositionalWithEnv(t *testing.T) {
expectedHelp := `
Usage: example [FOO]
Positional arguments:
FOO this is a positional with an env variable [env: FOO]
Options:
--help, -h display this help and exit
`
var args struct {
Foo string `arg:"positional,env:FOO" help:"this is a positional with an env variable"`
}
p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
}
func TestHelpShowsPositionalWithDefaultAndEnv(t *testing.T) {
expectedHelp := `
Usage: example [FOO]
Positional arguments:
FOO this is a positional with a default and an env variable [default: bar, env: FOO]
Options:
--help, -h display this help and exit
`
var args struct {
Foo string `arg:"positional,env:FOO" default:"bar" help:"this is a positional with a default and an env variable"`
}
p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
}

View File

@ -9,13 +9,13 @@ import (
// the width of the left column
const colWidth = 25
// Fail prints usage information to p.Config.Out and exits with status code 2.
// Fail prints usage information to stderr and exits with non-zero status
func (p *Parser) Fail(msg string) {
p.FailSubcommand(msg)
}
// FailSubcommand prints usage information for a specified subcommand to p.Config.Out,
// then exits with status code 2. To write usage information for a top-level
// FailSubcommand prints usage information for a specified subcommand to stderr,
// then exits with non-zero status. To write usage information for a top-level
// subcommand, provide just the name of that subcommand. To write usage
// information for a subcommand that is nested under another subcommand, provide
// a sequence of subcommand names starting with the top-level subcommand and so
@ -27,7 +27,7 @@ func (p *Parser) FailSubcommand(msg string, subcommand ...string) error {
}
fmt.Fprintln(p.config.Out, "error:", msg)
p.config.Exit(2)
p.config.Exit(-1)
return nil
}
@ -59,6 +59,10 @@ func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) erro
}
}
if p.version != "" {
fmt.Fprintln(w, p.version)
}
// print the beginning of the usage string
fmt.Fprintf(w, "Usage: %s", p.cmd.name)
for _, s := range subcommand {
@ -204,9 +208,6 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
positionals = append(positionals, spec)
case spec.long != "":
longOptions = append(longOptions, spec)
if spec.long == "version" {
hasVersionOption = true
}
case spec.short != "":
shortOptions = append(shortOptions, spec)
case spec.short == "" && spec.long == "":
@ -214,36 +215,16 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
}
}
// obtain a flattened list of options from all ancestors
// also determine if any ancestor has a version option spec
var globals []*spec
ancestor := cmd.parent
for ancestor != nil {
for _, spec := range ancestor.specs {
if spec.long == "version" {
hasVersionOption = true
break
}
}
globals = append(globals, ancestor.specs...)
ancestor = ancestor.parent
}
if p.description != "" {
fmt.Fprintln(w, p.description)
}
if !hasVersionOption && p.version != "" {
fmt.Fprintln(w, p.version)
}
p.WriteUsageForSubcommand(w, subcommand...)
// write the list of positionals
if len(positionals) > 0 {
fmt.Fprint(w, "\nPositional arguments:\n")
for _, spec := range positionals {
print(w, spec.placeholder, spec.help, withDefault(spec.defaultString), withEnv(spec.env))
print(w, spec.placeholder, spec.help)
}
}
@ -255,14 +236,28 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
}
for _, spec := range longOptions {
p.printOption(w, spec)
if spec.long == "version" {
hasVersionOption = true
}
}
}
// obtain a flattened list of options from all ancestors
var globals []*spec
ancestor := cmd.parent
for ancestor != nil {
globals = append(globals, ancestor.specs...)
ancestor = ancestor.parent
}
// write the list of global options
if len(globals) > 0 {
fmt.Fprint(w, "\nGlobal options:\n")
for _, spec := range globals {
p.printOption(w, spec)
if spec.long == "version" {
hasVersionOption = true
}
}
}
@ -333,10 +328,7 @@ func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) {
}
func synopsis(spec *spec, form string) string {
// if the user omits the placeholder tag then we pick one automatically,
// but if the user explicitly specifies an empty placeholder then we
// leave out the placeholder in the help message
if spec.cardinality == zero || spec.placeholder == "" {
if spec.cardinality == zero {
return form
}
return form + " " + spec.placeholder