Compare commits

...

19 Commits

Author SHA1 Message Date
Alex Flint c046f49e12 drop go.work and add it to .gitignore 2022-10-29 15:28:22 -04:00
Alex Flint f2539d7ad2 add go.work -- maybe remove before merge? 2022-10-29 12:28:46 -04:00
Alex Flint 2ffe24630b add mdtest command to generate and run tests from a markdown file 2022-10-07 14:14:01 -07:00
Alex Flint 47ff44303f drop support for help tag inside arg tag 2022-10-07 12:51:55 -07:00
Alex Flint 60a0117880 update readme for v2 (still has some TODOs) 2022-10-07 12:51:27 -07:00
Alex Flint 55d9025329 rename "accumulatedArgs" -> "accessible" 2022-10-04 13:54:53 -07:00
Alex Flint 0769dd5839 add tests for new Process* and OverwriteWith* functions 2022-10-04 13:51:51 -07:00
Alex Flint 84b7154efc add TestSliceWithEqualsSign 2022-10-04 13:25:01 -07:00
Alex Flint 1cc263f9f2 add processSequence and make it responsible for respecting "overwrite" 2022-10-04 13:23:57 -07:00
Alex Flint b365ec0781 add processSingle and make it responsible for checking whether an argument has been seen before 2022-10-04 13:12:38 -07:00
Alex Flint 64288c5521 add appendToSlice, appendToMap, appendToSliceOrMap 2022-10-04 12:48:04 -07:00
Alex Flint 2775f58376 add OverwriteWithOptions, OverwriteWithCommandLine 2022-10-04 12:34:53 -07:00
Alex Flint 5f0c48f092 move construction logic out of parse.go into construct.go 2022-10-04 11:54:22 -07:00
Alex Flint 5ca19cd72d cleaned up the test helpers parse, pparse, and parseWithEnv: now all are just using "parse" 2022-10-04 11:39:58 -07:00
Alex Flint 4aea783023 changed NewParser to take options at the end rather than config at the front 2022-10-04 11:28:34 -07:00
Alex Flint a1e2b672ea add a test to check that default values can be ignored if needed 2022-10-04 11:06:19 -07:00
Alex Flint 22f214d7ed added test that library does not directly access environment variables from OS 2022-10-04 11:02:57 -07:00
Alex Flint 09d28e1195 split the parsing logic into ProcessEnvironment, ProcessCommandLine, ProcessOptions, ProcessPositions, ProcessDefaults 2022-10-04 11:00:42 -07:00
Alex Flint 2e6284635a drop support for multiple destination structs 2022-10-04 08:56:31 -07:00
23 changed files with 5537 additions and 168 deletions

1
.gitignore vendored
View File

@ -22,3 +22,4 @@ _testmain.go
*.exe
*.test
*.prof
go.work

325
README.md
View File

@ -16,6 +16,12 @@
Declare command line arguments for your program by defining a struct.
```go
import "github.com/go-arg/v2
```
TODO
```go
var args struct {
Foo string
@ -33,7 +39,7 @@ hello true
### Installation
```shell
go get github.com/alexflint/go-arg
go get github.com/alexflint/go-arg/v2
```
### Required arguments
@ -90,36 +96,6 @@ $ WORKERS=4 ./example --workers=6
Workers: 6
```
You can also override the name of the environment variable:
```go
var args struct {
Workers int `arg:"env:NUM_WORKERS"`
}
arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```
$ NUM_WORKERS=4 ./example
Workers: 4
```
You can provide multiple values using the CSV (RFC 4180) format:
```go
var args struct {
Workers []int `arg:"env"`
}
arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```
$ WORKERS='1,99' ./example
Workers: [1 99]
```
### Usage strings
```go
var args struct {
@ -158,47 +134,23 @@ var args struct {
arg.MustParse(&args)
```
### Default values (before v1.2)
### Overriding the name of an environment variable
```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 {
Test string `arg:"-t,env:TEST" default:"something"`
Workers int `arg:"env:NUM_WORKERS"`
}
arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
#### Ignoring environment variables and/or default values
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)
```
$ NUM_WORKERS=4 ./example
Workers: 4
```
### Arguments with multiple values
```go
var args struct {
Database string
@ -213,23 +165,6 @@ fmt.Printf("Fetching the following IDs from %s: %q", args.Database, args.IDs)
Fetching the following IDs from foo: [1 2 3]
```
### Arguments that can be specified multiple times, mixed with positionals
```go
var args struct {
Commands []string `arg:"-c,separate"`
Files []string `arg:"-f,separate"`
Databases []string `arg:"positional"`
}
arg.MustParse(&args)
```
```shell
./example -c cmd1 db1 -f file1 db2 -c cmd2 -f file2 -f file3 db3 -c cmd3
Commands: [cmd1 cmd2 cmd3]
Files [file1 file2 file3]
Databases [db1 db2 db3]
```
### Arguments with keys and values
```go
var args struct {
@ -266,7 +201,7 @@ error: you must provide either --foo or --bar
```go
type args struct {
...
// ...
}
func (args) Version() string {
@ -353,7 +288,7 @@ The following types may be used as arguments:
- maps using any of the above as keys and values
- any type that implements `encoding.TextUnmarshaler`
### Custom parsing
### Custom parsing
Implement `encoding.TextUnmarshaler` to define your own parsing logic.
@ -391,73 +326,121 @@ Usage: example [--name NAME]
error: error processing --name: missing period in "oops"
```
### Custom parsing with default values
### Slice-valued environment variables
Implement `encoding.TextMarshaler` to define your own default value strings:
```go
// Accepts command line arguments of the form "head.tail"
type NameDotName struct {
Head, Tail string
}
func (n *NameDotName) UnmarshalText(b []byte) error {
// same as previous example
}
// this is only needed if you want to display a default value in the usage string
func (n *NameDotName) MarshalText() ([]byte, error) {
return []byte(fmt.Sprintf("%s.%s", n.Head, n.Tail)), nil
}
func main() {
var args struct {
Name NameDotName `default:"file.txt"`
}
arg.MustParse(&args)
fmt.Printf("%#v\n", args.Name)
}
```
```shell
$ ./example --help
Usage: test [--name NAME]
Options:
--name NAME [default: file.txt]
--help, -h display this help and exit
$ ./example
main.NameDotName{Head:"file", Tail:"txt"}
```
### Custom placeholders
*Introduced in version 1.3.0*
Use the `placeholder` tag to control which placeholder text is used in the usage text.
You can provide multiple values using the CSV (RFC 4180) format:
```go
var args struct {
Input string `arg:"positional" placeholder:"SRC"`
Output []string `arg:"positional" placeholder:"DST"`
Optimize int `arg:"-O" help:"optimization level" placeholder:"LEVEL"`
MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" placeholder:"N"`
Workers []int `arg:"env"`
}
arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```
$ WORKERS='1,99' ./example
Workers: [1 99]
```
### Parsing command line tokens and environment variables from a slice
You can override the command line tokens and environment variables processed by go-arg:
```go
var args struct {
Samsara int
Nirvana float64 `arg:"env:NIRVANA"`
}
p, err := arg.NewParser(&args)
if err != nil {
log.Fatal(err)
}
cmdline := []string{"./thisprogram", "--samsara=123"}
environ := []string{"NIRVANA=45.6"}
err = p.Parse(cmdline, environ)
if err != nil {
log.Fatal(err)
}
```
```
./example
SAMSARA: 123
NIRVANA: 45.6
```
### Configuration files
TODO
### Combining command line options, environment variables, and default values
By default, command line arguments take precedence over environment variables, which take precedence over default values. This means that we check whether a certain option was provided on the command line, then if not, we check for an environment variable (only if an `env` tag was provided), then, if none is found, we check for a `default` tag.
```go
var args struct {
Test string `arg:"-t,env:TEST" default:"something"`
}
arg.MustParse(&args)
```
### Changing precedence of command line options, environment variables, and default values
You can use the low-level functions `Process*` and `OverwriteWith*` to control which things override which other things. Here is an example in which environment variables take precedence over command line options, which is the opposite of the default behavior:
```go
var args struct {
Test string `arg:"env:TEST"`
}
p, err := arg.NewParser(&args)
if err != nil {
log.Fatal(err)
}
err = p.ParseCommandLine(os.Args)
if err != nil {
p.Fail(err.Error())
}
err = p.OverwriteWithEnvironment(os.Environ())
if err != nil {
p.Fail(err.Error())
}
err = p.Validate()
if err != nil {
p.Fail(err.Error())
}
fmt.Printf("test=%s\n", args.Test)
```
```
TEST=value_from_env ./example --test=value_from_option
test=value_from_env
```
### Ignoring environment variables
TODO
### Ignoring default values
TODO
### Arguments that can be specified multiple times
```go
var args struct {
Commands []string `arg:"-c,separate"`
Files []string `arg:"-f,separate"`
}
arg.MustParse(&args)
```
```shell
$ ./example -h
Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
Positional arguments:
SRC
DST
Options:
--optimize LEVEL, -O LEVEL
optimization level
--maxjobs N, -j N maximum number of simultaneous jobs
--help, -h display this help and exit
./example -c cmd1 -f file1 -c cmd2 -f file2 -f file3 -c cmd3
Commands: [cmd1 cmd2 cmd3]
Files [file1 file2 file3]
```
### Description strings
@ -521,18 +504,14 @@ 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:
Subcommands are commonly used in tools that group multiple functions into a single program. An example is the `git` tool:
```shell
$ git checkout [arguments specific to checking out code]
$ git commit [arguments specific to committing]
$ git push [arguments specific to pushing]
$ git commit [arguments specific to committing code]
$ git push [arguments specific to pushing code]
```
The strings "checkout", "commit", and "push" are different from simple positional arguments because the options available to the user change depending on which subcommand they choose.
This can be implemented with `go-arg` as follows:
This can be implemented with `go-arg` with the `arg:"subcommand"` tag:
```go
type CheckoutCmd struct {
@ -567,14 +546,9 @@ case args.Push != nil:
}
```
Some additional rules apply when working with subcommands:
* The `subcommand` tag can only be used with fields that are pointers to structs
* Any struct that contains a subcommand must not contain any positionals
Note that the `subcommand` tag can only be used with fields that are pointers to structs, and that any struct that contains subcommands cannot also contain positionals.
This package allows to have a program that accepts subcommands, but also does something else
when no subcommands are specified.
If on the other hand you want the program to terminate when no subcommands are specified,
the recommended way is:
### Terminating when no subcommands are specified
```go
p := arg.MustParse(&args)
@ -583,20 +557,39 @@ if p.Subcommand() == nil {
}
```
### Customizing placeholder strings
Use the `placeholder` tag to control which placeholder text is used in the usage text.
```go
var args struct {
Input string `arg:"positional" placeholder:"SRC"`
Output []string `arg:"positional" placeholder:"DST"`
Optimize int `arg:"-O" placeholder:"LEVEL"`
MaxJobs int `arg:"-j" placeholder:"N"`
}
arg.MustParse(&args)
```
```shell
$ ./example -h
Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
Positional arguments:
SRC
DST
Options:
--optimize LEVEL, -O LEVEL
--maxjobs N, -j N
--help, -h display this help and exit
```
### API Documentation
https://godoc.org/github.com/alexflint/go-arg
### Rationale
### Migrating from v1.x
There are many command line argument parsing libraries for Go, including one in the standard library, so why build another?
Migrating IgnoreEnv to passing a nil environ
The `flag` library that ships in the standard library seems awkward to me. Positional arguments must 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.
The idea behind `go-arg` is that Go already has an excellent way to describe data structures using structs, so there is no need to develop additional levels of abstraction. Instead of one API to specify which arguments your program accepts, and then another API to get the values of those arguments, `go-arg` replaces both with a single struct.
### Backward compatibility notes
Earlier versions of this library required the help text to be part of the `arg` tag. This is still supported but is now deprecated. Instead, you should use a separate `help` tag, described above, which 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.
Migrating from IgnoreDefault to calling ProcessCommandLine

2
go.sum
View File

@ -1,5 +1,3 @@
github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM=
github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
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=

11
mdtest/example1.go.tpl Normal file
View File

@ -0,0 +1,11 @@
package main
import (
"github.com/alexflint/go-arg/v2"
{{if contains .Code "fmt."}}"fmt"{{end}}
{{if contains .Code "strings."}}"strings"{{end}}
)
func main() {
{{.Code}}
}

9
mdtest/example2.go.tpl Normal file
View File

@ -0,0 +1,9 @@
package main
import (
"github.com/alexflint/go-arg/v2"
{{if contains .Code "fmt."}}"fmt"{{end}}
{{if contains .Code "strings."}}"strings"{{end}}
)
{{.Code}}

179
mdtest/mdtest.go Normal file
View File

@ -0,0 +1,179 @@
// mdtest executes code blocks in markdown and checks that they run as expected
package main
import (
"bytes"
"context"
_ "embed"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"text/template"
"time"
"github.com/alexflint/go-arg/v2"
)
// var pattern = "```go(.*)```\\s*```\\s*\\$(.*)\\n(.*)```"
var pattern = "(?s)```go([^`]*?)```\\s*```([^`]*?)```" //go(.*)```\\s*```\\s*\\$(.*)\\n(.*)```"
var re = regexp.MustCompile(pattern)
var funcs = map[string]any{
"contains": strings.Contains,
}
//go:embed example1.go.tpl
var templateSource1 string
//go:embed example2.go.tpl
var templateSource2 string
var t1 = template.Must(template.New("example1.go").Funcs(funcs).Parse(templateSource1))
var t2 = template.Must(template.New("example2.go").Funcs(funcs).Parse(templateSource2))
type payload struct {
Code string
}
func runCode(ctx context.Context, code []byte, cmd string) ([]byte, error) {
dir, err := os.MkdirTemp("", "")
if err != nil {
return nil, fmt.Errorf("error creating temp dir to build and run code: %w", err)
}
fmt.Println(dir)
fmt.Println(strings.Repeat("-", 80))
srcpath := filepath.Join(dir, "src.go")
binpath := filepath.Join(dir, "example")
// If the code contains a main function then use t2, otherwise use t1
t := t1
if strings.Contains(string(code), "func main") {
t = t2
}
var b bytes.Buffer
err = t.Execute(&b, payload{Code: string(code)})
if err != nil {
return nil, fmt.Errorf("error executing template for source file: %w", err)
}
fmt.Println(b.String())
fmt.Println(strings.Repeat("-", 80))
err = os.WriteFile(srcpath, b.Bytes(), os.ModePerm)
if err != nil {
return nil, fmt.Errorf("error writing temporary source file: %w", err)
}
compiler, err := exec.LookPath("go")
if err != nil {
return nil, fmt.Errorf("could not find path to go compiler: %w", err)
}
buildCmd := exec.CommandContext(ctx, compiler, "build", "-o", binpath, srcpath)
out, err := buildCmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("error building source: %w. Compiler said:\n%s", err, string(out))
}
// replace "./example" with full path to compiled program
var env, args []string
var found bool
for _, part := range strings.Split(cmd, " ") {
if found {
args = append(args, part)
} else if part == "./example" {
found = true
} else {
env = append(env, part)
}
}
runCmd := exec.CommandContext(ctx, binpath, args...)
runCmd.Env = env
output, err := runCmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("error runing example: %w. Program said:\n%s", err, string(output))
}
// Clean up the temp dir
if err := os.RemoveAll(dir); err != nil {
return nil, fmt.Errorf("error deleting temp dir: %w", err)
}
return output, nil
}
func Main() error {
ctx := context.Background()
var args struct {
Input string `arg:"positional,required"`
}
arg.MustParse(&args)
buf, err := os.ReadFile(args.Input)
if err != nil {
return err
}
fmt.Println(strings.Repeat("=", 80))
matches := re.FindAllSubmatchIndex(buf, -1)
for k, match := range matches {
codebegin, codeend := match[2], match[3]
code := buf[codebegin:codeend]
shellbegin, shellend := match[4], match[5]
shell := buf[shellbegin:shellend]
lines := strings.Split(string(shell), "\n")
for i := 0; i < len(lines); i++ {
if strings.HasPrefix(lines[i], "$") && strings.Contains(lines[i], "./example") {
cmd := strings.TrimSpace(strings.TrimPrefix(lines[i], "$"))
var output []string
i++
for i < len(lines) && !strings.HasPrefix(lines[i], "$") {
output = append(output, lines[i])
i++
}
expected := strings.TrimSpace(strings.Join(output, "\n"))
fmt.Println(string(code))
fmt.Println(strings.Repeat("-", 80))
fmt.Println(string(cmd))
fmt.Println(strings.Repeat("-", 80))
fmt.Println(string(expected))
fmt.Println(strings.Repeat("-", 80))
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
actual, err := runCode(ctx, code, cmd)
if err != nil {
return fmt.Errorf("error running example %d: %w\nCode was:\n%s", k, err, string(code))
}
fmt.Println(string(actual))
fmt.Println(strings.Repeat("=", 80))
}
}
}
fmt.Printf("found %d matches\n", len(matches))
return nil
}
func main() {
if err := Main(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

319
v2/construct.go Normal file
View File

@ -0,0 +1,319 @@
package arg
import (
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
)
// Argument represents a command line argument
type Argument struct {
dest path
field reflect.StructField // the struct field from which this option was created
long string // the --long form for this option, or empty if none
short string // the -s short form for this option, or empty if none
cardinality cardinality // determines how many tokens will be present (possible values: zero, one, multiple)
required bool // if true, this option must be present on the command line
positional bool // if true, this option will be looked for in the positional flags
separate bool // if true, each slice and map entry will have its own --flag
help string // the help text for this option
env string // the name of the environment variable for this option, or empty for none
defaultVal string // default value for this option
placeholder string // name of the data in help
}
// Command represents a named subcommand, or the top-level command
type Command struct {
name string
help string
dest path
args []*Argument
subcommands []*Command
parent *Command
}
// Parser represents a set of command line options with destination values
type Parser struct {
cmd *Command // the top-level command
root reflect.Value // destination struct to fill will values
version string // version from the argument struct
prologue string // prologue for help text (from the argument struct)
epilogue string // epilogue for help text (from the argument struct)
// the following fields are updated during processing of command line arguments
leaf *Command // the subcommand we processed last
accessible []*Argument // concatenation of the leaf subcommand's arguments plus all ancestors' arguments
seen map[*Argument]bool // the arguments we encountered while processing command line arguments
}
// Versioned is the interface that the destination struct should implement to
// make a version string appear at the top of the help message.
type Versioned interface {
// Version returns the version string that will be printed on a line by itself
// at the top of the help message.
Version() string
}
// Described is the interface that the destination struct should implement to
// make a description string appear at the top of the help message.
type Described interface {
// Description returns the string that will be printed on a line by itself
// at the top of the help message.
Description() string
}
// Epilogued is the interface that the destination struct should implement to
// add an epilogue string at the bottom of the help message.
type Epilogued interface {
// Epilogue returns the string that will be printed on a line by itself
// at the end of the help message.
Epilogue() string
}
// the ParserOption interface matches options for the parser constructor
type ParserOption interface {
parserOption()
}
type programNameParserOption struct {
s string
}
func (programNameParserOption) parserOption() {}
// WithProgramName overrides the name of the program as displayed in help test
func WithProgramName(name string) ParserOption {
return programNameParserOption{s: name}
}
// NewParser constructs a parser from a list of destination structs
func NewParser(dest interface{}, options ...ParserOption) (*Parser, error) {
// check the destination type
t := reflect.TypeOf(dest)
if t.Kind() != reflect.Ptr {
panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", t))
}
// pick a program name for help text and usage output
program := "program"
if len(os.Args) > 0 {
program = filepath.Base(os.Args[0])
}
// apply the options
for _, opt := range options {
switch opt := opt.(type) {
case programNameParserOption:
program = opt.s
}
}
// build the root command from the struct
cmd, err := cmdFromStruct(program, path{}, t)
if err != nil {
return nil, err
}
// construct the parser
p := Parser{
seen: make(map[*Argument]bool),
root: reflect.ValueOf(dest),
cmd: cmd,
}
// copy the args for the root command into "accessible", which will
// grow each time we open up a subcommand
p.accessible = make([]*Argument, len(p.cmd.args))
copy(p.accessible, p.cmd.args)
// check for version, prologue, and epilogue
if dest, ok := dest.(Versioned); ok {
p.version = dest.Version()
}
if dest, ok := dest.(Described); ok {
p.prologue = dest.Description()
}
if dest, ok := dest.(Epilogued); ok {
p.epilogue = dest.Epilogue()
}
return &p, nil
}
func cmdFromStruct(name string, dest path, t reflect.Type) (*Command, error) {
// commands can only be created from pointers to structs
if t.Kind() != reflect.Ptr {
return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a %s",
dest, t.Kind())
}
t = t.Elem()
if t.Kind() != reflect.Struct {
return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a pointer to %s",
dest, t.Kind())
}
cmd := Command{
name: name,
dest: dest,
}
var errs []string
walkFields(t, func(field reflect.StructField, t reflect.Type) bool {
// check for the ignore switch in the tag
tag := field.Tag.Get("arg")
if tag == "-" {
return false
}
// if this is an embedded struct then recurse into its fields, even if
// it is unexported, because exported fields on unexported embedded
// structs are still writable
if field.Anonymous && field.Type.Kind() == reflect.Struct {
return true
}
// ignore any other unexported field
if !isExported(field.Name) {
return false
}
// create a new destination path for this field
subdest := dest.Child(field)
arg := Argument{
dest: subdest,
field: field,
long: strings.ToLower(field.Name),
}
help, exists := field.Tag.Lookup("help")
if exists {
arg.help = help
}
defaultVal, hasDefault := field.Tag.Lookup("default")
if hasDefault {
arg.defaultVal = defaultVal
}
// Look at the tag
var isSubcommand bool // tracks whether this field is a subcommand
for _, key := range strings.Split(tag, ",") {
if key == "" {
continue
}
key = strings.TrimLeft(key, " ")
var value string
if pos := strings.Index(key, ":"); pos != -1 {
value = key[pos+1:]
key = key[:pos]
}
switch {
case strings.HasPrefix(key, "---"):
errs = append(errs, fmt.Sprintf("%s.%s: too many hyphens", t.Name(), field.Name))
case strings.HasPrefix(key, "--"):
arg.long = key[2:]
case strings.HasPrefix(key, "-"):
if len(key) != 2 {
errs = append(errs, fmt.Sprintf("%s.%s: short arguments must be one character only",
t.Name(), field.Name))
return false
}
arg.short = key[1:]
case key == "required":
if hasDefault {
errs = append(errs, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified",
t.Name(), field.Name))
return false
}
arg.required = true
case key == "positional":
arg.positional = true
case key == "separate":
arg.separate = true
case key == "env":
// Use override name if provided
if value != "" {
arg.env = value
} else {
arg.env = strings.ToUpper(field.Name)
}
case key == "subcommand":
// decide on a name for the subcommand
cmdname := value
if cmdname == "" {
cmdname = strings.ToLower(field.Name)
}
// parse the subcommand recursively
subcmd, err := cmdFromStruct(cmdname, subdest, field.Type)
if err != nil {
errs = append(errs, err.Error())
return false
}
subcmd.parent = &cmd
subcmd.help = field.Tag.Get("help")
cmd.subcommands = append(cmd.subcommands, subcmd)
isSubcommand = true
default:
errs = append(errs, fmt.Sprintf("unrecognized tag '%s' on field %s", key, tag))
return false
}
}
placeholder, hasPlaceholder := field.Tag.Lookup("placeholder")
if hasPlaceholder {
arg.placeholder = placeholder
} else if arg.long != "" {
arg.placeholder = strings.ToUpper(arg.long)
} else {
arg.placeholder = strings.ToUpper(arg.field.Name)
}
// Check whether this field is supported. It's good to do this here rather than
// wait until ParseValue because it means that a program with invalid argument
// fields will always fail regardless of whether the arguments it received
// exercised those fields.
if !isSubcommand {
cmd.args = append(cmd.args, &arg)
var err error
arg.cardinality, err = cardinalityOf(field.Type)
if err != nil {
errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported",
t.Name(), field.Name, field.Type.String()))
return false
}
if arg.cardinality == multiple && hasDefault {
errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields",
t.Name(), field.Name))
return false
}
}
// if this was an embedded field then we already returned true up above
return false
})
if len(errs) > 0 {
return nil, errors.New(strings.Join(errs, "\n"))
}
// check that we don't have both positionals and subcommands
var hasPositional bool
for _, arg := range cmd.args {
if arg.positional {
hasPositional = true
}
}
if hasPositional && len(cmd.subcommands) > 0 {
return nil, fmt.Errorf("%s cannot have both subcommands and positional arguments", dest)
}
return &cmd, nil
}

25
v2/construct_test.go Normal file
View File

@ -0,0 +1,25 @@
package arg
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestInvalidTag(t *testing.T) {
var args struct {
Foo string `arg:"this_is_not_valid"`
}
_, err := NewParser(&args)
assert.Error(t, err)
}
func TestUnexportedFieldsSkipped(t *testing.T) {
var args struct {
unexported struct{}
}
_, err := NewParser(&args)
require.NoError(t, err)
}

39
v2/doc.go Normal file
View File

@ -0,0 +1,39 @@
// Package arg parses command line arguments using the fields from a struct.
//
// For example,
//
// var args struct {
// Iter int
// Debug bool
// }
// arg.MustParse(&args)
//
// defines two command line arguments, which can be set using any of
//
// ./example --iter=1 --debug // debug is a boolean flag so its value is set to true
// ./example -iter 1 // debug defaults to its zero value (false)
// ./example --debug=true // iter defaults to its zero value (zero)
//
// The fastest way to see how to use go-arg is to read the examples below.
//
// Fields can be bool, string, any float type, or any signed or unsigned integer type.
// They can also be slices of any of the above, or slices of pointers to any of the above.
//
// Tags can be specified using the `arg` and `help` tag names:
//
// var args struct {
// Input string `arg:"positional"`
// Log string `arg:"positional,required"`
// Debug bool `arg:"-d" help:"turn on debug mode"`
// RealMode bool `arg:"--real"
// Wr io.Writer `arg:"-"`
// }
//
// Any tag string that starts with a single hyphen is the short form for an argument
// (e.g. `./example -d`), and any tag string that starts with two hyphens is the long
// form for the argument (instead of the field name).
//
// Other valid tag strings are `positional` and `required`.
//
// Fields can be excluded from processing with `arg:"-"`.
package arg

507
v2/example_test.go Normal file
View File

@ -0,0 +1,507 @@
package arg
import (
"fmt"
"net"
"net/mail"
"net/url"
"os"
"strings"
"time"
)
func split(s string) []string {
return strings.Split(s, " ")
}
// This example demonstrates basic usage
func Example() {
// These are the args you would pass in on the command line
os.Args = split("./example --foo=hello --bar")
var args struct {
Foo string
Bar bool
}
MustParse(&args)
fmt.Println(args.Foo, args.Bar)
// output: hello true
}
// This example demonstrates arguments that have default values
func Example_defaultValues() {
// These are the args you would pass in on the command line
os.Args = split("./example")
var args struct {
Foo string `default:"abc"`
}
MustParse(&args)
fmt.Println(args.Foo)
// output: abc
}
// This example demonstrates arguments that are required
func Example_requiredArguments() {
// These are the args you would pass in on the command line
os.Args = split("./example --foo=abc --bar")
var args struct {
Foo string `arg:"required"`
Bar bool
}
MustParse(&args)
fmt.Println(args.Foo, args.Bar)
// output: abc true
}
// This example demonstrates positional arguments
func Example_positionalArguments() {
// These are the args you would pass in on the command line
os.Args = split("./example in out1 out2 out3")
var args struct {
Input string `arg:"positional"`
Output []string `arg:"positional"`
}
MustParse(&args)
fmt.Println("In:", args.Input)
fmt.Println("Out:", args.Output)
// output:
// In: in
// Out: [out1 out2 out3]
}
// This example demonstrates arguments that have multiple values
func Example_multipleValues() {
// The args you would pass in on the command line
os.Args = split("./example --database localhost --ids 1 2 3")
var args struct {
Database string
IDs []int64
}
MustParse(&args)
fmt.Printf("Fetching the following IDs from %s: %v", args.Database, args.IDs)
// output: Fetching the following IDs from localhost: [1 2 3]
}
// This example demonstrates arguments with keys and values
func Example_mappings() {
// The args you would pass in on the command line
os.Args = split("./example --userids john=123 mary=456")
var args struct {
UserIDs map[string]int
}
MustParse(&args)
fmt.Println(args.UserIDs)
// output: map[john:123 mary:456]
}
type commaSeparated struct {
M map[string]string
}
func (c *commaSeparated) UnmarshalText(b []byte) error {
c.M = make(map[string]string)
for _, part := range strings.Split(string(b), ",") {
pos := strings.Index(part, "=")
if pos == -1 {
return fmt.Errorf("error parsing %q, expected format key=value", part)
}
c.M[part[:pos]] = part[pos+1:]
}
return nil
}
// This example demonstrates arguments with keys and values separated by commas
func Example_mappingWithCommas() {
// The args you would pass in on the command line
os.Args = split("./example --values one=two,three=four")
var args struct {
Values commaSeparated
}
MustParse(&args)
fmt.Println(args.Values.M)
// output: map[one:two three:four]
}
// This eample demonstrates multiple value arguments that can be mixed with
// other arguments.
func Example_multipleMixed() {
os.Args = split("./example -c cmd1 db1 -f file1 db2 -c cmd2 -f file2 -f file3 db3 -c cmd3")
var args struct {
Commands []string `arg:"-c,separate"`
Files []string `arg:"-f,separate"`
Databases []string `arg:"positional"`
}
MustParse(&args)
fmt.Println("Commands:", args.Commands)
fmt.Println("Files:", args.Files)
fmt.Println("Databases:", args.Databases)
// output:
// Commands: [cmd1 cmd2 cmd3]
// Files: [file1 file2 file3]
// Databases: [db1 db2 db3]
}
// This example shows the usage string generated by go-arg
func Example_helpText() {
// These are the args you would pass in on the command line
os.Args = split("./example --help")
var args struct {
Input string `arg:"positional,required"`
Output []string `arg:"positional"`
Verbose bool `arg:"-v" help:"verbosity level"`
Dataset string `help:"dataset to use"`
Optimize int `arg:"-O,--optim" help:"optimization level"`
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stdout = os.Stdout
MustParse(&args)
// output:
// Usage: example [--verbose] [--dataset DATASET] [--optim OPTIM] INPUT [OUTPUT [OUTPUT ...]]
//
// Positional arguments:
// INPUT
// OUTPUT
//
// Options:
// --verbose, -v verbosity level
// --dataset DATASET dataset to use
// --optim OPTIM, -O OPTIM
// optimization level
// --help, -h display this help and exit
}
// This example shows the usage string generated by go-arg with customized placeholders
func Example_helpPlaceholder() {
// These are the args you would pass in on the command line
os.Args = split("./example --help")
var args struct {
Input string `arg:"positional,required" placeholder:"SRC"`
Output []string `arg:"positional" placeholder:"DST"`
Optimize int `arg:"-O" help:"optimization level" placeholder:"LEVEL"`
MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" placeholder:"N"`
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stdout = os.Stdout
MustParse(&args)
// output:
// Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
// Positional arguments:
// SRC
// DST
// Options:
// --optimize LEVEL, -O LEVEL
// optimization level
// --maxjobs N, -j N maximum number of simultaneous jobs
// --help, -h display this help and exit
}
// This example shows the usage string generated by go-arg when using subcommands
func Example_helpTextWithSubcommand() {
// These are the args you would pass in on the command line
os.Args = split("./example --help")
type getCmd struct {
Item string `arg:"positional" help:"item to fetch"`
}
type listCmd struct {
Format string `help:"output format"`
Limit int
}
var args struct {
Verbose bool
Get *getCmd `arg:"subcommand" help:"fetch an item and print it"`
List *listCmd `arg:"subcommand" help:"list available items"`
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stdout = os.Stdout
MustParse(&args)
// output:
// Usage: example [--verbose] <command> [<args>]
//
// Options:
// --verbose
// --help, -h display this help and exit
//
// Commands:
// get fetch an item and print it
// list list available items
}
// This example shows the usage string generated by go-arg when using subcommands
func Example_helpTextWhenUsingSubcommand() {
// These are the args you would pass in on the command line
os.Args = split("./example get --help")
type getCmd struct {
Item string `arg:"positional,required" help:"item to fetch"`
}
type listCmd struct {
Format string `help:"output format"`
Limit int
}
var args struct {
Verbose bool
Get *getCmd `arg:"subcommand" help:"fetch an item and print it"`
List *listCmd `arg:"subcommand" help:"list available items"`
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stdout = os.Stdout
MustParse(&args)
// output:
// Usage: example get ITEM
//
// Positional arguments:
// ITEM item to fetch
//
// Global options:
// --verbose
// --help, -h display this help and exit
}
// This example shows how to print help for an explicit subcommand
func Example_writeHelpForSubcommand() {
// These are the args you would pass in on the command line
os.Args = split("./example get --help")
type getCmd struct {
Item string `arg:"positional" help:"item to fetch"`
}
type listCmd struct {
Format string `help:"output format"`
Limit int
}
var args struct {
Verbose bool
Get *getCmd `arg:"subcommand" help:"fetch an item and print it"`
List *listCmd `arg:"subcommand" help:"list available items"`
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stdout = os.Stdout
p, err := NewParser(&args, WithProgramName("example"))
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = p.WriteHelpForSubcommand(os.Stdout, "list")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// output:
// Usage: example list [--format FORMAT] [--limit LIMIT]
//
// Options:
// --format FORMAT output format
// --limit LIMIT
//
// Global options:
// --verbose
// --help, -h display this help and exit
}
// This example shows how to print help for a subcommand that is nested several levels deep
func Example_writeHelpForSubcommandNested() {
// These are the args you would pass in on the command line
os.Args = split("./example get --help")
type mostNestedCmd struct {
Item string
}
type nestedCmd struct {
MostNested *mostNestedCmd `arg:"subcommand"`
}
type topLevelCmd struct {
Nested *nestedCmd `arg:"subcommand"`
}
var args struct {
TopLevel *topLevelCmd `arg:"subcommand"`
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stdout = os.Stdout
p, err := NewParser(&args, WithProgramName("example"))
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = p.WriteHelpForSubcommand(os.Stdout, "toplevel", "nested", "mostnested")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// output:
// Usage: example toplevel nested mostnested [--item ITEM]
//
// Options:
// --item ITEM
// --help, -h display this help and exit
}
// This example shows the error string generated by go-arg when an invalid option is provided
func Example_errorText() {
// These are the args you would pass in on the command line
os.Args = split("./example --optimize INVALID")
var args struct {
Input string `arg:"positional,required"`
Output []string `arg:"positional"`
Verbose bool `arg:"-v" help:"verbosity level"`
Dataset string `help:"dataset to use"`
Optimize int `arg:"-O,help:optimization level"`
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stderr = os.Stdout
MustParse(&args)
// output:
// Usage: example [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]]
// error: error processing default value for --optimize: strconv.ParseInt: parsing "INVALID": invalid syntax
}
// This example shows the error string generated by go-arg when an invalid option is provided
func Example_errorTextForSubcommand() {
// These are the args you would pass in on the command line
os.Args = split("./example get --count INVALID")
type getCmd struct {
Count int
}
var args struct {
Get *getCmd `arg:"subcommand"`
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stderr = os.Stdout
MustParse(&args)
// output:
// Usage: example get [--count COUNT]
// error: error processing default value for --count: strconv.ParseInt: parsing "INVALID": invalid syntax
}
// This example demonstrates use of subcommands
func Example_subcommand() {
// These are the args you would pass in on the command line
os.Args = split("./example commit -a -m what-this-commit-is-about")
type CheckoutCmd struct {
Branch string `arg:"positional"`
Track bool `arg:"-t"`
}
type CommitCmd struct {
All bool `arg:"-a"`
Message string `arg:"-m"`
}
type PushCmd struct {
Remote string `arg:"positional"`
Branch string `arg:"positional"`
SetUpstream bool `arg:"-u"`
}
var args struct {
Checkout *CheckoutCmd `arg:"subcommand:checkout"`
Commit *CommitCmd `arg:"subcommand:commit"`
Push *PushCmd `arg:"subcommand:push"`
Quiet bool `arg:"-q"` // this flag is global to all subcommands
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stderr = os.Stdout
MustParse(&args)
switch {
case args.Checkout != nil:
fmt.Printf("checkout requested for branch %s\n", args.Checkout.Branch)
case args.Commit != nil:
fmt.Printf("commit requested with message \"%s\"\n", args.Commit.Message)
case args.Push != nil:
fmt.Printf("push requested from %s to %s\n", args.Push.Branch, args.Push.Remote)
}
// output:
// commit requested with message "what-this-commit-is-about"
}
func Example_allSupportedTypes() {
// These are the args you would pass in on the command line
os.Args = []string{}
var args struct {
Bool bool
Byte byte
Rune rune
Int int
Int8 int8
Int16 int16
Int32 int32
Int64 int64
Float32 float32
Float64 float64
String string
Duration time.Duration
URL url.URL
Email mail.Address
MAC net.HardwareAddr
}
// go-arg supports each of the types above, as well as pointers to any of
// the above and slices of any of the above. It also supports any types that
// implements encoding.TextUnmarshaler.
MustParse(&args)
// output:
}

8
v2/go.mod Normal file
View File

@ -0,0 +1,8 @@
module github.com/alexflint/go-arg/v2
require (
github.com/alexflint/go-scalar v1.2.0
github.com/stretchr/testify v1.7.0
)
go 1.13

15
v2/go.sum Normal file
View File

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

561
v2/parse.go Normal file
View File

@ -0,0 +1,561 @@
package arg
import (
"encoding/csv"
"errors"
"fmt"
"os"
"reflect"
"strings"
scalar "github.com/alexflint/go-scalar"
)
// ErrHelp indicates that -h or --help were provided
var ErrHelp = errors.New("help requested by user")
// ErrVersion indicates that --version was provided
var ErrVersion = errors.New("version requested by user")
// MustParse processes command line arguments and exits upon failure
func MustParse(dest interface{}) *Parser {
p, err := NewParser(dest)
if err != nil {
fmt.Fprintln(stdout, err)
osExit(-1)
return nil // just in case osExit was monkey-patched
}
err = p.Parse(os.Args, os.Environ())
switch {
case err == ErrHelp:
p.writeHelpForSubcommand(stdout, p.leaf)
osExit(0)
case err == ErrVersion:
fmt.Fprintln(stdout, p.version)
osExit(0)
case err != nil:
p.failWithSubcommand(err.Error(), p.leaf)
}
return p
}
// Parse processes command line arguments and stores them in dest
func Parse(dest interface{}, options ...ParserOption) error {
p, err := NewParser(dest, options...)
if err != nil {
return err
}
return p.Parse(os.Args, os.Environ())
}
// Parse processes the given command line option, storing the results in the field
// of the structs from which NewParser was constructed
func (p *Parser) Parse(args, env []string) error {
p.seen = make(map[*Argument]bool)
// If -h or --help were specified then make sure help text supercedes other errors
var help bool
for _, arg := range args {
if arg == "-h" || arg == "--help" {
help = true
}
if arg == "--" {
break
}
}
err := p.ProcessCommandLine(args)
if err != nil {
if help {
return ErrHelp
}
return err
}
err = p.ProcessEnvironment(env)
if err != nil {
if help {
return ErrHelp
}
return err
}
err = p.ProcessDefaults()
if err != nil {
if help {
return ErrHelp
}
return err
}
return p.Validate()
}
// ProcessCommandLine scans arguments one-by-one, parses them and assigns
// the result to fields of the struct passed to NewParser. It returns
// an error if an argument is invalid or unknown, but not if a
// required argument is missing. To check that all required arguments
// are set, call Validate(). This function ignores the first element
// of args, which is assumed to be the program name itself. This function
// never overwrites arguments previously seen in a call to any Process*
// function.
func (p *Parser) ProcessCommandLine(args []string) error {
if len(args) == 0 {
return nil
}
positionals, err := p.ProcessOptions(args[1:])
if err != nil {
return err
}
return p.ProcessPositionals(positionals)
}
// OverwriteWithCommandLine is like ProcessCommandLine but it overwrites
// any previously seen values.
func (p *Parser) OverwriteWithCommandLine(args []string) error {
if len(args) == 0 {
return nil
}
positionals, err := p.OverwriteWithOptions(args[1:])
if err != nil {
return err
}
return p.OverwriteWithPositionals(positionals)
}
// ProcessOptions processes options but not positionals from the
// command line. Positionals are returned and can be passed to
// ProcessPositionals. Arguments seen in a previous call to any
// Process* or OverwriteWith* functions are ignored.
func (p *Parser) ProcessOptions(args []string) ([]string, error) {
return p.processOptions(args, false)
}
// OverwriteWithOptions is like ProcessOptions except previously seen
// arguments are overwritten
func (p *Parser) OverwriteWithOptions(args []string) ([]string, error) {
return p.processOptions(args, true)
}
func (p *Parser) processOptions(args []string, overwrite bool) ([]string, error) {
// note that p.cmd.args has already been copied into p.accessible in NewParser
// union of args for the chain of subcommands encountered so far
p.leaf = p.cmd
var allpositional bool
var positionals []string
// must use explicit for loop, not range, because we manipulate i inside the loop
for i := 0; i < len(args); i++ {
token := args[i]
// the "--" token indicates that all further tokens should be treated as positionals
if token == "--" {
allpositional = true
continue
}
// check whether this is a positional argument
if !isFlag(token) || allpositional {
// each subcommand can have either subcommands or positionals, but not both
if len(p.leaf.subcommands) == 0 {
positionals = append(positionals, token)
continue
}
// if we have a subcommand then make sure it is valid for the current context
subcmd := findSubcommand(p.leaf.subcommands, token)
if subcmd == nil {
return nil, fmt.Errorf("invalid subcommand: %s", token)
}
// instantiate the field to point to a new struct
v := p.val(subcmd.dest)
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers
}
// add the new options to the set of allowed options
p.accessible = append(p.accessible, subcmd.args...)
p.leaf = subcmd
continue
}
// check for special --help and --version flags
switch token {
case "-h", "--help":
return nil, ErrHelp
case "--version":
return nil, ErrVersion
}
// check for an equals sign, as in "--foo=bar"
var value string
opt := strings.TrimLeft(token, "-")
if pos := strings.Index(opt, "="); pos != -1 {
value = opt[pos+1:]
opt = opt[:pos]
}
// look up the arg for this option (note that the "args" slice changes as
// we expand subcommands so it is better not to use a map)
arg := findOption(p.accessible, opt)
if arg == nil {
return nil, fmt.Errorf("unknown argument %s", token)
}
// for the case of multiple values, consume tokens until next --option
if arg.cardinality == multiple && !arg.separate {
var values []string
if value == "" {
for i+1 < len(args) && !isFlag(args[i+1]) && args[i+1] != "--" {
values = append(values, args[i+1])
i++
}
} else {
values = append(values, value)
}
if err := p.processSequence(arg, values, overwrite); err != nil {
return nil, err
}
continue
}
// if it's a flag and it has no value then set the value to true
// use boolean because this takes account of TextUnmarshaler
if arg.cardinality == zero && value == "" {
value = "true"
}
// if we have something like "--foo" then the value is the next argument
if value == "" {
if i+1 == len(args) {
return nil, fmt.Errorf("missing value for %s", token)
}
if isFlag(args[i+1]) {
return nil, fmt.Errorf("missing value for %s", token)
}
value = args[i+1]
i++
}
// send the value to the argument
if err := p.processScalar(arg, value, overwrite); err != nil {
return nil, err
}
}
return positionals, nil
}
// ProcessPositionals processes a list of positional arguments. If
// this list contains tokens that begin with a hyphen they will still be
// treated as positional arguments. Arguments seen in a previous call
// to any Process* or OverwriteWith* functions are ignored.
func (p *Parser) ProcessPositionals(positionals []string) error {
return p.processPositionals(positionals, false)
}
// OverwriteWithPositionals is like ProcessPositionals except previously
// seen arguments are overwritten.
func (p *Parser) OverwriteWithPositionals(positionals []string) error {
return p.processPositionals(positionals, true)
}
func (p *Parser) processPositionals(positionals []string, overwrite bool) error {
for _, arg := range p.accessible {
if !arg.positional {
continue
}
if len(positionals) == 0 {
break
}
if arg.cardinality == multiple {
if err := p.processSequence(arg, positionals, overwrite); err != nil {
return err
}
positionals = nil
break
}
if err := p.processScalar(arg, positionals[0], overwrite); err != nil {
return err
}
positionals = positionals[1:]
}
if len(positionals) > 0 {
return fmt.Errorf("too many positional arguments at '%s'", positionals[0])
}
return nil
}
// ProcessEnvironment processes environment variables from a list of strings
// of the form KEY=VALUE. You can pass in os.Environ(). It
// does not overwrite any fields with values already populated.
func (p *Parser) ProcessEnvironment(environ []string) error {
return p.processEnvironment(environ, false)
}
// OverwriteWithEnvironment processes environment variables from a list
// of strings of the form "KEY=VALUE". Any existing values are overwritten.
func (p *Parser) OverwriteWithEnvironment(environ []string) error {
return p.processEnvironment(environ, true)
}
// ProcessEnvironment processes environment variables from a list of strings
// of the form KEY=VALUE. You can pass in os.Environ(). It
// overwrites already-populated fields only if overwrite is true.
func (p *Parser) processEnvironment(environ []string, overwrite bool) error {
// parse the list of KEY=VAL strings in environ
env := make(map[string]string)
for _, s := range environ {
if i := strings.Index(s, "="); i >= 0 {
env[s[:i]] = s[i+1:]
}
}
// process arguments one-by-one
for _, arg := range p.accessible {
if arg.env == "" {
continue
}
value, found := env[arg.env]
if !found {
continue
}
if arg.cardinality == multiple && !arg.separate {
// expect a CSV string in an environment
// variable in the case of multiple values
var values []string
if len(strings.TrimSpace(value)) > 0 {
var err error
values, err = csv.NewReader(strings.NewReader(value)).Read()
if err != nil {
return fmt.Errorf("error parsing CSV string from environment variable %s: %v", arg.env, err)
}
}
if err := p.processSequence(arg, values, overwrite); err != nil {
return fmt.Errorf("error processing environment variable %s: %v", arg.env, err)
}
continue
}
if err := p.processScalar(arg, value, overwrite); err != nil {
return fmt.Errorf("error processing environment variable %s: %v", arg.env, err)
}
}
return nil
}
// ProcessDefaults assigns default values to all fields that have default values and
// are not already populated.
func (p *Parser) ProcessDefaults() error {
return p.processDefaults(false)
}
// OverwriteWithDefaults assigns default values to all fields that have default values,
// overwriting any previous value
func (p *Parser) OverwriteWithDefaults() error {
return p.processDefaults(true)
}
// processDefaults assigns default values to all fields in all expanded subcommands.
// If overwrite is true then it overwrites existing values.
func (p *Parser) processDefaults(overwrite bool) error {
for _, arg := range p.accessible {
if arg.defaultVal == "" {
continue
}
if err := p.processScalar(arg, arg.defaultVal, overwrite); err != nil {
return err
}
}
return nil
}
// processScalar parses a single argument, inserts it into the struct,
// and marks the argument as "seen" (unless the argument has been seen
// before and overwrite=false, in which case the value is ignored)
func (p *Parser) processScalar(arg *Argument, value string, overwrite bool) error {
if p.seen[arg] && !overwrite && !arg.separate {
return nil
}
name := strings.ToLower(arg.field.Name)
if arg.long != "" && !arg.positional {
name = "--" + arg.long
}
if arg.cardinality == multiple {
err := appendToSliceOrMap(p.val(arg.dest), value)
if err != nil {
return fmt.Errorf("error processing default value for %s: %v", name, err)
}
} else {
err := scalar.ParseValue(p.val(arg.dest), value)
if err != nil {
return fmt.Errorf("error processing default value for %s: %v", name, err)
}
}
p.seen[arg] = true
return nil
}
// processSequence parses a sequence argument, inserts it into the struct,
// and marks the argument as "seen" (unless the argument has been seen
// before and overwrite=false, in which case the value is ignored)
func (p *Parser) processSequence(arg *Argument, values []string, overwrite bool) error {
if p.seen[arg] && !overwrite && !arg.separate {
return nil
}
name := strings.ToLower(arg.field.Name)
if arg.long != "" && !arg.positional {
name = "--" + arg.long
}
if arg.cardinality != multiple {
panic(fmt.Sprintf("processSequence called for argument %s which has cardinality %v", arg.field.Name, arg.cardinality))
}
err := setSliceOrMap(p.val(arg.dest), values)
if err != nil {
return fmt.Errorf("error processing default value for %s: %v", name, err)
}
p.seen[arg] = true
return nil
}
// Missing returns a list of required arguments that were not provided
func (p *Parser) Missing() []*Argument {
var missing []*Argument
for _, arg := range p.accessible {
if arg.required && !p.seen[arg] {
missing = append(missing, arg)
}
}
return missing
}
// Validate returns an error if any required arguments were missing
func (p *Parser) Validate() error {
if missing := p.Missing(); len(missing) > 0 {
name := strings.ToLower(missing[0].field.Name)
if missing[0].long != "" && !missing[0].positional {
name = "--" + missing[0].long
}
if missing[0].env == "" {
return fmt.Errorf("%s is required", name)
}
return fmt.Errorf("%s is required (or environment variable %s)", name, missing[0].env)
}
return nil
}
// val returns a reflect.Value corresponding to the current value for the
// given path
func (p *Parser) val(dest path) reflect.Value {
v := p.root
for _, field := range dest.fields {
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return reflect.Value{}
}
v = v.Elem()
}
v = v.FieldByIndex(field.Index)
}
return v
}
// path represents a sequence of steps to find the output location for an
// argument or subcommand in the final destination struct
type path struct {
fields []reflect.StructField // sequence of struct fields to traverse
}
// String gets a string representation of the given path
func (p path) String() string {
s := "args"
for _, f := range p.fields {
s += "." + f.Name
}
return s
}
// Child gets a new path representing a child of this path.
func (p path) Child(f reflect.StructField) path {
// copy the entire slice of fields to avoid possible slice overwrite
subfields := make([]reflect.StructField, len(p.fields)+1)
copy(subfields, p.fields)
subfields[len(subfields)-1] = f
return path{
fields: subfields,
}
}
// walkFields calls a function for each field of a struct, recursively expanding struct fields.
func walkFields(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool) {
walkFieldsImpl(t, visit, nil)
}
func walkFieldsImpl(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool, path []int) {
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
field.Index = make([]int, len(path)+1)
copy(field.Index, append(path, i))
expand := visit(field, t)
if expand && field.Type.Kind() == reflect.Struct {
var subpath []int
if field.Anonymous {
subpath = append(path, i)
}
walkFieldsImpl(field.Type, visit, subpath)
}
}
}
// isFlag returns true if a token is a flag such as "-v" or "--user" but not "-" or "--"
func isFlag(s string) bool {
return strings.HasPrefix(s, "-") && strings.TrimLeft(s, "-") != ""
}
// findOption finds an option from its name, or returns nil if no arg is found
func findOption(args []*Argument, name string) *Argument {
for _, arg := range args {
if arg.positional {
continue
}
if arg.long == name || arg.short == name {
return arg
}
}
return nil
}
// findSubcommand finds a subcommand using its name, or returns nil if no subcommand is found
func findSubcommand(cmds []*Command, name string) *Command {
for _, cmd := range cmds {
if cmd.name == name {
return cmd
}
}
return nil
}

1461
v2/parse_test.go Normal file

File diff suppressed because it is too large Load Diff

271
v2/precedence_test.go Normal file
View File

@ -0,0 +1,271 @@
package arg
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// this file contains tests related to the precedence rules for:
// ProcessCommandLine
// ProcessOptions
// ProcessPositionals
// ProcessEnvironment
// ProcessMap
// ProcessSingle
// ProcessSequence
// OverwriteWithCommandLine
// OverwriteWithOptions
// OverwriteWithPositionals
// OverwriteWithEnvironment
// OverwriteWithMap
//
// The Process* functions should not overwrite fields that have
// been previously populated, whereas the OverwriteWith* functions
// should overwrite fields that have been previously populated.
// check that we can accumulate "separate" args across env, cmdline, map, and defaults
// check what happens if we have a required arg with a default value
// add more tests for combinations of separate and cardinality
// check what happens if we call ProcessCommandLine multiple times with different subcommands
func TestProcessOptions(t *testing.T) {
var args struct {
Arg string
}
p, err := NewParser(&args)
require.NoError(t, err)
_, err = p.ProcessOptions([]string{"program", "--arg=hello"})
require.NoError(t, err)
assert.Equal(t, "hello", args.Arg)
}
func TestProcessOptionsDoesNotOverwrite(t *testing.T) {
var args struct {
Arg string `arg:"env"`
}
p, err := NewParser(&args)
require.NoError(t, err)
err = p.ProcessEnvironment([]string{"ARG=123"})
require.NoError(t, err)
_, err = p.ProcessOptions([]string{"--arg=hello"})
require.NoError(t, err)
assert.EqualValues(t, "123", args.Arg)
}
func TestOverwriteWithOptions(t *testing.T) {
var args struct {
Arg string `arg:"env"`
}
p, err := NewParser(&args)
require.NoError(t, err)
err = p.ProcessEnvironment([]string{"ARG=123"})
require.NoError(t, err)
_, err = p.OverwriteWithOptions([]string{"--arg=hello"})
require.NoError(t, err)
assert.EqualValues(t, "hello", args.Arg)
}
func TestProcessPositionals(t *testing.T) {
var args struct {
Arg string `arg:"positional"`
}
p, err := NewParser(&args)
require.NoError(t, err)
err = p.ProcessPositionals([]string{"hello"})
require.NoError(t, err)
assert.Equal(t, "hello", args.Arg)
}
func TestProcessPositionalsDoesNotOverwrite(t *testing.T) {
var args struct {
Arg string `arg:"env,positional"`
}
p, err := NewParser(&args)
require.NoError(t, err)
err = p.ProcessEnvironment([]string{"ARG=123"})
require.NoError(t, err)
err = p.ProcessPositionals([]string{"hello"})
require.NoError(t, err)
assert.EqualValues(t, "123", args.Arg)
}
func TestOverwriteWithPositionals(t *testing.T) {
var args struct {
Arg string `arg:"env,positional"`
}
p, err := NewParser(&args)
require.NoError(t, err)
err = p.ProcessEnvironment([]string{"ARG=123"})
require.NoError(t, err)
err = p.OverwriteWithPositionals([]string{"hello"})
require.NoError(t, err)
assert.EqualValues(t, "hello", args.Arg)
}
func TestProcessCommandLine(t *testing.T) {
var args struct {
Arg string
}
p, err := NewParser(&args)
require.NoError(t, err)
err = p.ProcessCommandLine([]string{"program", "--arg=hello"})
require.NoError(t, err)
assert.Equal(t, "hello", args.Arg)
}
func TestProcessCommandLineDoesNotOverwrite(t *testing.T) {
var args struct {
Arg string `arg:"env"`
}
p, err := NewParser(&args)
require.NoError(t, err)
err = p.ProcessEnvironment([]string{"ARG=123"})
require.NoError(t, err)
err = p.ProcessCommandLine([]string{"program", "--arg=hello"})
require.NoError(t, err)
assert.EqualValues(t, "123", args.Arg)
}
func TestOverwriteWithCommandLine(t *testing.T) {
var args struct {
Arg string `arg:"env"`
}
p, err := NewParser(&args)
require.NoError(t, err)
err = p.ProcessEnvironment([]string{"ARG=123"})
require.NoError(t, err)
err = p.OverwriteWithCommandLine([]string{"program", "--arg=hello"})
require.NoError(t, err)
assert.EqualValues(t, "hello", args.Arg)
}
func TestProcessEnvironment(t *testing.T) {
var args struct {
Arg string `arg:"env"`
}
p, err := NewParser(&args)
require.NoError(t, err)
err = p.ProcessEnvironment([]string{"ARG=hello"})
require.NoError(t, err)
assert.EqualValues(t, "hello", args.Arg)
}
func TestProcessEnvironmentDoesNotOverwrite(t *testing.T) {
var args struct {
Arg string `arg:"env"`
}
p, err := NewParser(&args)
require.NoError(t, err)
_, err = p.ProcessOptions([]string{"--arg=123"})
require.NoError(t, err)
err = p.ProcessEnvironment([]string{"ARG=hello"})
require.NoError(t, err)
assert.EqualValues(t, "123", args.Arg)
}
func TestOverwriteWithEnvironment(t *testing.T) {
var args struct {
Arg string `arg:"env"`
}
p, err := NewParser(&args)
require.NoError(t, err)
_, err = p.ProcessOptions([]string{"--arg=123"})
require.NoError(t, err)
err = p.OverwriteWithEnvironment([]string{"ARG=hello"})
require.NoError(t, err)
assert.EqualValues(t, "hello", args.Arg)
}
func TestProcessDefaults(t *testing.T) {
var args struct {
Arg string `default:"hello"`
}
p, err := NewParser(&args)
require.NoError(t, err)
err = p.ProcessDefaults()
require.NoError(t, err)
assert.EqualValues(t, "hello", args.Arg)
}
func TestProcessDefaultsDoesNotOverwrite(t *testing.T) {
var args struct {
Arg string `default:"hello"`
}
p, err := NewParser(&args)
require.NoError(t, err)
_, err = p.ProcessOptions([]string{"--arg=123"})
require.NoError(t, err)
err = p.ProcessDefaults()
require.NoError(t, err)
assert.EqualValues(t, "123", args.Arg)
}
func TestOverwriteWithDefaults(t *testing.T) {
var args struct {
Arg string `default:"hello"`
}
p, err := NewParser(&args)
require.NoError(t, err)
_, err = p.ProcessOptions([]string{"--arg=123"})
require.NoError(t, err)
err = p.OverwriteWithDefaults()
require.NoError(t, err)
assert.EqualValues(t, "hello", args.Arg)
}

107
v2/reflect.go Normal file
View File

@ -0,0 +1,107 @@
package arg
import (
"encoding"
"fmt"
"reflect"
"unicode"
"unicode/utf8"
scalar "github.com/alexflint/go-scalar"
)
var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()
// cardinality tracks how many tokens are expected for a given spec
// - zero is a boolean, which does to expect any value
// - one is an ordinary option that will be parsed from a single token
// - multiple is a slice or map that can accept zero or more tokens
type cardinality int
const (
zero cardinality = iota
one
multiple
unsupported
)
func (k cardinality) String() string {
switch k {
case zero:
return "zero"
case one:
return "one"
case multiple:
return "multiple"
case unsupported:
return "unsupported"
default:
return fmt.Sprintf("unknown(%d)", int(k))
}
}
// cardinalityOf returns true if the type can be parsed from a string
func cardinalityOf(t reflect.Type) (cardinality, error) {
if scalar.CanParse(t) {
if isBoolean(t) {
return zero, nil
}
return one, nil
}
// look inside pointer types
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
// look inside slice and map types
switch t.Kind() {
case reflect.Slice:
if !scalar.CanParse(t.Elem()) {
return unsupported, fmt.Errorf("cannot parse into %v because %v not supported", t, t.Elem())
}
return multiple, nil
case reflect.Map:
if !scalar.CanParse(t.Key()) {
return unsupported, fmt.Errorf("cannot parse into %v because key type %v not supported", t, t.Elem())
}
if !scalar.CanParse(t.Elem()) {
return unsupported, fmt.Errorf("cannot parse into %v because value type %v not supported", t, t.Elem())
}
return multiple, nil
default:
return unsupported, fmt.Errorf("cannot parse into %v", t)
}
}
// isBoolean returns true if the type can be parsed from a single string
func isBoolean(t reflect.Type) bool {
switch {
case t.Implements(textUnmarshalerType):
return false
case t.Kind() == reflect.Bool:
return true
case t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Bool:
return true
default:
return false
}
}
// isExported returns true if the struct field name is exported
func isExported(field string) bool {
r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8
return unicode.IsLetter(r) && unicode.IsUpper(r)
}
// isZero returns true if v contains the zero value for its type
func isZero(v reflect.Value) bool {
t := v.Type()
if t.Kind() == reflect.Slice || t.Kind() == reflect.Map {
return v.IsNil()
}
if !t.Comparable() {
return false
}
return v.Interface() == reflect.Zero(t).Interface()
}

112
v2/reflect_test.go Normal file
View File

@ -0,0 +1,112 @@
package arg
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func assertCardinality(t *testing.T, typ reflect.Type, expected cardinality) {
actual, err := cardinalityOf(typ)
assert.Equal(t, expected, actual, "expected %v to have cardinality %v but got %v", typ, expected, actual)
if expected == unsupported {
assert.Error(t, err)
}
}
func TestCardinalityOf(t *testing.T) {
var b bool
var i int
var s string
var f float64
var bs []bool
var is []int
var m map[string]int
var unsupported1 struct{}
var unsupported2 []struct{}
var unsupported3 map[string]struct{}
var unsupported4 map[struct{}]string
assertCardinality(t, reflect.TypeOf(b), zero)
assertCardinality(t, reflect.TypeOf(i), one)
assertCardinality(t, reflect.TypeOf(s), one)
assertCardinality(t, reflect.TypeOf(f), one)
assertCardinality(t, reflect.TypeOf(&b), zero)
assertCardinality(t, reflect.TypeOf(&s), one)
assertCardinality(t, reflect.TypeOf(&i), one)
assertCardinality(t, reflect.TypeOf(&f), one)
assertCardinality(t, reflect.TypeOf(bs), multiple)
assertCardinality(t, reflect.TypeOf(is), multiple)
assertCardinality(t, reflect.TypeOf(&bs), multiple)
assertCardinality(t, reflect.TypeOf(&is), multiple)
assertCardinality(t, reflect.TypeOf(m), multiple)
assertCardinality(t, reflect.TypeOf(&m), multiple)
assertCardinality(t, reflect.TypeOf(unsupported1), unsupported)
assertCardinality(t, reflect.TypeOf(&unsupported1), unsupported)
assertCardinality(t, reflect.TypeOf(unsupported2), unsupported)
assertCardinality(t, reflect.TypeOf(&unsupported2), unsupported)
assertCardinality(t, reflect.TypeOf(unsupported3), unsupported)
assertCardinality(t, reflect.TypeOf(&unsupported3), unsupported)
assertCardinality(t, reflect.TypeOf(unsupported4), unsupported)
assertCardinality(t, reflect.TypeOf(&unsupported4), unsupported)
}
type implementsTextUnmarshaler struct{}
func (*implementsTextUnmarshaler) UnmarshalText(text []byte) error {
return nil
}
func TestCardinalityTextUnmarshaler(t *testing.T) {
var x implementsTextUnmarshaler
var s []implementsTextUnmarshaler
var m []implementsTextUnmarshaler
assertCardinality(t, reflect.TypeOf(x), one)
assertCardinality(t, reflect.TypeOf(&x), one)
assertCardinality(t, reflect.TypeOf(s), multiple)
assertCardinality(t, reflect.TypeOf(&s), multiple)
assertCardinality(t, reflect.TypeOf(m), multiple)
assertCardinality(t, reflect.TypeOf(&m), multiple)
}
func TestIsExported(t *testing.T) {
assert.True(t, isExported("Exported"))
assert.False(t, isExported("notExported"))
assert.False(t, isExported(""))
assert.False(t, isExported(string([]byte{255})))
}
func TestCardinalityString(t *testing.T) {
assert.Equal(t, "zero", zero.String())
assert.Equal(t, "one", one.String())
assert.Equal(t, "multiple", multiple.String())
assert.Equal(t, "unsupported", unsupported.String())
assert.Equal(t, "unknown(42)", cardinality(42).String())
}
func TestIsZero(t *testing.T) {
var zero int
var notZero = 3
var nilSlice []int
var nonNilSlice = []int{1, 2, 3}
var nilMap map[string]string
var nonNilMap = map[string]string{"foo": "bar"}
var uncomparable = func() {}
assert.True(t, isZero(reflect.ValueOf(zero)))
assert.False(t, isZero(reflect.ValueOf(notZero)))
assert.True(t, isZero(reflect.ValueOf(nilSlice)))
assert.False(t, isZero(reflect.ValueOf(nonNilSlice)))
assert.True(t, isZero(reflect.ValueOf(nilMap)))
assert.False(t, isZero(reflect.ValueOf(nonNilMap)))
assert.False(t, isZero(reflect.ValueOf(uncomparable)))
}

218
v2/sequence.go Normal file
View File

@ -0,0 +1,218 @@
package arg
import (
"fmt"
"reflect"
"strings"
scalar "github.com/alexflint/go-scalar"
)
// setSliceOrMap parses a sequence of strings into a slice or map. The slice or
// map is always cleared first.
func setSliceOrMap(dest reflect.Value, values []string) error {
if !dest.CanSet() {
return fmt.Errorf("field is not writable")
}
t := dest.Type()
if t.Kind() == reflect.Ptr {
dest = dest.Elem()
t = t.Elem()
}
switch t.Kind() {
case reflect.Slice:
return setSlice(dest, values)
case reflect.Map:
return setMap(dest, values)
default:
return fmt.Errorf("cannot insert multiple values into a %v", t)
}
}
// setSlice parses a sequence of strings and inserts them into a slice. The
// slice is cleared first.
func setSlice(dest reflect.Value, values []string) error {
var ptr bool
elem := dest.Type().Elem()
if elem.Kind() == reflect.Ptr && !elem.Implements(textUnmarshalerType) {
ptr = true
elem = elem.Elem()
}
// clear the slice in case default values exist
if !dest.IsNil() {
dest.SetLen(0)
}
// parse the values one-by-one
for _, s := range values {
v := reflect.New(elem)
if err := scalar.ParseValue(v.Elem(), s); err != nil {
return err
}
if !ptr {
v = v.Elem()
}
dest.Set(reflect.Append(dest, v))
}
return nil
}
// setMap parses a sequence of name=value strings and inserts them into a map.
// The map is always cleared first.
func setMap(dest reflect.Value, values []string) error {
// determine the key and value type
var keyIsPtr bool
keyType := dest.Type().Key()
if keyType.Kind() == reflect.Ptr && !keyType.Implements(textUnmarshalerType) {
keyIsPtr = true
keyType = keyType.Elem()
}
var valIsPtr bool
valType := dest.Type().Elem()
if valType.Kind() == reflect.Ptr && !valType.Implements(textUnmarshalerType) {
valIsPtr = true
valType = valType.Elem()
}
// clear the slice in case default values exist
if !dest.IsNil() {
for _, k := range dest.MapKeys() {
dest.SetMapIndex(k, reflect.Value{})
}
}
// allocate the map if it is not allocated
if dest.IsNil() {
dest.Set(reflect.MakeMap(dest.Type()))
}
// parse the values one-by-one
for _, s := range values {
// split at the first equals sign
pos := strings.Index(s, "=")
if pos == -1 {
return fmt.Errorf("cannot parse %q into a map, expected format key=value", s)
}
// parse the key
k := reflect.New(keyType)
if err := scalar.ParseValue(k.Elem(), s[:pos]); err != nil {
return err
}
if !keyIsPtr {
k = k.Elem()
}
// parse the value
v := reflect.New(valType)
if err := scalar.ParseValue(v.Elem(), s[pos+1:]); err != nil {
return err
}
if !valIsPtr {
v = v.Elem()
}
// add it to the map
dest.SetMapIndex(k, v)
}
return nil
}
// appendSliceOrMap parses a string and appends it to an existing slice or map.
func appendToSliceOrMap(dest reflect.Value, value string) error {
if !dest.CanSet() {
return fmt.Errorf("field is not writable")
}
t := dest.Type()
if t.Kind() == reflect.Ptr {
dest = dest.Elem()
t = t.Elem()
}
switch t.Kind() {
case reflect.Slice:
return appendToSlice(dest, value)
case reflect.Map:
return appendToMap(dest, value)
default:
return fmt.Errorf("cannot insert multiple values into a %v", t)
}
}
// appendSlice parses a string and appends the result into a slice.
func appendToSlice(dest reflect.Value, s string) error {
var ptr bool
elem := dest.Type().Elem()
if elem.Kind() == reflect.Ptr && !elem.Implements(textUnmarshalerType) {
ptr = true
elem = elem.Elem()
}
// parse the value and append
v := reflect.New(elem)
if err := scalar.ParseValue(v.Elem(), s); err != nil {
return err
}
if !ptr {
v = v.Elem()
}
dest.Set(reflect.Append(dest, v))
return nil
}
// appendToMap parses a name=value string and inserts it into a map.
// If clear is true then any values already in the map are removed.
func appendToMap(dest reflect.Value, s string) error {
// determine the key and value type
var keyIsPtr bool
keyType := dest.Type().Key()
if keyType.Kind() == reflect.Ptr && !keyType.Implements(textUnmarshalerType) {
keyIsPtr = true
keyType = keyType.Elem()
}
var valIsPtr bool
valType := dest.Type().Elem()
if valType.Kind() == reflect.Ptr && !valType.Implements(textUnmarshalerType) {
valIsPtr = true
valType = valType.Elem()
}
// allocate the map if it is not allocated
if dest.IsNil() {
dest.Set(reflect.MakeMap(dest.Type()))
}
// split at the first equals sign
pos := strings.Index(s, "=")
if pos == -1 {
return fmt.Errorf("cannot parse %q into a map, expected format key=value", s)
}
// parse the key
k := reflect.New(keyType)
if err := scalar.ParseValue(k.Elem(), s[:pos]); err != nil {
return err
}
if !keyIsPtr {
k = k.Elem()
}
// parse the value
v := reflect.New(valType)
if err := scalar.ParseValue(v.Elem(), s[pos+1:]); err != nil {
return err
}
if !valIsPtr {
v = v.Elem()
}
// add it to the map
dest.SetMapIndex(k, v)
return nil
}

149
v2/sequence_test.go Normal file
View File

@ -0,0 +1,149 @@
package arg
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAppendToSlice(t *testing.T) {
xs := []int{10}
err := appendToSlice(reflect.ValueOf(&xs).Elem(), "3")
require.NoError(t, err)
assert.Equal(t, []int{10, 3}, xs)
}
func TestSetSlice(t *testing.T) {
xs := []int{100}
entries := []string{"1", "2", "3"}
err := setSlice(reflect.ValueOf(&xs).Elem(), entries)
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, xs)
}
func TestSetSliceInvalid(t *testing.T) {
xs := []int{100}
entries := []string{"invalid"}
err := setSlice(reflect.ValueOf(&xs).Elem(), entries)
assert.Error(t, err)
}
func TestSetSlicePtr(t *testing.T) {
var xs []*int
entries := []string{"1", "2", "3"}
err := setSlice(reflect.ValueOf(&xs).Elem(), entries)
require.NoError(t, err)
require.Len(t, xs, 3)
assert.Equal(t, 1, *xs[0])
assert.Equal(t, 2, *xs[1])
assert.Equal(t, 3, *xs[2])
}
func TestSetSliceTextUnmarshaller(t *testing.T) {
// textUnmarshaler is a struct that captures the length of the string passed to it
var xs []*textUnmarshaler
entries := []string{"a", "aa", "aaa"}
err := setSlice(reflect.ValueOf(&xs).Elem(), entries)
require.NoError(t, err)
require.Len(t, xs, 3)
assert.Equal(t, 1, xs[0].val)
assert.Equal(t, 2, xs[1].val)
assert.Equal(t, 3, xs[2].val)
}
func TestAppendToMap(t *testing.T) {
m := map[string]int{"foo": 10}
err := appendToMap(reflect.ValueOf(&m).Elem(), "a=1")
require.NoError(t, err)
require.Len(t, m, 2)
assert.Equal(t, 1, m["a"])
assert.Equal(t, 10, m["foo"])
}
func TestSetMapAfterClearing(t *testing.T) {
m := map[string]int{"foo": 10}
entries := []string{"a=1", "b=2"}
err := setMap(reflect.ValueOf(&m).Elem(), entries)
require.NoError(t, err)
require.Len(t, m, 2)
assert.Equal(t, 1, m["a"])
assert.Equal(t, 2, m["b"])
}
func TestSetMapWithKeyPointer(t *testing.T) {
// textUnmarshaler is a struct that captures the length of the string passed to it
var m map[*string]int
entries := []string{"abc=123"}
err := setMap(reflect.ValueOf(&m).Elem(), entries)
require.NoError(t, err)
require.Len(t, m, 1)
}
func TestSetMapWithValuePointer(t *testing.T) {
// textUnmarshaler is a struct that captures the length of the string passed to it
var m map[string]*int
entries := []string{"abc=123"}
err := setMap(reflect.ValueOf(&m).Elem(), entries)
require.NoError(t, err)
require.Len(t, m, 1)
assert.Equal(t, 123, *m["abc"])
}
func TestSetMapTextUnmarshaller(t *testing.T) {
// textUnmarshaler is a struct that captures the length of the string passed to it
var m map[textUnmarshaler]*textUnmarshaler
entries := []string{"a=123", "aa=12", "aaa=1"}
err := setMap(reflect.ValueOf(&m).Elem(), entries)
require.NoError(t, err)
require.Len(t, m, 3)
assert.Equal(t, &textUnmarshaler{3}, m[textUnmarshaler{1}])
assert.Equal(t, &textUnmarshaler{2}, m[textUnmarshaler{2}])
assert.Equal(t, &textUnmarshaler{1}, m[textUnmarshaler{3}])
}
func TestSetMapInvalidKey(t *testing.T) {
var m map[int]int
entries := []string{"invalid=123"}
err := setMap(reflect.ValueOf(&m).Elem(), entries)
assert.Error(t, err)
}
func TestSetMapInvalidValue(t *testing.T) {
var m map[int]int
entries := []string{"123=invalid"}
err := setMap(reflect.ValueOf(&m).Elem(), entries)
assert.Error(t, err)
}
func TestSetMapMalformed(t *testing.T) {
// textUnmarshaler is a struct that captures the length of the string passed to it
var m map[string]string
entries := []string{"missing_equals_sign"}
err := setMap(reflect.ValueOf(&m).Elem(), entries)
assert.Error(t, err)
}
func TestSetSliceOrMapErrors(t *testing.T) {
var err error
var dest reflect.Value
// converting a slice to a reflect.Value in this way will make it read only
var cannotSet []int
dest = reflect.ValueOf(cannotSet)
err = setSliceOrMap(dest, nil)
assert.Error(t, err)
// check what happens when we pass in something that is not a slice or a map
var notSliceOrMap string
dest = reflect.ValueOf(&notSliceOrMap).Elem()
err = setSliceOrMap(dest, nil)
assert.Error(t, err)
// check what happens when we pass in a pointer to something that is not a slice or a map
var stringPtr *string
dest = reflect.ValueOf(&stringPtr).Elem()
err = setSliceOrMap(dest, nil)
assert.Error(t, err)
}

37
v2/subcommand.go Normal file
View File

@ -0,0 +1,37 @@
package arg
// Subcommand returns the user struct for the subcommand selected by
// the command line arguments most recently processed by the parser.
// The return value is always a pointer to a struct. If no subcommand
// was specified then it returns the top-level arguments struct. If
// no command line arguments have been processed by this parser then it
// returns nil.
func (p *Parser) Subcommand() interface{} {
if p.leaf == nil || p.leaf.parent == nil {
return nil
}
return p.val(p.leaf.dest).Interface()
}
// SubcommandNames returns the sequence of subcommands specified by the
// user. If no subcommands were given then it returns an empty slice.
func (p *Parser) SubcommandNames() []string {
if p.leaf == nil {
return nil
}
// make a list of ancestor commands
var ancestors []string
cur := p.leaf
for cur.parent != nil { // we want to exclude the root
ancestors = append(ancestors, cur.name)
cur = cur.parent
}
// reverse the list
out := make([]string, len(ancestors))
for i := 0; i < len(ancestors); i++ {
out[i] = ancestors[len(ancestors)-i-1]
}
return out
}

411
v2/subcommand_test.go Normal file
View File

@ -0,0 +1,411 @@
package arg
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// This file contains tests for parse.go but I decided to put them here
// since that file is getting large
func TestSubcommandNotAPointer(t *testing.T) {
var args struct {
A string `arg:"subcommand"`
}
_, err := NewParser(&args)
assert.Error(t, err)
}
func TestSubcommandNotAPointerToStruct(t *testing.T) {
var args struct {
A struct{} `arg:"subcommand"`
}
_, err := NewParser(&args)
assert.Error(t, err)
}
func TestPositionalAndSubcommandNotAllowed(t *testing.T) {
var args struct {
A string `arg:"positional"`
B *struct{} `arg:"subcommand"`
}
_, err := NewParser(&args)
assert.Error(t, err)
}
func TestMinimalSubcommand(t *testing.T) {
type listCmd struct {
}
var args struct {
List *listCmd `arg:"subcommand"`
}
p, err := parse(&args, "list")
require.NoError(t, err)
assert.NotNil(t, args.List)
assert.Equal(t, args.List, p.Subcommand())
assert.Equal(t, []string{"list"}, p.SubcommandNames())
}
func TestSubcommandNamesBeforeParsing(t *testing.T) {
type listCmd struct{}
var args struct {
List *listCmd `arg:"subcommand"`
}
p, err := NewParser(&args)
require.NoError(t, err)
assert.Nil(t, p.Subcommand())
assert.Nil(t, p.SubcommandNames())
}
func TestNoSuchSubcommand(t *testing.T) {
type listCmd struct {
}
var args struct {
List *listCmd `arg:"subcommand"`
}
_, err := parse(&args, "invalid")
assert.Error(t, err)
}
func TestNamedSubcommand(t *testing.T) {
type listCmd struct {
}
var args struct {
List *listCmd `arg:"subcommand:ls"`
}
p, err := parse(&args, "ls")
require.NoError(t, err)
assert.NotNil(t, args.List)
assert.Equal(t, args.List, p.Subcommand())
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
}
func TestEmptySubcommand(t *testing.T) {
type listCmd struct {
}
var args struct {
List *listCmd `arg:"subcommand"`
}
p, err := parse(&args, "")
require.NoError(t, err)
assert.Nil(t, args.List)
assert.Nil(t, p.Subcommand())
assert.Empty(t, p.SubcommandNames())
}
func TestTwoSubcommands(t *testing.T) {
type getCmd struct {
}
type listCmd struct {
}
var args struct {
Get *getCmd `arg:"subcommand"`
List *listCmd `arg:"subcommand"`
}
p, err := parse(&args, "list")
require.NoError(t, err)
assert.Nil(t, args.Get)
assert.NotNil(t, args.List)
assert.Equal(t, args.List, p.Subcommand())
assert.Equal(t, []string{"list"}, p.SubcommandNames())
}
func TestSubcommandsWithOptions(t *testing.T) {
type getCmd struct {
Name string
}
type listCmd struct {
Limit int
}
type cmd struct {
Verbose bool
Get *getCmd `arg:"subcommand"`
List *listCmd `arg:"subcommand"`
}
{
var args cmd
_, err := parse(&args, "list")
require.NoError(t, err)
assert.Nil(t, args.Get)
assert.NotNil(t, args.List)
}
{
var args cmd
_, err := parse(&args, "list --limit 3")
require.NoError(t, err)
assert.Nil(t, args.Get)
assert.NotNil(t, args.List)
assert.Equal(t, args.List.Limit, 3)
}
{
var args cmd
_, err := parse(&args, "list --limit 3 --verbose")
require.NoError(t, err)
assert.Nil(t, args.Get)
assert.NotNil(t, args.List)
assert.Equal(t, args.List.Limit, 3)
assert.True(t, args.Verbose)
}
{
var args cmd
_, err := parse(&args, "list --verbose --limit 3")
require.NoError(t, err)
assert.Nil(t, args.Get)
assert.NotNil(t, args.List)
assert.Equal(t, args.List.Limit, 3)
assert.True(t, args.Verbose)
}
{
var args cmd
_, err := parse(&args, "--verbose list --limit 3")
require.NoError(t, err)
assert.Nil(t, args.Get)
assert.NotNil(t, args.List)
assert.Equal(t, args.List.Limit, 3)
assert.True(t, args.Verbose)
}
{
var args cmd
_, err := parse(&args, "get")
require.NoError(t, err)
assert.NotNil(t, args.Get)
assert.Nil(t, args.List)
}
{
var args cmd
_, err := parse(&args, "get --name test")
require.NoError(t, err)
assert.NotNil(t, args.Get)
assert.Nil(t, args.List)
assert.Equal(t, args.Get.Name, "test")
}
}
func TestSubcommandsWithEnvVars(t *testing.T) {
type getCmd struct {
Name string `arg:"env"`
}
type listCmd struct {
Limit int `arg:"env"`
}
type cmd struct {
Verbose bool
Get *getCmd `arg:"subcommand"`
List *listCmd `arg:"subcommand"`
}
{
var args cmd
_, err := parse(&args, "list", "LIMIT=123")
require.NoError(t, err)
require.NotNil(t, args.List)
assert.Equal(t, 123, args.List.Limit)
}
{
var args cmd
_, err := parse(&args, "list", "LIMIT=not_an_integer")
assert.Error(t, err)
}
}
func TestNestedSubcommands(t *testing.T) {
type child struct{}
type parent struct {
Child *child `arg:"subcommand"`
}
type grandparent struct {
Parent *parent `arg:"subcommand"`
}
type root struct {
Grandparent *grandparent `arg:"subcommand"`
}
{
var args root
p, err := parse(&args, "grandparent parent child")
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.NotNil(t, args.Grandparent.Parent)
require.NotNil(t, args.Grandparent.Parent.Child)
assert.Equal(t, args.Grandparent.Parent.Child, p.Subcommand())
assert.Equal(t, []string{"grandparent", "parent", "child"}, p.SubcommandNames())
}
{
var args root
p, err := parse(&args, "grandparent parent")
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.NotNil(t, args.Grandparent.Parent)
require.Nil(t, args.Grandparent.Parent.Child)
assert.Equal(t, args.Grandparent.Parent, p.Subcommand())
assert.Equal(t, []string{"grandparent", "parent"}, p.SubcommandNames())
}
{
var args root
p, err := parse(&args, "grandparent")
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.Nil(t, args.Grandparent.Parent)
assert.Equal(t, args.Grandparent, p.Subcommand())
assert.Equal(t, []string{"grandparent"}, p.SubcommandNames())
}
{
var args root
p, err := parse(&args, "")
require.NoError(t, err)
require.Nil(t, args.Grandparent)
assert.Nil(t, p.Subcommand())
assert.Empty(t, p.SubcommandNames())
}
}
func TestSubcommandsWithPositionals(t *testing.T) {
type listCmd struct {
Pattern string `arg:"positional"`
}
type cmd struct {
Format string
List *listCmd `arg:"subcommand"`
}
{
var args cmd
_, err := parse(&args, "list")
require.NoError(t, err)
assert.NotNil(t, args.List)
assert.Equal(t, "", args.List.Pattern)
}
{
var args cmd
_, err := parse(&args, "list --format json")
require.NoError(t, err)
assert.NotNil(t, args.List)
assert.Equal(t, "", args.List.Pattern)
assert.Equal(t, "json", args.Format)
}
{
var args cmd
_, err := parse(&args, "list somepattern")
require.NoError(t, err)
assert.NotNil(t, args.List)
assert.Equal(t, "somepattern", args.List.Pattern)
}
{
var args cmd
_, err := parse(&args, "list somepattern --format json")
require.NoError(t, err)
assert.NotNil(t, args.List)
assert.Equal(t, "somepattern", args.List.Pattern)
assert.Equal(t, "json", args.Format)
}
{
var args cmd
_, err := parse(&args, "list --format json somepattern")
require.NoError(t, err)
assert.NotNil(t, args.List)
assert.Equal(t, "somepattern", args.List.Pattern)
assert.Equal(t, "json", args.Format)
}
{
var args cmd
_, err := parse(&args, "--format json list somepattern")
require.NoError(t, err)
assert.NotNil(t, args.List)
assert.Equal(t, "somepattern", args.List.Pattern)
assert.Equal(t, "json", args.Format)
}
{
var args cmd
_, err := parse(&args, "--format json")
require.NoError(t, err)
assert.Nil(t, args.List)
assert.Equal(t, "json", args.Format)
}
}
func TestSubcommandsWithMultiplePositionals(t *testing.T) {
type getCmd struct {
Items []string `arg:"positional"`
}
type cmd struct {
Limit int
Get *getCmd `arg:"subcommand"`
}
{
var args cmd
_, err := parse(&args, "get")
require.NoError(t, err)
assert.NotNil(t, args.Get)
assert.Empty(t, args.Get.Items)
}
{
var args cmd
_, err := parse(&args, "get --limit 5")
require.NoError(t, err)
assert.NotNil(t, args.Get)
assert.Empty(t, args.Get.Items)
assert.Equal(t, 5, args.Limit)
}
{
var args cmd
_, err := parse(&args, "get item1")
require.NoError(t, err)
assert.NotNil(t, args.Get)
assert.Equal(t, []string{"item1"}, args.Get.Items)
}
{
var args cmd
_, err := parse(&args, "get item1 item2 item3")
require.NoError(t, err)
assert.NotNil(t, args.Get)
assert.Equal(t, []string{"item1", "item2", "item3"}, args.Get.Items)
}
{
var args cmd
_, err := parse(&args, "get item1 --limit 5 item2")
require.NoError(t, err)
assert.NotNil(t, args.Get)
assert.Equal(t, []string{"item1", "item2"}, args.Get.Items)
assert.Equal(t, 5, args.Limit)
}
}
func TestValForNilStruct(t *testing.T) {
type subcmd struct{}
var cmd struct {
Sub *subcmd `arg:"subcommand"`
}
p, err := NewParser(&cmd)
require.NoError(t, err)
typ := reflect.TypeOf(cmd)
subField, _ := typ.FieldByName("Sub")
v := p.val(path{fields: []reflect.StructField{subField, subField}})
assert.False(t, v.IsValid())
}

331
v2/usage.go Normal file
View File

@ -0,0 +1,331 @@
package arg
import (
"fmt"
"io"
"os"
"strings"
)
// the width of the left column
const colWidth = 25
// to allow monkey patching in tests
var (
stdout io.Writer = os.Stdout
stderr io.Writer = os.Stderr
osExit = os.Exit
)
// Fail prints usage information to stderr and exits with non-zero status
func (p *Parser) Fail(msg string) {
p.failWithSubcommand(msg, p.cmd)
}
// FailSubcommand prints usage information for a specified subcommand to stderr,
// then exits with non-zero status. To write usage information for a top-level
// subcommand, provide just the name of that subcommand. To write usage
// information for a subcommand that is nested under another subcommand, provide
// a sequence of subcommand names starting with the top-level subcommand and so
// on down the tree.
func (p *Parser) FailSubcommand(msg string, subcommand ...string) error {
cmd, err := p.lookupCommand(subcommand...)
if err != nil {
return err
}
p.failWithSubcommand(msg, cmd)
return nil
}
// failWithSubcommand prints usage information for the given subcommand to stderr and exits with non-zero status
func (p *Parser) failWithSubcommand(msg string, cmd *Command) {
p.writeUsageForSubcommand(stderr, cmd)
fmt.Fprintln(stderr, "error:", msg)
osExit(-1)
}
// WriteUsage writes usage information for the top-level command
func (p *Parser) WriteUsage(w io.Writer) {
p.writeUsageForSubcommand(w, p.cmd)
}
// WriteUsageForSubcommand writes the usage information for a specified
// subcommand. To write usage information for a top-level subcommand, provide
// just the name of that subcommand. To write usage information for a subcommand
// that is nested under another subcommand, provide a sequence of subcommand
// names starting with the top-level subcommand and so on down the tree.
func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) error {
cmd, err := p.lookupCommand(subcommand...)
if err != nil {
return err
}
p.writeUsageForSubcommand(w, cmd)
return nil
}
// writeUsageForSubcommand writes usage information for the given subcommand
func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *Command) {
var positionals, longOptions, shortOptions []*Argument
for _, arg := range cmd.args {
switch {
case arg.positional:
positionals = append(positionals, arg)
case arg.long != "":
longOptions = append(longOptions, arg)
case arg.short != "":
shortOptions = append(shortOptions, arg)
}
}
if p.version != "" {
fmt.Fprintln(w, p.version)
}
// make a list of ancestor commands so that we print with full context
var ancestors []string
ancestor := cmd
for ancestor != nil {
ancestors = append(ancestors, ancestor.name)
ancestor = ancestor.parent
}
// print the beginning of the usage string
fmt.Fprint(w, "Usage:")
for i := len(ancestors) - 1; i >= 0; i-- {
fmt.Fprint(w, " "+ancestors[i])
}
// write the option component of the usage message
for _, arg := range shortOptions {
// prefix with a space
fmt.Fprint(w, " ")
if !arg.required {
fmt.Fprint(w, "[")
}
fmt.Fprint(w, synopsis(arg, "-"+arg.short))
if !arg.required {
fmt.Fprint(w, "]")
}
}
for _, arg := range longOptions {
// prefix with a space
fmt.Fprint(w, " ")
if !arg.required {
fmt.Fprint(w, "[")
}
fmt.Fprint(w, synopsis(arg, "--"+arg.long))
if !arg.required {
fmt.Fprint(w, "]")
}
}
// When we parse positionals, we check that:
// 1. required positionals come before non-required positionals
// 2. there is at most one multiple-value positional
// 3. if there is a multiple-value positional then it comes after all other positionals
// Here we merely print the usage string, so we do not explicitly re-enforce those rules
// write the positionals in following form:
// REQUIRED1 REQUIRED2
// REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]
// REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]
// REQUIRED1 REQUIRED2 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]
// REQUIRED1 REQUIRED2 [OPTIONAL1 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]]
var closeBrackets int
for _, arg := range positionals {
fmt.Fprint(w, " ")
if !arg.required {
fmt.Fprint(w, "[")
closeBrackets += 1
}
if arg.cardinality == multiple {
fmt.Fprintf(w, "%s [%s ...]", arg.placeholder, arg.placeholder)
} else {
fmt.Fprint(w, arg.placeholder)
}
}
fmt.Fprint(w, strings.Repeat("]", closeBrackets))
// if the program supports subcommands, give a hint to the user about their existence
if len(cmd.subcommands) > 0 {
fmt.Fprint(w, " <command> [<args>]")
}
fmt.Fprint(w, "\n")
}
func printTwoCols(w io.Writer, left, help string, defaultVal string, envVal string) {
lhs := " " + left
fmt.Fprint(w, lhs)
if help != "" {
if len(lhs)+2 < colWidth {
fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs)))
} else {
fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
}
fmt.Fprint(w, help)
}
bracketsContent := []string{}
if defaultVal != "" {
bracketsContent = append(bracketsContent,
fmt.Sprintf("default: %s", defaultVal),
)
}
if envVal != "" {
bracketsContent = append(bracketsContent,
fmt.Sprintf("env: %s", envVal),
)
}
if len(bracketsContent) > 0 {
fmt.Fprintf(w, " [%s]", strings.Join(bracketsContent, ", "))
}
fmt.Fprint(w, "\n")
}
// WriteHelp writes the usage string for the top-level command
func (p *Parser) WriteHelp(w io.Writer) {
p.writeHelpForSubcommand(w, p.cmd)
}
// WriteHelpForSubcommand writes the usage string followed by the full help
// string for a specified subcommand. To write help for a top-level subcommand,
// provide just the name of that subcommand. To write help for a subcommand that
// is nested under another subcommand, provide a sequence of subcommand names
// starting with the top-level subcommand and so on down the tree.
func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error {
cmd, err := p.lookupCommand(subcommand...)
if err != nil {
return err
}
p.writeHelpForSubcommand(w, cmd)
return nil
}
// writeHelp writes the usage string for the given subcommand
func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *Command) {
var positionals, longOptions, shortOptions []*Argument
for _, arg := range cmd.args {
switch {
case arg.positional:
positionals = append(positionals, arg)
case arg.long != "":
longOptions = append(longOptions, arg)
case arg.short != "":
shortOptions = append(shortOptions, arg)
}
}
if p.prologue != "" {
fmt.Fprintln(w, p.prologue)
}
p.writeUsageForSubcommand(w, cmd)
// write the list of positionals
if len(positionals) > 0 {
fmt.Fprint(w, "\nPositional arguments:\n")
for _, arg := range positionals {
printTwoCols(w, arg.placeholder, arg.help, "", "")
}
}
// write the list of options with the short-only ones first to match the usage string
if len(shortOptions)+len(longOptions) > 0 || cmd.parent == nil {
fmt.Fprint(w, "\nOptions:\n")
for _, arg := range shortOptions {
p.printOption(w, arg)
}
for _, arg := range longOptions {
p.printOption(w, arg)
}
}
// obtain a flattened list of options from all ancestors
var globals []*Argument
ancestor := cmd.parent
for ancestor != nil {
globals = append(globals, ancestor.args...)
ancestor = ancestor.parent
}
// write the list of global options
if len(globals) > 0 {
fmt.Fprint(w, "\nGlobal options:\n")
for _, arg := range globals {
p.printOption(w, arg)
}
}
// write the list of built in options
p.printOption(w, &Argument{
cardinality: zero,
long: "help",
short: "h",
help: "display this help and exit",
})
if p.version != "" {
p.printOption(w, &Argument{
cardinality: zero,
long: "version",
help: "display version and exit",
})
}
// write the list of subcommands
if len(cmd.subcommands) > 0 {
fmt.Fprint(w, "\nCommands:\n")
for _, subcmd := range cmd.subcommands {
printTwoCols(w, subcmd.name, subcmd.help, "", "")
}
}
if p.epilogue != "" {
fmt.Fprintln(w, "\n"+p.epilogue)
}
}
func (p *Parser) printOption(w io.Writer, arg *Argument) {
ways := make([]string, 0, 2)
if arg.long != "" {
ways = append(ways, synopsis(arg, "--"+arg.long))
}
if arg.short != "" {
ways = append(ways, synopsis(arg, "-"+arg.short))
}
if len(ways) > 0 {
printTwoCols(w, strings.Join(ways, ", "), arg.help, arg.defaultVal, arg.env)
}
}
// lookupCommand finds a subcommand based on a sequence of subcommand names. The
// first string should be a top-level subcommand, the next should be a child
// subcommand of that subcommand, and so on. If no strings are given then the
// root command is returned. If no such subcommand exists then an error is
// returned.
func (p *Parser) lookupCommand(path ...string) (*Command, error) {
cmd := p.cmd
for _, name := range path {
var found *Command
for _, child := range cmd.subcommands {
if child.name == name {
found = child
}
}
if found == nil {
return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
}
cmd = found
}
return cmd, nil
}
func synopsis(arg *Argument, form string) string {
if arg.cardinality == zero {
return form
}
return form + " " + arg.placeholder
}

607
v2/usage_test.go Normal file
View File

@ -0,0 +1,607 @@
package arg
import (
"bytes"
"errors"
"fmt"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type NameDotName struct {
Head, Tail string
}
func (n *NameDotName) UnmarshalText(b []byte) error {
s := string(b)
pos := strings.Index(s, ".")
if pos == -1 {
return fmt.Errorf("missing period in %s", s)
}
n.Head = s[:pos]
n.Tail = s[pos+1:]
return nil
}
func (n *NameDotName) MarshalText() (text []byte, err error) {
text = []byte(fmt.Sprintf("%s.%s", n.Head, n.Tail))
return
}
func TestWriteUsage(t *testing.T) {
expectedUsage := "Usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] [--testenv TESTENV] [--file FILE] INPUT [OUTPUT [OUTPUT ...]]"
expectedHelp := `
Usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] [--testenv TESTENV] [--file FILE] INPUT [OUTPUT [OUTPUT ...]]
Positional arguments:
INPUT
OUTPUT list of outputs
Options:
--name NAME name to use [default: Foo Bar]
--value VALUE secret value [default: 42]
--verbose, -v verbosity level
--dataset DATASET dataset to use
--optimize OPTIMIZE, -O OPTIMIZE
optimization level
--ids IDS Ids
--values VALUES Values
--workers WORKERS, -w WORKERS
number of workers to start [default: 10, env: WORKERS]
--testenv TESTENV, -a TESTENV [env: TEST_ENV]
--file FILE, -f FILE File with mandatory extension
--help, -h display this help and exit
`
var args struct {
Input string `arg:"positional,required"`
Output []string `arg:"positional" help:"list of outputs"`
Name string `help:"name to use" default:"Foo Bar"`
Value int `help:"secret value" default:"42"`
Verbose bool `arg:"-v" help:"verbosity level"`
Dataset string `help:"dataset to use"`
Optimize int `arg:"-O" help:"optimization level"`
Ids []int64 `help:"Ids"`
Values []float64 `help:"Values"`
Workers int `arg:"-w,env:WORKERS" help:"number of workers to start" default:"10"`
TestEnv string `arg:"-a,env:TEST_ENV"`
File *NameDotName `arg:"-f" help:"File with mandatory extension"`
}
p, err := NewParser(&args, WithProgramName("example"))
require.NoError(t, err)
os.Args[0] = "example"
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
type MyEnum int
func (n *MyEnum) UnmarshalText(b []byte) error {
return nil
}
func (n *MyEnum) MarshalText() ([]byte, error) {
return nil, errors.New("There was a problem")
}
func TestUsageWithDefaults(t *testing.T) {
expectedUsage := "Usage: example [--label LABEL] [--content CONTENT]"
expectedHelp := `
Usage: example [--label LABEL] [--content CONTENT]
Options:
--label LABEL [default: cat]
--content CONTENT [default: dog]
--help, -h display this help and exit
`
var args struct {
Label string `default:"cat"`
Content string `default:"dog"`
}
p, err := NewParser(&args, WithProgramName("example"))
require.NoError(t, err)
args.Label = "should_ignore_this"
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestUsageLongPositionalWithHelp_legacyForm(t *testing.T) {
expectedUsage := "Usage: example [VERYLONGPOSITIONALWITHHELP]"
expectedHelp := `
Usage: example [VERYLONGPOSITIONALWITHHELP]
Positional arguments:
VERYLONGPOSITIONALWITHHELP
this positional argument is very long but cannot include commas
Options:
--help, -h display this help and exit
`
var args struct {
VeryLongPositionalWithHelp string `arg:"positional,help:this positional argument is very long but cannot include commas"`
}
p, err := NewParser(&args, WithProgramName("example"))
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestUsageLongPositionalWithHelp_newForm(t *testing.T) {
expectedUsage := "Usage: example [VERYLONGPOSITIONALWITHHELP]"
expectedHelp := `
Usage: example [VERYLONGPOSITIONALWITHHELP]
Positional arguments:
VERYLONGPOSITIONALWITHHELP
this positional argument is very long, and includes: commas, colons etc
Options:
--help, -h display this help and exit
`
var args struct {
VeryLongPositionalWithHelp string `arg:"positional" help:"this positional argument is very long, and includes: commas, colons etc"`
}
p, err := NewParser(&args, WithProgramName("example"))
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestUsageWithProgramName(t *testing.T) {
expectedUsage := "Usage: myprogram"
expectedHelp := `
Usage: myprogram
Options:
--help, -h display this help and exit
`
p, err := NewParser(&struct{}{}, WithProgramName("myprogram"))
require.NoError(t, err)
os.Args[0] = "example"
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
type versioned struct{}
// Version returns the version for this program
func (versioned) Version() string {
return "example 3.2.1"
}
func TestUsageWithVersion(t *testing.T) {
expectedUsage := "example 3.2.1\nUsage: example"
expectedHelp := `
example 3.2.1
Usage: example
Options:
--help, -h display this help and exit
--version display version and exit
`
p, err := NewParser(&versioned{}, WithProgramName("example"))
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
type described struct{}
// Described returns the description for this program
func (described) Description() string {
return "this program does this and that"
}
func TestUsageWithDescription(t *testing.T) {
expectedUsage := "Usage: example"
expectedHelp := `
this program does this and that
Usage: example
Options:
--help, -h display this help and exit
`
p, err := NewParser(&described{}, WithProgramName("example"))
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
type epilogued struct{}
// Epilogued returns the epilogue for this program
func (epilogued) Epilogue() string {
return "For more information visit github.com/alexflint/go-arg"
}
func TestUsageWithEpilogue(t *testing.T) {
expectedUsage := "Usage: example"
expectedHelp := `
Usage: example
Options:
--help, -h display this help and exit
For more information visit github.com/alexflint/go-arg
`
os.Args[0] = "example"
p, err := NewParser(&epilogued{}, WithProgramName("example"))
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestUsageForRequiredPositionals(t *testing.T) {
expectedUsage := "Usage: example REQUIRED1 REQUIRED2\n"
var args struct {
Required1 string `arg:"positional,required"`
Required2 string `arg:"positional,required"`
}
p, err := NewParser(&args, WithProgramName("example"))
require.NoError(t, err)
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, usage.String())
}
func TestUsageForMixedPositionals(t *testing.T) {
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]\n"
var args struct {
Required1 string `arg:"positional,required"`
Required2 string `arg:"positional,required"`
Optional1 string `arg:"positional"`
Optional2 string `arg:"positional"`
}
p, err := NewParser(&args, WithProgramName("example"))
require.NoError(t, err)
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, usage.String())
}
func TestUsageForRepeatedPositionals(t *testing.T) {
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]\n"
var args struct {
Required1 string `arg:"positional,required"`
Required2 string `arg:"positional,required"`
Repeated []string `arg:"positional,required"`
}
p, err := NewParser(&args, WithProgramName("example"))
require.NoError(t, err)
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, usage.String())
}
func TestUsageForMixedAndRepeatedPositionals(t *testing.T) {
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2 [REPEATED [REPEATED ...]]]]\n"
var args struct {
Required1 string `arg:"positional,required"`
Required2 string `arg:"positional,required"`
Optional1 string `arg:"positional"`
Optional2 string `arg:"positional"`
Repeated []string `arg:"positional"`
}
p, err := NewParser(&args, WithProgramName("example"))
require.NoError(t, err)
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, usage.String())
}
func TestRequiredMultiplePositionals(t *testing.T) {
expectedUsage := "Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]\n"
expectedHelp := `
Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]
Positional arguments:
REQUIREDMULTIPLE required multiple positional
Options:
--help, -h display this help and exit
`
var args struct {
RequiredMultiple []string `arg:"positional,required" help:"required multiple positional"`
}
p, err := NewParser(&args, WithProgramName("example"))
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, usage.String())
}
func TestUsageWithNestedSubcommands(t *testing.T) {
expectedUsage := "Usage: example child nested [--enable] OUTPUT"
expectedHelp := `
Usage: example child nested [--enable] OUTPUT
Positional arguments:
OUTPUT
Options:
--enable
Global options:
--values VALUES Values
--verbose, -v verbosity level
--help, -h display this help and exit
`
var args struct {
Verbose bool `arg:"-v" help:"verbosity level"`
Child *struct {
Values []float64 `help:"Values"`
Nested *struct {
Enable bool
Output string `arg:"positional,required"`
} `arg:"subcommand:nested"`
} `arg:"subcommand:child"`
}
os.Args[0] = "example"
p, err := NewParser(&args)
require.NoError(t, err)
_ = p.Parse([]string{"child", "nested", "value"}, nil)
var help2 bytes.Buffer
p.WriteHelpForSubcommand(&help2, "child", "nested")
assert.Equal(t, expectedHelp[1:], help2.String())
var usage2 bytes.Buffer
p.WriteUsageForSubcommand(&usage2, "child", "nested")
assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String()))
}
func TestNonexistentSubcommand(t *testing.T) {
var args struct {
sub *struct{} `arg:"subcommand"`
}
p, err := NewParser(&args)
require.NoError(t, err)
var b bytes.Buffer
err = p.WriteUsageForSubcommand(&b, "does_not_exist")
assert.Error(t, err)
err = p.WriteHelpForSubcommand(&b, "does_not_exist")
assert.Error(t, err)
err = p.FailSubcommand("something went wrong", "does_not_exist")
assert.Error(t, err)
err = p.WriteUsageForSubcommand(&b, "sub", "does_not_exist")
assert.Error(t, err)
err = p.WriteHelpForSubcommand(&b, "sub", "does_not_exist")
assert.Error(t, err)
err = p.FailSubcommand("something went wrong", "sub", "does_not_exist")
assert.Error(t, err)
}
func TestUsageWithoutLongNames(t *testing.T) {
expectedUsage := "Usage: example [-a PLACEHOLDER] -b SHORTONLY2"
expectedHelp := `
Usage: example [-a PLACEHOLDER] -b SHORTONLY2
Options:
-a PLACEHOLDER some help [default: some val]
-b SHORTONLY2 some help2
--help, -h display this help and exit
`
var args struct {
ShortOnly string `arg:"-a,--" help:"some help" default:"some val" placeholder:"PLACEHOLDER"`
ShortOnly2 string `arg:"-b,--,required" help:"some help2"`
}
p, err := NewParser(&args, WithProgramName("example"))
assert.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestUsageWithShortFirst(t *testing.T) {
expectedUsage := "Usage: example [-c CAT] [--dog DOG]"
expectedHelp := `
Usage: example [-c CAT] [--dog DOG]
Options:
-c CAT
--dog DOG
--help, -h display this help and exit
`
var args struct {
Dog string
Cat string `arg:"-c,--"`
}
p, err := NewParser(&args, WithProgramName("example"))
assert.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestUsageWithEnvOptions(t *testing.T) {
expectedUsage := "Usage: example [-s SHORT]"
expectedHelp := `
Usage: example [-s SHORT]
Options:
-s SHORT [env: SHORT]
--help, -h display this help and exit
`
var args struct {
Short string `arg:"--,-s,env"`
EnvOnly string `arg:"--,env"`
EnvOnlyOverriden string `arg:"--,env:CUSTOM"`
}
p, err := NewParser(&args, WithProgramName("example"))
assert.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestFail(t *testing.T) {
originalStderr := stderr
originalExit := osExit
defer func() {
stderr = originalStderr
osExit = originalExit
}()
var b bytes.Buffer
stderr = &b
var exitCode int
osExit = func(code int) { exitCode = code }
expectedStdout := `
Usage: example [--foo FOO]
error: something went wrong
`
var args struct {
Foo int
}
p, err := NewParser(&args, WithProgramName("example"))
require.NoError(t, err)
p.Fail("something went wrong")
assert.Equal(t, expectedStdout[1:], b.String())
assert.Equal(t, -1, exitCode)
}
func TestFailSubcommand(t *testing.T) {
originalStderr := stderr
originalExit := osExit
defer func() {
stderr = originalStderr
osExit = originalExit
}()
var b bytes.Buffer
stderr = &b
var exitCode int
osExit = func(code int) { exitCode = code }
expectedStdout := `
Usage: example sub
error: something went wrong
`
var args struct {
Sub *struct{} `arg:"subcommand"`
}
p, err := NewParser(&args, WithProgramName("example"))
require.NoError(t, err)
err = p.FailSubcommand("something went wrong", "sub")
require.NoError(t, err)
assert.Equal(t, expectedStdout[1:], b.String())
assert.Equal(t, -1, exitCode)
}