Compare commits
19 Commits
guimaster
...
prototype-
Author | SHA1 | Date |
---|---|---|
Alex Flint | c046f49e12 | |
Alex Flint | f2539d7ad2 | |
Alex Flint | 2ffe24630b | |
Alex Flint | 47ff44303f | |
Alex Flint | 60a0117880 | |
Alex Flint | 55d9025329 | |
Alex Flint | 0769dd5839 | |
Alex Flint | 84b7154efc | |
Alex Flint | 1cc263f9f2 | |
Alex Flint | b365ec0781 | |
Alex Flint | 64288c5521 | |
Alex Flint | 2775f58376 | |
Alex Flint | 5f0c48f092 | |
Alex Flint | 5ca19cd72d | |
Alex Flint | 4aea783023 | |
Alex Flint | a1e2b672ea | |
Alex Flint | 22f214d7ed | |
Alex Flint | 09d28e1195 | |
Alex Flint | 2e6284635a |
|
@ -22,3 +22,4 @@ _testmain.go
|
|||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
go.work
|
||||
|
|
325
README.md
325
README.md
|
@ -16,6 +16,12 @@
|
|||
|
||||
Declare command line arguments for your program by defining a struct.
|
||||
|
||||
```go
|
||||
import "github.com/go-arg/v2
|
||||
```
|
||||
|
||||
TODO
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Foo string
|
||||
|
@ -33,7 +39,7 @@ hello true
|
|||
### Installation
|
||||
|
||||
```shell
|
||||
go get github.com/alexflint/go-arg
|
||||
go get github.com/alexflint/go-arg/v2
|
||||
```
|
||||
|
||||
### Required arguments
|
||||
|
@ -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
2
go.sum
|
@ -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=
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/alexflint/go-arg/v2"
|
||||
{{if contains .Code "fmt."}}"fmt"{{end}}
|
||||
{{if contains .Code "strings."}}"strings"{{end}}
|
||||
)
|
||||
|
||||
func main() {
|
||||
{{.Code}}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/alexflint/go-arg/v2"
|
||||
{{if contains .Code "fmt."}}"fmt"{{end}}
|
||||
{{if contains .Code "strings."}}"strings"{{end}}
|
||||
)
|
||||
|
||||
{{.Code}}
|
|
@ -0,0 +1,179 @@
|
|||
// mdtest executes code blocks in markdown and checks that they run as expected
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/alexflint/go-arg/v2"
|
||||
)
|
||||
|
||||
// var pattern = "```go(.*)```\\s*```\\s*\\$(.*)\\n(.*)```"
|
||||
var pattern = "(?s)```go([^`]*?)```\\s*```([^`]*?)```" //go(.*)```\\s*```\\s*\\$(.*)\\n(.*)```"
|
||||
|
||||
var re = regexp.MustCompile(pattern)
|
||||
|
||||
var funcs = map[string]any{
|
||||
"contains": strings.Contains,
|
||||
}
|
||||
|
||||
//go:embed example1.go.tpl
|
||||
var templateSource1 string
|
||||
|
||||
//go:embed example2.go.tpl
|
||||
var templateSource2 string
|
||||
|
||||
var t1 = template.Must(template.New("example1.go").Funcs(funcs).Parse(templateSource1))
|
||||
var t2 = template.Must(template.New("example2.go").Funcs(funcs).Parse(templateSource2))
|
||||
|
||||
type payload struct {
|
||||
Code string
|
||||
}
|
||||
|
||||
func runCode(ctx context.Context, code []byte, cmd string) ([]byte, error) {
|
||||
dir, err := os.MkdirTemp("", "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating temp dir to build and run code: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(dir)
|
||||
fmt.Println(strings.Repeat("-", 80))
|
||||
|
||||
srcpath := filepath.Join(dir, "src.go")
|
||||
binpath := filepath.Join(dir, "example")
|
||||
|
||||
// If the code contains a main function then use t2, otherwise use t1
|
||||
t := t1
|
||||
if strings.Contains(string(code), "func main") {
|
||||
t = t2
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
err = t.Execute(&b, payload{Code: string(code)})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error executing template for source file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(b.String())
|
||||
fmt.Println(strings.Repeat("-", 80))
|
||||
|
||||
err = os.WriteFile(srcpath, b.Bytes(), os.ModePerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error writing temporary source file: %w", err)
|
||||
}
|
||||
|
||||
compiler, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not find path to go compiler: %w", err)
|
||||
}
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, compiler, "build", "-o", binpath, srcpath)
|
||||
out, err := buildCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error building source: %w. Compiler said:\n%s", err, string(out))
|
||||
}
|
||||
|
||||
// replace "./example" with full path to compiled program
|
||||
var env, args []string
|
||||
var found bool
|
||||
for _, part := range strings.Split(cmd, " ") {
|
||||
if found {
|
||||
args = append(args, part)
|
||||
} else if part == "./example" {
|
||||
found = true
|
||||
} else {
|
||||
env = append(env, part)
|
||||
}
|
||||
}
|
||||
|
||||
runCmd := exec.CommandContext(ctx, binpath, args...)
|
||||
runCmd.Env = env
|
||||
output, err := runCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error runing example: %w. Program said:\n%s", err, string(output))
|
||||
}
|
||||
|
||||
// Clean up the temp dir
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
return nil, fmt.Errorf("error deleting temp dir: %w", err)
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func Main() error {
|
||||
ctx := context.Background()
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional,required"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
|
||||
buf, err := os.ReadFile(args.Input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(strings.Repeat("=", 80))
|
||||
|
||||
matches := re.FindAllSubmatchIndex(buf, -1)
|
||||
for k, match := range matches {
|
||||
codebegin, codeend := match[2], match[3]
|
||||
code := buf[codebegin:codeend]
|
||||
|
||||
shellbegin, shellend := match[4], match[5]
|
||||
shell := buf[shellbegin:shellend]
|
||||
|
||||
lines := strings.Split(string(shell), "\n")
|
||||
for i := 0; i < len(lines); i++ {
|
||||
if strings.HasPrefix(lines[i], "$") && strings.Contains(lines[i], "./example") {
|
||||
cmd := strings.TrimSpace(strings.TrimPrefix(lines[i], "$"))
|
||||
|
||||
var output []string
|
||||
i++
|
||||
for i < len(lines) && !strings.HasPrefix(lines[i], "$") {
|
||||
output = append(output, lines[i])
|
||||
i++
|
||||
}
|
||||
|
||||
expected := strings.TrimSpace(strings.Join(output, "\n"))
|
||||
|
||||
fmt.Println(string(code))
|
||||
fmt.Println(strings.Repeat("-", 80))
|
||||
fmt.Println(string(cmd))
|
||||
fmt.Println(strings.Repeat("-", 80))
|
||||
fmt.Println(string(expected))
|
||||
fmt.Println(strings.Repeat("-", 80))
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
actual, err := runCode(ctx, code, cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running example %d: %w\nCode was:\n%s", k, err, string(code))
|
||||
}
|
||||
|
||||
fmt.Println(string(actual))
|
||||
fmt.Println(strings.Repeat("=", 80))
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("found %d matches\n", len(matches))
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := Main(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,319 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Argument represents a command line argument
|
||||
type Argument struct {
|
||||
dest path
|
||||
field reflect.StructField // the struct field from which this option was created
|
||||
long string // the --long form for this option, or empty if none
|
||||
short string // the -s short form for this option, or empty if none
|
||||
cardinality cardinality // determines how many tokens will be present (possible values: zero, one, multiple)
|
||||
required bool // if true, this option must be present on the command line
|
||||
positional bool // if true, this option will be looked for in the positional flags
|
||||
separate bool // if true, each slice and map entry will have its own --flag
|
||||
help string // the help text for this option
|
||||
env string // the name of the environment variable for this option, or empty for none
|
||||
defaultVal string // default value for this option
|
||||
placeholder string // name of the data in help
|
||||
}
|
||||
|
||||
// Command represents a named subcommand, or the top-level command
|
||||
type Command struct {
|
||||
name string
|
||||
help string
|
||||
dest path
|
||||
args []*Argument
|
||||
subcommands []*Command
|
||||
parent *Command
|
||||
}
|
||||
|
||||
// Parser represents a set of command line options with destination values
|
||||
type Parser struct {
|
||||
cmd *Command // the top-level command
|
||||
root reflect.Value // destination struct to fill will values
|
||||
version string // version from the argument struct
|
||||
prologue string // prologue for help text (from the argument struct)
|
||||
epilogue string // epilogue for help text (from the argument struct)
|
||||
|
||||
// the following fields are updated during processing of command line arguments
|
||||
leaf *Command // the subcommand we processed last
|
||||
accessible []*Argument // concatenation of the leaf subcommand's arguments plus all ancestors' arguments
|
||||
seen map[*Argument]bool // the arguments we encountered while processing command line arguments
|
||||
}
|
||||
|
||||
// Versioned is the interface that the destination struct should implement to
|
||||
// make a version string appear at the top of the help message.
|
||||
type Versioned interface {
|
||||
// Version returns the version string that will be printed on a line by itself
|
||||
// at the top of the help message.
|
||||
Version() string
|
||||
}
|
||||
|
||||
// Described is the interface that the destination struct should implement to
|
||||
// make a description string appear at the top of the help message.
|
||||
type Described interface {
|
||||
// Description returns the string that will be printed on a line by itself
|
||||
// at the top of the help message.
|
||||
Description() string
|
||||
}
|
||||
|
||||
// Epilogued is the interface that the destination struct should implement to
|
||||
// add an epilogue string at the bottom of the help message.
|
||||
type Epilogued interface {
|
||||
// Epilogue returns the string that will be printed on a line by itself
|
||||
// at the end of the help message.
|
||||
Epilogue() string
|
||||
}
|
||||
|
||||
// the ParserOption interface matches options for the parser constructor
|
||||
type ParserOption interface {
|
||||
parserOption()
|
||||
}
|
||||
|
||||
type programNameParserOption struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func (programNameParserOption) parserOption() {}
|
||||
|
||||
// WithProgramName overrides the name of the program as displayed in help test
|
||||
func WithProgramName(name string) ParserOption {
|
||||
return programNameParserOption{s: name}
|
||||
}
|
||||
|
||||
// NewParser constructs a parser from a list of destination structs
|
||||
func NewParser(dest interface{}, options ...ParserOption) (*Parser, error) {
|
||||
// check the destination type
|
||||
t := reflect.TypeOf(dest)
|
||||
if t.Kind() != reflect.Ptr {
|
||||
panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", t))
|
||||
}
|
||||
|
||||
// pick a program name for help text and usage output
|
||||
program := "program"
|
||||
if len(os.Args) > 0 {
|
||||
program = filepath.Base(os.Args[0])
|
||||
}
|
||||
|
||||
// apply the options
|
||||
for _, opt := range options {
|
||||
switch opt := opt.(type) {
|
||||
case programNameParserOption:
|
||||
program = opt.s
|
||||
}
|
||||
}
|
||||
|
||||
// build the root command from the struct
|
||||
cmd, err := cmdFromStruct(program, path{}, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// construct the parser
|
||||
p := Parser{
|
||||
seen: make(map[*Argument]bool),
|
||||
root: reflect.ValueOf(dest),
|
||||
cmd: cmd,
|
||||
}
|
||||
// copy the args for the root command into "accessible", which will
|
||||
// grow each time we open up a subcommand
|
||||
p.accessible = make([]*Argument, len(p.cmd.args))
|
||||
copy(p.accessible, p.cmd.args)
|
||||
|
||||
// check for version, prologue, and epilogue
|
||||
if dest, ok := dest.(Versioned); ok {
|
||||
p.version = dest.Version()
|
||||
}
|
||||
if dest, ok := dest.(Described); ok {
|
||||
p.prologue = dest.Description()
|
||||
}
|
||||
if dest, ok := dest.(Epilogued); ok {
|
||||
p.epilogue = dest.Epilogue()
|
||||
}
|
||||
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func cmdFromStruct(name string, dest path, t reflect.Type) (*Command, error) {
|
||||
// commands can only be created from pointers to structs
|
||||
if t.Kind() != reflect.Ptr {
|
||||
return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a %s",
|
||||
dest, t.Kind())
|
||||
}
|
||||
|
||||
t = t.Elem()
|
||||
if t.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a pointer to %s",
|
||||
dest, t.Kind())
|
||||
}
|
||||
|
||||
cmd := Command{
|
||||
name: name,
|
||||
dest: dest,
|
||||
}
|
||||
|
||||
var errs []string
|
||||
walkFields(t, func(field reflect.StructField, t reflect.Type) bool {
|
||||
// check for the ignore switch in the tag
|
||||
tag := field.Tag.Get("arg")
|
||||
if tag == "-" {
|
||||
return false
|
||||
}
|
||||
|
||||
// if this is an embedded struct then recurse into its fields, even if
|
||||
// it is unexported, because exported fields on unexported embedded
|
||||
// structs are still writable
|
||||
if field.Anonymous && field.Type.Kind() == reflect.Struct {
|
||||
return true
|
||||
}
|
||||
|
||||
// ignore any other unexported field
|
||||
if !isExported(field.Name) {
|
||||
return false
|
||||
}
|
||||
|
||||
// create a new destination path for this field
|
||||
subdest := dest.Child(field)
|
||||
arg := Argument{
|
||||
dest: subdest,
|
||||
field: field,
|
||||
long: strings.ToLower(field.Name),
|
||||
}
|
||||
|
||||
help, exists := field.Tag.Lookup("help")
|
||||
if exists {
|
||||
arg.help = help
|
||||
}
|
||||
|
||||
defaultVal, hasDefault := field.Tag.Lookup("default")
|
||||
if hasDefault {
|
||||
arg.defaultVal = defaultVal
|
||||
}
|
||||
|
||||
// Look at the tag
|
||||
var isSubcommand bool // tracks whether this field is a subcommand
|
||||
for _, key := range strings.Split(tag, ",") {
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
key = strings.TrimLeft(key, " ")
|
||||
var value string
|
||||
if pos := strings.Index(key, ":"); pos != -1 {
|
||||
value = key[pos+1:]
|
||||
key = key[:pos]
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(key, "---"):
|
||||
errs = append(errs, fmt.Sprintf("%s.%s: too many hyphens", t.Name(), field.Name))
|
||||
case strings.HasPrefix(key, "--"):
|
||||
arg.long = key[2:]
|
||||
case strings.HasPrefix(key, "-"):
|
||||
if len(key) != 2 {
|
||||
errs = append(errs, fmt.Sprintf("%s.%s: short arguments must be one character only",
|
||||
t.Name(), field.Name))
|
||||
return false
|
||||
}
|
||||
arg.short = key[1:]
|
||||
case key == "required":
|
||||
if hasDefault {
|
||||
errs = append(errs, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified",
|
||||
t.Name(), field.Name))
|
||||
return false
|
||||
}
|
||||
arg.required = true
|
||||
case key == "positional":
|
||||
arg.positional = true
|
||||
case key == "separate":
|
||||
arg.separate = true
|
||||
case key == "env":
|
||||
// Use override name if provided
|
||||
if value != "" {
|
||||
arg.env = value
|
||||
} else {
|
||||
arg.env = strings.ToUpper(field.Name)
|
||||
}
|
||||
case key == "subcommand":
|
||||
// decide on a name for the subcommand
|
||||
cmdname := value
|
||||
if cmdname == "" {
|
||||
cmdname = strings.ToLower(field.Name)
|
||||
}
|
||||
|
||||
// parse the subcommand recursively
|
||||
subcmd, err := cmdFromStruct(cmdname, subdest, field.Type)
|
||||
if err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
subcmd.parent = &cmd
|
||||
subcmd.help = field.Tag.Get("help")
|
||||
|
||||
cmd.subcommands = append(cmd.subcommands, subcmd)
|
||||
isSubcommand = true
|
||||
default:
|
||||
errs = append(errs, fmt.Sprintf("unrecognized tag '%s' on field %s", key, tag))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
placeholder, hasPlaceholder := field.Tag.Lookup("placeholder")
|
||||
if hasPlaceholder {
|
||||
arg.placeholder = placeholder
|
||||
} else if arg.long != "" {
|
||||
arg.placeholder = strings.ToUpper(arg.long)
|
||||
} else {
|
||||
arg.placeholder = strings.ToUpper(arg.field.Name)
|
||||
}
|
||||
|
||||
// Check whether this field is supported. It's good to do this here rather than
|
||||
// wait until ParseValue because it means that a program with invalid argument
|
||||
// fields will always fail regardless of whether the arguments it received
|
||||
// exercised those fields.
|
||||
if !isSubcommand {
|
||||
cmd.args = append(cmd.args, &arg)
|
||||
|
||||
var err error
|
||||
arg.cardinality, err = cardinalityOf(field.Type)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported",
|
||||
t.Name(), field.Name, field.Type.String()))
|
||||
return false
|
||||
}
|
||||
if arg.cardinality == multiple && hasDefault {
|
||||
errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields",
|
||||
t.Name(), field.Name))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// if this was an embedded field then we already returned true up above
|
||||
return false
|
||||
})
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, errors.New(strings.Join(errs, "\n"))
|
||||
}
|
||||
|
||||
// check that we don't have both positionals and subcommands
|
||||
var hasPositional bool
|
||||
for _, arg := range cmd.args {
|
||||
if arg.positional {
|
||||
hasPositional = true
|
||||
}
|
||||
}
|
||||
if hasPositional && len(cmd.subcommands) > 0 {
|
||||
return nil, fmt.Errorf("%s cannot have both subcommands and positional arguments", dest)
|
||||
}
|
||||
|
||||
return &cmd, nil
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInvalidTag(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string `arg:"this_is_not_valid"`
|
||||
}
|
||||
_, err := NewParser(&args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUnexportedFieldsSkipped(t *testing.T) {
|
||||
var args struct {
|
||||
unexported struct{}
|
||||
}
|
||||
|
||||
_, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// Package arg parses command line arguments using the fields from a struct.
|
||||
//
|
||||
// For example,
|
||||
//
|
||||
// var args struct {
|
||||
// Iter int
|
||||
// Debug bool
|
||||
// }
|
||||
// arg.MustParse(&args)
|
||||
//
|
||||
// defines two command line arguments, which can be set using any of
|
||||
//
|
||||
// ./example --iter=1 --debug // debug is a boolean flag so its value is set to true
|
||||
// ./example -iter 1 // debug defaults to its zero value (false)
|
||||
// ./example --debug=true // iter defaults to its zero value (zero)
|
||||
//
|
||||
// The fastest way to see how to use go-arg is to read the examples below.
|
||||
//
|
||||
// Fields can be bool, string, any float type, or any signed or unsigned integer type.
|
||||
// They can also be slices of any of the above, or slices of pointers to any of the above.
|
||||
//
|
||||
// Tags can be specified using the `arg` and `help` tag names:
|
||||
//
|
||||
// var args struct {
|
||||
// Input string `arg:"positional"`
|
||||
// Log string `arg:"positional,required"`
|
||||
// Debug bool `arg:"-d" help:"turn on debug mode"`
|
||||
// RealMode bool `arg:"--real"
|
||||
// Wr io.Writer `arg:"-"`
|
||||
// }
|
||||
//
|
||||
// Any tag string that starts with a single hyphen is the short form for an argument
|
||||
// (e.g. `./example -d`), and any tag string that starts with two hyphens is the long
|
||||
// form for the argument (instead of the field name).
|
||||
//
|
||||
// Other valid tag strings are `positional` and `required`.
|
||||
//
|
||||
// Fields can be excluded from processing with `arg:"-"`.
|
||||
package arg
|
|
@ -0,0 +1,507 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func split(s string) []string {
|
||||
return strings.Split(s, " ")
|
||||
}
|
||||
|
||||
// This example demonstrates basic usage
|
||||
func Example() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --foo=hello --bar")
|
||||
|
||||
var args struct {
|
||||
Foo string
|
||||
Bar bool
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println(args.Foo, args.Bar)
|
||||
// output: hello true
|
||||
}
|
||||
|
||||
// This example demonstrates arguments that have default values
|
||||
func Example_defaultValues() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example")
|
||||
|
||||
var args struct {
|
||||
Foo string `default:"abc"`
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println(args.Foo)
|
||||
// output: abc
|
||||
}
|
||||
|
||||
// This example demonstrates arguments that are required
|
||||
func Example_requiredArguments() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --foo=abc --bar")
|
||||
|
||||
var args struct {
|
||||
Foo string `arg:"required"`
|
||||
Bar bool
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println(args.Foo, args.Bar)
|
||||
// output: abc true
|
||||
}
|
||||
|
||||
// This example demonstrates positional arguments
|
||||
func Example_positionalArguments() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example in out1 out2 out3")
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional"`
|
||||
Output []string `arg:"positional"`
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println("In:", args.Input)
|
||||
fmt.Println("Out:", args.Output)
|
||||
// output:
|
||||
// In: in
|
||||
// Out: [out1 out2 out3]
|
||||
}
|
||||
|
||||
// This example demonstrates arguments that have multiple values
|
||||
func Example_multipleValues() {
|
||||
// The args you would pass in on the command line
|
||||
os.Args = split("./example --database localhost --ids 1 2 3")
|
||||
|
||||
var args struct {
|
||||
Database string
|
||||
IDs []int64
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Printf("Fetching the following IDs from %s: %v", args.Database, args.IDs)
|
||||
// output: Fetching the following IDs from localhost: [1 2 3]
|
||||
}
|
||||
|
||||
// This example demonstrates arguments with keys and values
|
||||
func Example_mappings() {
|
||||
// The args you would pass in on the command line
|
||||
os.Args = split("./example --userids john=123 mary=456")
|
||||
|
||||
var args struct {
|
||||
UserIDs map[string]int
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println(args.UserIDs)
|
||||
// output: map[john:123 mary:456]
|
||||
}
|
||||
|
||||
type commaSeparated struct {
|
||||
M map[string]string
|
||||
}
|
||||
|
||||
func (c *commaSeparated) UnmarshalText(b []byte) error {
|
||||
c.M = make(map[string]string)
|
||||
for _, part := range strings.Split(string(b), ",") {
|
||||
pos := strings.Index(part, "=")
|
||||
if pos == -1 {
|
||||
return fmt.Errorf("error parsing %q, expected format key=value", part)
|
||||
}
|
||||
c.M[part[:pos]] = part[pos+1:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// This example demonstrates arguments with keys and values separated by commas
|
||||
func Example_mappingWithCommas() {
|
||||
// The args you would pass in on the command line
|
||||
os.Args = split("./example --values one=two,three=four")
|
||||
|
||||
var args struct {
|
||||
Values commaSeparated
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println(args.Values.M)
|
||||
// output: map[one:two three:four]
|
||||
}
|
||||
|
||||
// This eample demonstrates multiple value arguments that can be mixed with
|
||||
// other arguments.
|
||||
func Example_multipleMixed() {
|
||||
os.Args = split("./example -c cmd1 db1 -f file1 db2 -c cmd2 -f file2 -f file3 db3 -c cmd3")
|
||||
var args struct {
|
||||
Commands []string `arg:"-c,separate"`
|
||||
Files []string `arg:"-f,separate"`
|
||||
Databases []string `arg:"positional"`
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println("Commands:", args.Commands)
|
||||
fmt.Println("Files:", args.Files)
|
||||
fmt.Println("Databases:", args.Databases)
|
||||
|
||||
// output:
|
||||
// Commands: [cmd1 cmd2 cmd3]
|
||||
// Files: [file1 file2 file3]
|
||||
// Databases: [db1 db2 db3]
|
||||
}
|
||||
|
||||
// This example shows the usage string generated by go-arg
|
||||
func Example_helpText() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --help")
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional,required"`
|
||||
Output []string `arg:"positional"`
|
||||
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||
Dataset string `help:"dataset to use"`
|
||||
Optimize int `arg:"-O,--optim" help:"optimization level"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
stdout = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
// Usage: example [--verbose] [--dataset DATASET] [--optim OPTIM] INPUT [OUTPUT [OUTPUT ...]]
|
||||
//
|
||||
// Positional arguments:
|
||||
// INPUT
|
||||
// OUTPUT
|
||||
//
|
||||
// Options:
|
||||
// --verbose, -v verbosity level
|
||||
// --dataset DATASET dataset to use
|
||||
// --optim OPTIM, -O OPTIM
|
||||
// optimization level
|
||||
// --help, -h display this help and exit
|
||||
}
|
||||
|
||||
// This example shows the usage string generated by go-arg with customized placeholders
|
||||
func Example_helpPlaceholder() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --help")
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional,required" placeholder:"SRC"`
|
||||
Output []string `arg:"positional" placeholder:"DST"`
|
||||
Optimize int `arg:"-O" help:"optimization level" placeholder:"LEVEL"`
|
||||
MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" placeholder:"N"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
stdout = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
|
||||
// Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
|
||||
|
||||
// Positional arguments:
|
||||
// SRC
|
||||
// DST
|
||||
|
||||
// Options:
|
||||
// --optimize LEVEL, -O LEVEL
|
||||
// optimization level
|
||||
// --maxjobs N, -j N maximum number of simultaneous jobs
|
||||
// --help, -h display this help and exit
|
||||
}
|
||||
|
||||
// This example shows the usage string generated by go-arg when using subcommands
|
||||
func Example_helpTextWithSubcommand() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --help")
|
||||
|
||||
type getCmd struct {
|
||||
Item string `arg:"positional" help:"item to fetch"`
|
||||
}
|
||||
|
||||
type listCmd struct {
|
||||
Format string `help:"output format"`
|
||||
Limit int
|
||||
}
|
||||
|
||||
var args struct {
|
||||
Verbose bool
|
||||
Get *getCmd `arg:"subcommand" help:"fetch an item and print it"`
|
||||
List *listCmd `arg:"subcommand" help:"list available items"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
stdout = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
// Usage: example [--verbose] <command> [<args>]
|
||||
//
|
||||
// Options:
|
||||
// --verbose
|
||||
// --help, -h display this help and exit
|
||||
//
|
||||
// Commands:
|
||||
// get fetch an item and print it
|
||||
// list list available items
|
||||
}
|
||||
|
||||
// This example shows the usage string generated by go-arg when using subcommands
|
||||
func Example_helpTextWhenUsingSubcommand() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example get --help")
|
||||
|
||||
type getCmd struct {
|
||||
Item string `arg:"positional,required" help:"item to fetch"`
|
||||
}
|
||||
|
||||
type listCmd struct {
|
||||
Format string `help:"output format"`
|
||||
Limit int
|
||||
}
|
||||
|
||||
var args struct {
|
||||
Verbose bool
|
||||
Get *getCmd `arg:"subcommand" help:"fetch an item and print it"`
|
||||
List *listCmd `arg:"subcommand" help:"list available items"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
stdout = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
// Usage: example get ITEM
|
||||
//
|
||||
// Positional arguments:
|
||||
// ITEM item to fetch
|
||||
//
|
||||
// Global options:
|
||||
// --verbose
|
||||
// --help, -h display this help and exit
|
||||
}
|
||||
|
||||
// This example shows how to print help for an explicit subcommand
|
||||
func Example_writeHelpForSubcommand() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example get --help")
|
||||
|
||||
type getCmd struct {
|
||||
Item string `arg:"positional" help:"item to fetch"`
|
||||
}
|
||||
|
||||
type listCmd struct {
|
||||
Format string `help:"output format"`
|
||||
Limit int
|
||||
}
|
||||
|
||||
var args struct {
|
||||
Verbose bool
|
||||
Get *getCmd `arg:"subcommand" help:"fetch an item and print it"`
|
||||
List *listCmd `arg:"subcommand" help:"list available items"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
stdout = os.Stdout
|
||||
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = p.WriteHelpForSubcommand(os.Stdout, "list")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// output:
|
||||
// Usage: example list [--format FORMAT] [--limit LIMIT]
|
||||
//
|
||||
// Options:
|
||||
// --format FORMAT output format
|
||||
// --limit LIMIT
|
||||
//
|
||||
// Global options:
|
||||
// --verbose
|
||||
// --help, -h display this help and exit
|
||||
}
|
||||
|
||||
// This example shows how to print help for a subcommand that is nested several levels deep
|
||||
func Example_writeHelpForSubcommandNested() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example get --help")
|
||||
|
||||
type mostNestedCmd struct {
|
||||
Item string
|
||||
}
|
||||
|
||||
type nestedCmd struct {
|
||||
MostNested *mostNestedCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
type topLevelCmd struct {
|
||||
Nested *nestedCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
var args struct {
|
||||
TopLevel *topLevelCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
stdout = os.Stdout
|
||||
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = p.WriteHelpForSubcommand(os.Stdout, "toplevel", "nested", "mostnested")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// output:
|
||||
// Usage: example toplevel nested mostnested [--item ITEM]
|
||||
//
|
||||
// Options:
|
||||
// --item ITEM
|
||||
// --help, -h display this help and exit
|
||||
}
|
||||
|
||||
// This example shows the error string generated by go-arg when an invalid option is provided
|
||||
func Example_errorText() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --optimize INVALID")
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional,required"`
|
||||
Output []string `arg:"positional"`
|
||||
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||
Dataset string `help:"dataset to use"`
|
||||
Optimize int `arg:"-O,help:optimization level"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
stderr = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
// Usage: example [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]]
|
||||
// error: error processing default value for --optimize: strconv.ParseInt: parsing "INVALID": invalid syntax
|
||||
}
|
||||
|
||||
// This example shows the error string generated by go-arg when an invalid option is provided
|
||||
func Example_errorTextForSubcommand() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example get --count INVALID")
|
||||
|
||||
type getCmd struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
var args struct {
|
||||
Get *getCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
stderr = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
// Usage: example get [--count COUNT]
|
||||
// error: error processing default value for --count: strconv.ParseInt: parsing "INVALID": invalid syntax
|
||||
}
|
||||
|
||||
// This example demonstrates use of subcommands
|
||||
func Example_subcommand() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example commit -a -m what-this-commit-is-about")
|
||||
|
||||
type CheckoutCmd struct {
|
||||
Branch string `arg:"positional"`
|
||||
Track bool `arg:"-t"`
|
||||
}
|
||||
type CommitCmd struct {
|
||||
All bool `arg:"-a"`
|
||||
Message string `arg:"-m"`
|
||||
}
|
||||
type PushCmd struct {
|
||||
Remote string `arg:"positional"`
|
||||
Branch string `arg:"positional"`
|
||||
SetUpstream bool `arg:"-u"`
|
||||
}
|
||||
var args struct {
|
||||
Checkout *CheckoutCmd `arg:"subcommand:checkout"`
|
||||
Commit *CommitCmd `arg:"subcommand:commit"`
|
||||
Push *PushCmd `arg:"subcommand:push"`
|
||||
Quiet bool `arg:"-q"` // this flag is global to all subcommands
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
osExit = func(int) {}
|
||||
stderr = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
switch {
|
||||
case args.Checkout != nil:
|
||||
fmt.Printf("checkout requested for branch %s\n", args.Checkout.Branch)
|
||||
case args.Commit != nil:
|
||||
fmt.Printf("commit requested with message \"%s\"\n", args.Commit.Message)
|
||||
case args.Push != nil:
|
||||
fmt.Printf("push requested from %s to %s\n", args.Push.Branch, args.Push.Remote)
|
||||
}
|
||||
|
||||
// output:
|
||||
// commit requested with message "what-this-commit-is-about"
|
||||
}
|
||||
|
||||
func Example_allSupportedTypes() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = []string{}
|
||||
|
||||
var args struct {
|
||||
Bool bool
|
||||
Byte byte
|
||||
Rune rune
|
||||
Int int
|
||||
Int8 int8
|
||||
Int16 int16
|
||||
Int32 int32
|
||||
Int64 int64
|
||||
Float32 float32
|
||||
Float64 float64
|
||||
String string
|
||||
Duration time.Duration
|
||||
URL url.URL
|
||||
Email mail.Address
|
||||
MAC net.HardwareAddr
|
||||
}
|
||||
|
||||
// go-arg supports each of the types above, as well as pointers to any of
|
||||
// the above and slices of any of the above. It also supports any types that
|
||||
// implements encoding.TextUnmarshaler.
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
module github.com/alexflint/go-arg/v2
|
||||
|
||||
require (
|
||||
github.com/alexflint/go-scalar v1.2.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
)
|
||||
|
||||
go 1.13
|
|
@ -0,0 +1,15 @@
|
|||
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
||||
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -0,0 +1,561 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
scalar "github.com/alexflint/go-scalar"
|
||||
)
|
||||
|
||||
// ErrHelp indicates that -h or --help were provided
|
||||
var ErrHelp = errors.New("help requested by user")
|
||||
|
||||
// ErrVersion indicates that --version was provided
|
||||
var ErrVersion = errors.New("version requested by user")
|
||||
|
||||
// MustParse processes command line arguments and exits upon failure
|
||||
func MustParse(dest interface{}) *Parser {
|
||||
p, err := NewParser(dest)
|
||||
if err != nil {
|
||||
fmt.Fprintln(stdout, err)
|
||||
osExit(-1)
|
||||
return nil // just in case osExit was monkey-patched
|
||||
}
|
||||
|
||||
err = p.Parse(os.Args, os.Environ())
|
||||
switch {
|
||||
case err == ErrHelp:
|
||||
p.writeHelpForSubcommand(stdout, p.leaf)
|
||||
osExit(0)
|
||||
case err == ErrVersion:
|
||||
fmt.Fprintln(stdout, p.version)
|
||||
osExit(0)
|
||||
case err != nil:
|
||||
p.failWithSubcommand(err.Error(), p.leaf)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Parse processes command line arguments and stores them in dest
|
||||
func Parse(dest interface{}, options ...ParserOption) error {
|
||||
p, err := NewParser(dest, options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.Parse(os.Args, os.Environ())
|
||||
}
|
||||
|
||||
// Parse processes the given command line option, storing the results in the field
|
||||
// of the structs from which NewParser was constructed
|
||||
func (p *Parser) Parse(args, env []string) error {
|
||||
p.seen = make(map[*Argument]bool)
|
||||
|
||||
// If -h or --help were specified then make sure help text supercedes other errors
|
||||
var help bool
|
||||
for _, arg := range args {
|
||||
if arg == "-h" || arg == "--help" {
|
||||
help = true
|
||||
}
|
||||
if arg == "--" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err := p.ProcessCommandLine(args)
|
||||
if err != nil {
|
||||
if help {
|
||||
return ErrHelp
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
err = p.ProcessEnvironment(env)
|
||||
if err != nil {
|
||||
if help {
|
||||
return ErrHelp
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
err = p.ProcessDefaults()
|
||||
if err != nil {
|
||||
if help {
|
||||
return ErrHelp
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Validate()
|
||||
}
|
||||
|
||||
// ProcessCommandLine scans arguments one-by-one, parses them and assigns
|
||||
// the result to fields of the struct passed to NewParser. It returns
|
||||
// an error if an argument is invalid or unknown, but not if a
|
||||
// required argument is missing. To check that all required arguments
|
||||
// are set, call Validate(). This function ignores the first element
|
||||
// of args, which is assumed to be the program name itself. This function
|
||||
// never overwrites arguments previously seen in a call to any Process*
|
||||
// function.
|
||||
func (p *Parser) ProcessCommandLine(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
positionals, err := p.ProcessOptions(args[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.ProcessPositionals(positionals)
|
||||
}
|
||||
|
||||
// OverwriteWithCommandLine is like ProcessCommandLine but it overwrites
|
||||
// any previously seen values.
|
||||
func (p *Parser) OverwriteWithCommandLine(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
positionals, err := p.OverwriteWithOptions(args[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.OverwriteWithPositionals(positionals)
|
||||
}
|
||||
|
||||
// ProcessOptions processes options but not positionals from the
|
||||
// command line. Positionals are returned and can be passed to
|
||||
// ProcessPositionals. Arguments seen in a previous call to any
|
||||
// Process* or OverwriteWith* functions are ignored.
|
||||
func (p *Parser) ProcessOptions(args []string) ([]string, error) {
|
||||
return p.processOptions(args, false)
|
||||
}
|
||||
|
||||
// OverwriteWithOptions is like ProcessOptions except previously seen
|
||||
// arguments are overwritten
|
||||
func (p *Parser) OverwriteWithOptions(args []string) ([]string, error) {
|
||||
return p.processOptions(args, true)
|
||||
}
|
||||
|
||||
func (p *Parser) processOptions(args []string, overwrite bool) ([]string, error) {
|
||||
// note that p.cmd.args has already been copied into p.accessible in NewParser
|
||||
|
||||
// union of args for the chain of subcommands encountered so far
|
||||
p.leaf = p.cmd
|
||||
var allpositional bool
|
||||
var positionals []string
|
||||
|
||||
// must use explicit for loop, not range, because we manipulate i inside the loop
|
||||
for i := 0; i < len(args); i++ {
|
||||
token := args[i]
|
||||
|
||||
// the "--" token indicates that all further tokens should be treated as positionals
|
||||
if token == "--" {
|
||||
allpositional = true
|
||||
continue
|
||||
}
|
||||
|
||||
// check whether this is a positional argument
|
||||
if !isFlag(token) || allpositional {
|
||||
// each subcommand can have either subcommands or positionals, but not both
|
||||
if len(p.leaf.subcommands) == 0 {
|
||||
positionals = append(positionals, token)
|
||||
continue
|
||||
}
|
||||
|
||||
// if we have a subcommand then make sure it is valid for the current context
|
||||
subcmd := findSubcommand(p.leaf.subcommands, token)
|
||||
if subcmd == nil {
|
||||
return nil, fmt.Errorf("invalid subcommand: %s", token)
|
||||
}
|
||||
|
||||
// instantiate the field to point to a new struct
|
||||
v := p.val(subcmd.dest)
|
||||
if v.IsNil() {
|
||||
v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers
|
||||
}
|
||||
|
||||
// add the new options to the set of allowed options
|
||||
p.accessible = append(p.accessible, subcmd.args...)
|
||||
p.leaf = subcmd
|
||||
continue
|
||||
}
|
||||
|
||||
// check for special --help and --version flags
|
||||
switch token {
|
||||
case "-h", "--help":
|
||||
return nil, ErrHelp
|
||||
case "--version":
|
||||
return nil, ErrVersion
|
||||
}
|
||||
|
||||
// check for an equals sign, as in "--foo=bar"
|
||||
var value string
|
||||
opt := strings.TrimLeft(token, "-")
|
||||
if pos := strings.Index(opt, "="); pos != -1 {
|
||||
value = opt[pos+1:]
|
||||
opt = opt[:pos]
|
||||
}
|
||||
|
||||
// look up the arg for this option (note that the "args" slice changes as
|
||||
// we expand subcommands so it is better not to use a map)
|
||||
arg := findOption(p.accessible, opt)
|
||||
if arg == nil {
|
||||
return nil, fmt.Errorf("unknown argument %s", token)
|
||||
}
|
||||
|
||||
// for the case of multiple values, consume tokens until next --option
|
||||
if arg.cardinality == multiple && !arg.separate {
|
||||
var values []string
|
||||
if value == "" {
|
||||
for i+1 < len(args) && !isFlag(args[i+1]) && args[i+1] != "--" {
|
||||
values = append(values, args[i+1])
|
||||
i++
|
||||
}
|
||||
} else {
|
||||
values = append(values, value)
|
||||
}
|
||||
|
||||
if err := p.processSequence(arg, values, overwrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// if it's a flag and it has no value then set the value to true
|
||||
// use boolean because this takes account of TextUnmarshaler
|
||||
if arg.cardinality == zero && value == "" {
|
||||
value = "true"
|
||||
}
|
||||
|
||||
// if we have something like "--foo" then the value is the next argument
|
||||
if value == "" {
|
||||
if i+1 == len(args) {
|
||||
return nil, fmt.Errorf("missing value for %s", token)
|
||||
}
|
||||
if isFlag(args[i+1]) {
|
||||
return nil, fmt.Errorf("missing value for %s", token)
|
||||
}
|
||||
value = args[i+1]
|
||||
i++
|
||||
}
|
||||
|
||||
// send the value to the argument
|
||||
if err := p.processScalar(arg, value, overwrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return positionals, nil
|
||||
}
|
||||
|
||||
// ProcessPositionals processes a list of positional arguments. If
|
||||
// this list contains tokens that begin with a hyphen they will still be
|
||||
// treated as positional arguments. Arguments seen in a previous call
|
||||
// to any Process* or OverwriteWith* functions are ignored.
|
||||
func (p *Parser) ProcessPositionals(positionals []string) error {
|
||||
return p.processPositionals(positionals, false)
|
||||
}
|
||||
|
||||
// OverwriteWithPositionals is like ProcessPositionals except previously
|
||||
// seen arguments are overwritten.
|
||||
func (p *Parser) OverwriteWithPositionals(positionals []string) error {
|
||||
return p.processPositionals(positionals, true)
|
||||
}
|
||||
|
||||
func (p *Parser) processPositionals(positionals []string, overwrite bool) error {
|
||||
for _, arg := range p.accessible {
|
||||
if !arg.positional {
|
||||
continue
|
||||
}
|
||||
if len(positionals) == 0 {
|
||||
break
|
||||
}
|
||||
if arg.cardinality == multiple {
|
||||
if err := p.processSequence(arg, positionals, overwrite); err != nil {
|
||||
return err
|
||||
}
|
||||
positionals = nil
|
||||
break
|
||||
}
|
||||
|
||||
if err := p.processScalar(arg, positionals[0], overwrite); err != nil {
|
||||
return err
|
||||
}
|
||||
positionals = positionals[1:]
|
||||
}
|
||||
|
||||
if len(positionals) > 0 {
|
||||
return fmt.Errorf("too many positional arguments at '%s'", positionals[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessEnvironment processes environment variables from a list of strings
|
||||
// of the form KEY=VALUE. You can pass in os.Environ(). It
|
||||
// does not overwrite any fields with values already populated.
|
||||
func (p *Parser) ProcessEnvironment(environ []string) error {
|
||||
return p.processEnvironment(environ, false)
|
||||
}
|
||||
|
||||
// OverwriteWithEnvironment processes environment variables from a list
|
||||
// of strings of the form "KEY=VALUE". Any existing values are overwritten.
|
||||
func (p *Parser) OverwriteWithEnvironment(environ []string) error {
|
||||
return p.processEnvironment(environ, true)
|
||||
}
|
||||
|
||||
// ProcessEnvironment processes environment variables from a list of strings
|
||||
// of the form KEY=VALUE. You can pass in os.Environ(). It
|
||||
// overwrites already-populated fields only if overwrite is true.
|
||||
func (p *Parser) processEnvironment(environ []string, overwrite bool) error {
|
||||
// parse the list of KEY=VAL strings in environ
|
||||
env := make(map[string]string)
|
||||
for _, s := range environ {
|
||||
if i := strings.Index(s, "="); i >= 0 {
|
||||
env[s[:i]] = s[i+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// process arguments one-by-one
|
||||
for _, arg := range p.accessible {
|
||||
if arg.env == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
value, found := env[arg.env]
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
if arg.cardinality == multiple && !arg.separate {
|
||||
// expect a CSV string in an environment
|
||||
// variable in the case of multiple values
|
||||
var values []string
|
||||
if len(strings.TrimSpace(value)) > 0 {
|
||||
var err error
|
||||
values, err = csv.NewReader(strings.NewReader(value)).Read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing CSV string from environment variable %s: %v", arg.env, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.processSequence(arg, values, overwrite); err != nil {
|
||||
return fmt.Errorf("error processing environment variable %s: %v", arg.env, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := p.processScalar(arg, value, overwrite); err != nil {
|
||||
return fmt.Errorf("error processing environment variable %s: %v", arg.env, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessDefaults assigns default values to all fields that have default values and
|
||||
// are not already populated.
|
||||
func (p *Parser) ProcessDefaults() error {
|
||||
return p.processDefaults(false)
|
||||
}
|
||||
|
||||
// OverwriteWithDefaults assigns default values to all fields that have default values,
|
||||
// overwriting any previous value
|
||||
func (p *Parser) OverwriteWithDefaults() error {
|
||||
return p.processDefaults(true)
|
||||
}
|
||||
|
||||
// processDefaults assigns default values to all fields in all expanded subcommands.
|
||||
// If overwrite is true then it overwrites existing values.
|
||||
func (p *Parser) processDefaults(overwrite bool) error {
|
||||
for _, arg := range p.accessible {
|
||||
if arg.defaultVal == "" {
|
||||
continue
|
||||
}
|
||||
if err := p.processScalar(arg, arg.defaultVal, overwrite); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processScalar parses a single argument, inserts it into the struct,
|
||||
// and marks the argument as "seen" (unless the argument has been seen
|
||||
// before and overwrite=false, in which case the value is ignored)
|
||||
func (p *Parser) processScalar(arg *Argument, value string, overwrite bool) error {
|
||||
if p.seen[arg] && !overwrite && !arg.separate {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := strings.ToLower(arg.field.Name)
|
||||
if arg.long != "" && !arg.positional {
|
||||
name = "--" + arg.long
|
||||
}
|
||||
|
||||
if arg.cardinality == multiple {
|
||||
err := appendToSliceOrMap(p.val(arg.dest), value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing default value for %s: %v", name, err)
|
||||
}
|
||||
} else {
|
||||
err := scalar.ParseValue(p.val(arg.dest), value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing default value for %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
p.seen[arg] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// processSequence parses a sequence argument, inserts it into the struct,
|
||||
// and marks the argument as "seen" (unless the argument has been seen
|
||||
// before and overwrite=false, in which case the value is ignored)
|
||||
func (p *Parser) processSequence(arg *Argument, values []string, overwrite bool) error {
|
||||
if p.seen[arg] && !overwrite && !arg.separate {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := strings.ToLower(arg.field.Name)
|
||||
if arg.long != "" && !arg.positional {
|
||||
name = "--" + arg.long
|
||||
}
|
||||
|
||||
if arg.cardinality != multiple {
|
||||
panic(fmt.Sprintf("processSequence called for argument %s which has cardinality %v", arg.field.Name, arg.cardinality))
|
||||
}
|
||||
|
||||
err := setSliceOrMap(p.val(arg.dest), values)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing default value for %s: %v", name, err)
|
||||
}
|
||||
|
||||
p.seen[arg] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Missing returns a list of required arguments that were not provided
|
||||
func (p *Parser) Missing() []*Argument {
|
||||
var missing []*Argument
|
||||
for _, arg := range p.accessible {
|
||||
if arg.required && !p.seen[arg] {
|
||||
missing = append(missing, arg)
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
// Validate returns an error if any required arguments were missing
|
||||
func (p *Parser) Validate() error {
|
||||
if missing := p.Missing(); len(missing) > 0 {
|
||||
name := strings.ToLower(missing[0].field.Name)
|
||||
if missing[0].long != "" && !missing[0].positional {
|
||||
name = "--" + missing[0].long
|
||||
}
|
||||
|
||||
if missing[0].env == "" {
|
||||
return fmt.Errorf("%s is required", name)
|
||||
}
|
||||
return fmt.Errorf("%s is required (or environment variable %s)", name, missing[0].env)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// val returns a reflect.Value corresponding to the current value for the
|
||||
// given path
|
||||
func (p *Parser) val(dest path) reflect.Value {
|
||||
v := p.root
|
||||
for _, field := range dest.fields {
|
||||
if v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return reflect.Value{}
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
v = v.FieldByIndex(field.Index)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// path represents a sequence of steps to find the output location for an
|
||||
// argument or subcommand in the final destination struct
|
||||
type path struct {
|
||||
fields []reflect.StructField // sequence of struct fields to traverse
|
||||
}
|
||||
|
||||
// String gets a string representation of the given path
|
||||
func (p path) String() string {
|
||||
s := "args"
|
||||
for _, f := range p.fields {
|
||||
s += "." + f.Name
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Child gets a new path representing a child of this path.
|
||||
func (p path) Child(f reflect.StructField) path {
|
||||
// copy the entire slice of fields to avoid possible slice overwrite
|
||||
subfields := make([]reflect.StructField, len(p.fields)+1)
|
||||
copy(subfields, p.fields)
|
||||
subfields[len(subfields)-1] = f
|
||||
return path{
|
||||
fields: subfields,
|
||||
}
|
||||
}
|
||||
|
||||
// walkFields calls a function for each field of a struct, recursively expanding struct fields.
|
||||
func walkFields(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool) {
|
||||
walkFieldsImpl(t, visit, nil)
|
||||
}
|
||||
|
||||
func walkFieldsImpl(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool, path []int) {
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
field.Index = make([]int, len(path)+1)
|
||||
copy(field.Index, append(path, i))
|
||||
expand := visit(field, t)
|
||||
if expand && field.Type.Kind() == reflect.Struct {
|
||||
var subpath []int
|
||||
if field.Anonymous {
|
||||
subpath = append(path, i)
|
||||
}
|
||||
walkFieldsImpl(field.Type, visit, subpath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isFlag returns true if a token is a flag such as "-v" or "--user" but not "-" or "--"
|
||||
func isFlag(s string) bool {
|
||||
return strings.HasPrefix(s, "-") && strings.TrimLeft(s, "-") != ""
|
||||
}
|
||||
|
||||
// findOption finds an option from its name, or returns nil if no arg is found
|
||||
func findOption(args []*Argument, name string) *Argument {
|
||||
for _, arg := range args {
|
||||
if arg.positional {
|
||||
continue
|
||||
}
|
||||
if arg.long == name || arg.short == name {
|
||||
return arg
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findSubcommand finds a subcommand using its name, or returns nil if no subcommand is found
|
||||
func findSubcommand(cmds []*Command, name string) *Command {
|
||||
for _, cmd := range cmds {
|
||||
if cmd.name == name {
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,271 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// this file contains tests related to the precedence rules for:
|
||||
// ProcessCommandLine
|
||||
// ProcessOptions
|
||||
// ProcessPositionals
|
||||
// ProcessEnvironment
|
||||
// ProcessMap
|
||||
// ProcessSingle
|
||||
// ProcessSequence
|
||||
// OverwriteWithCommandLine
|
||||
// OverwriteWithOptions
|
||||
// OverwriteWithPositionals
|
||||
// OverwriteWithEnvironment
|
||||
// OverwriteWithMap
|
||||
//
|
||||
// The Process* functions should not overwrite fields that have
|
||||
// been previously populated, whereas the OverwriteWith* functions
|
||||
// should overwrite fields that have been previously populated.
|
||||
|
||||
// check that we can accumulate "separate" args across env, cmdline, map, and defaults
|
||||
|
||||
// check what happens if we have a required arg with a default value
|
||||
|
||||
// add more tests for combinations of separate and cardinality
|
||||
|
||||
// check what happens if we call ProcessCommandLine multiple times with different subcommands
|
||||
|
||||
func TestProcessOptions(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = p.ProcessOptions([]string{"program", "--arg=hello"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessOptionsDoesNotOverwrite(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessEnvironment([]string{"ARG=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = p.ProcessOptions([]string{"--arg=hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "123", args.Arg)
|
||||
}
|
||||
|
||||
func TestOverwriteWithOptions(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessEnvironment([]string{"ARG=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = p.OverwriteWithOptions([]string{"--arg=hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessPositionals(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"positional"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessPositionals([]string{"hello"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessPositionalsDoesNotOverwrite(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env,positional"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessEnvironment([]string{"ARG=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessPositionals([]string{"hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "123", args.Arg)
|
||||
}
|
||||
|
||||
func TestOverwriteWithPositionals(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env,positional"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessEnvironment([]string{"ARG=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.OverwriteWithPositionals([]string{"hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessCommandLine(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessCommandLine([]string{"program", "--arg=hello"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessCommandLineDoesNotOverwrite(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessEnvironment([]string{"ARG=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessCommandLine([]string{"program", "--arg=hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "123", args.Arg)
|
||||
}
|
||||
|
||||
func TestOverwriteWithCommandLine(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessEnvironment([]string{"ARG=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.OverwriteWithCommandLine([]string{"program", "--arg=hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessEnvironment(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessEnvironment([]string{"ARG=hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessEnvironmentDoesNotOverwrite(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = p.ProcessOptions([]string{"--arg=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessEnvironment([]string{"ARG=hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "123", args.Arg)
|
||||
}
|
||||
|
||||
func TestOverwriteWithEnvironment(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `arg:"env"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = p.ProcessOptions([]string{"--arg=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.OverwriteWithEnvironment([]string{"ARG=hello"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessDefaults(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `default:"hello"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessDefaults()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "hello", args.Arg)
|
||||
}
|
||||
|
||||
func TestProcessDefaultsDoesNotOverwrite(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `default:"hello"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = p.ProcessOptions([]string{"--arg=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.ProcessDefaults()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "123", args.Arg)
|
||||
}
|
||||
|
||||
func TestOverwriteWithDefaults(t *testing.T) {
|
||||
var args struct {
|
||||
Arg string `default:"hello"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = p.ProcessOptions([]string{"--arg=123"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.OverwriteWithDefaults()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "hello", args.Arg)
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
scalar "github.com/alexflint/go-scalar"
|
||||
)
|
||||
|
||||
var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()
|
||||
|
||||
// cardinality tracks how many tokens are expected for a given spec
|
||||
// - zero is a boolean, which does to expect any value
|
||||
// - one is an ordinary option that will be parsed from a single token
|
||||
// - multiple is a slice or map that can accept zero or more tokens
|
||||
type cardinality int
|
||||
|
||||
const (
|
||||
zero cardinality = iota
|
||||
one
|
||||
multiple
|
||||
unsupported
|
||||
)
|
||||
|
||||
func (k cardinality) String() string {
|
||||
switch k {
|
||||
case zero:
|
||||
return "zero"
|
||||
case one:
|
||||
return "one"
|
||||
case multiple:
|
||||
return "multiple"
|
||||
case unsupported:
|
||||
return "unsupported"
|
||||
default:
|
||||
return fmt.Sprintf("unknown(%d)", int(k))
|
||||
}
|
||||
}
|
||||
|
||||
// cardinalityOf returns true if the type can be parsed from a string
|
||||
func cardinalityOf(t reflect.Type) (cardinality, error) {
|
||||
if scalar.CanParse(t) {
|
||||
if isBoolean(t) {
|
||||
return zero, nil
|
||||
}
|
||||
return one, nil
|
||||
}
|
||||
|
||||
// look inside pointer types
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
// look inside slice and map types
|
||||
switch t.Kind() {
|
||||
case reflect.Slice:
|
||||
if !scalar.CanParse(t.Elem()) {
|
||||
return unsupported, fmt.Errorf("cannot parse into %v because %v not supported", t, t.Elem())
|
||||
}
|
||||
return multiple, nil
|
||||
case reflect.Map:
|
||||
if !scalar.CanParse(t.Key()) {
|
||||
return unsupported, fmt.Errorf("cannot parse into %v because key type %v not supported", t, t.Elem())
|
||||
}
|
||||
if !scalar.CanParse(t.Elem()) {
|
||||
return unsupported, fmt.Errorf("cannot parse into %v because value type %v not supported", t, t.Elem())
|
||||
}
|
||||
return multiple, nil
|
||||
default:
|
||||
return unsupported, fmt.Errorf("cannot parse into %v", t)
|
||||
}
|
||||
}
|
||||
|
||||
// isBoolean returns true if the type can be parsed from a single string
|
||||
func isBoolean(t reflect.Type) bool {
|
||||
switch {
|
||||
case t.Implements(textUnmarshalerType):
|
||||
return false
|
||||
case t.Kind() == reflect.Bool:
|
||||
return true
|
||||
case t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Bool:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// isExported returns true if the struct field name is exported
|
||||
func isExported(field string) bool {
|
||||
r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8
|
||||
return unicode.IsLetter(r) && unicode.IsUpper(r)
|
||||
}
|
||||
|
||||
// isZero returns true if v contains the zero value for its type
|
||||
func isZero(v reflect.Value) bool {
|
||||
t := v.Type()
|
||||
if t.Kind() == reflect.Slice || t.Kind() == reflect.Map {
|
||||
return v.IsNil()
|
||||
}
|
||||
if !t.Comparable() {
|
||||
return false
|
||||
}
|
||||
return v.Interface() == reflect.Zero(t).Interface()
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func assertCardinality(t *testing.T, typ reflect.Type, expected cardinality) {
|
||||
actual, err := cardinalityOf(typ)
|
||||
assert.Equal(t, expected, actual, "expected %v to have cardinality %v but got %v", typ, expected, actual)
|
||||
if expected == unsupported {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCardinalityOf(t *testing.T) {
|
||||
var b bool
|
||||
var i int
|
||||
var s string
|
||||
var f float64
|
||||
var bs []bool
|
||||
var is []int
|
||||
var m map[string]int
|
||||
var unsupported1 struct{}
|
||||
var unsupported2 []struct{}
|
||||
var unsupported3 map[string]struct{}
|
||||
var unsupported4 map[struct{}]string
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(b), zero)
|
||||
assertCardinality(t, reflect.TypeOf(i), one)
|
||||
assertCardinality(t, reflect.TypeOf(s), one)
|
||||
assertCardinality(t, reflect.TypeOf(f), one)
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(&b), zero)
|
||||
assertCardinality(t, reflect.TypeOf(&s), one)
|
||||
assertCardinality(t, reflect.TypeOf(&i), one)
|
||||
assertCardinality(t, reflect.TypeOf(&f), one)
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(bs), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(is), multiple)
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(&bs), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(&is), multiple)
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(m), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(&m), multiple)
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(unsupported1), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(&unsupported1), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(unsupported2), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(&unsupported2), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(unsupported3), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(&unsupported3), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(unsupported4), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(&unsupported4), unsupported)
|
||||
}
|
||||
|
||||
type implementsTextUnmarshaler struct{}
|
||||
|
||||
func (*implementsTextUnmarshaler) UnmarshalText(text []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCardinalityTextUnmarshaler(t *testing.T) {
|
||||
var x implementsTextUnmarshaler
|
||||
var s []implementsTextUnmarshaler
|
||||
var m []implementsTextUnmarshaler
|
||||
assertCardinality(t, reflect.TypeOf(x), one)
|
||||
assertCardinality(t, reflect.TypeOf(&x), one)
|
||||
assertCardinality(t, reflect.TypeOf(s), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(&s), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(m), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(&m), multiple)
|
||||
}
|
||||
|
||||
func TestIsExported(t *testing.T) {
|
||||
assert.True(t, isExported("Exported"))
|
||||
assert.False(t, isExported("notExported"))
|
||||
assert.False(t, isExported(""))
|
||||
assert.False(t, isExported(string([]byte{255})))
|
||||
}
|
||||
|
||||
func TestCardinalityString(t *testing.T) {
|
||||
assert.Equal(t, "zero", zero.String())
|
||||
assert.Equal(t, "one", one.String())
|
||||
assert.Equal(t, "multiple", multiple.String())
|
||||
assert.Equal(t, "unsupported", unsupported.String())
|
||||
assert.Equal(t, "unknown(42)", cardinality(42).String())
|
||||
}
|
||||
|
||||
func TestIsZero(t *testing.T) {
|
||||
var zero int
|
||||
var notZero = 3
|
||||
var nilSlice []int
|
||||
var nonNilSlice = []int{1, 2, 3}
|
||||
var nilMap map[string]string
|
||||
var nonNilMap = map[string]string{"foo": "bar"}
|
||||
var uncomparable = func() {}
|
||||
|
||||
assert.True(t, isZero(reflect.ValueOf(zero)))
|
||||
assert.False(t, isZero(reflect.ValueOf(notZero)))
|
||||
|
||||
assert.True(t, isZero(reflect.ValueOf(nilSlice)))
|
||||
assert.False(t, isZero(reflect.ValueOf(nonNilSlice)))
|
||||
|
||||
assert.True(t, isZero(reflect.ValueOf(nilMap)))
|
||||
assert.False(t, isZero(reflect.ValueOf(nonNilMap)))
|
||||
|
||||
assert.False(t, isZero(reflect.ValueOf(uncomparable)))
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
scalar "github.com/alexflint/go-scalar"
|
||||
)
|
||||
|
||||
// setSliceOrMap parses a sequence of strings into a slice or map. The slice or
|
||||
// map is always cleared first.
|
||||
func setSliceOrMap(dest reflect.Value, values []string) error {
|
||||
if !dest.CanSet() {
|
||||
return fmt.Errorf("field is not writable")
|
||||
}
|
||||
|
||||
t := dest.Type()
|
||||
if t.Kind() == reflect.Ptr {
|
||||
dest = dest.Elem()
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
switch t.Kind() {
|
||||
case reflect.Slice:
|
||||
return setSlice(dest, values)
|
||||
case reflect.Map:
|
||||
return setMap(dest, values)
|
||||
default:
|
||||
return fmt.Errorf("cannot insert multiple values into a %v", t)
|
||||
}
|
||||
}
|
||||
|
||||
// setSlice parses a sequence of strings and inserts them into a slice. The
|
||||
// slice is cleared first.
|
||||
func setSlice(dest reflect.Value, values []string) error {
|
||||
var ptr bool
|
||||
elem := dest.Type().Elem()
|
||||
if elem.Kind() == reflect.Ptr && !elem.Implements(textUnmarshalerType) {
|
||||
ptr = true
|
||||
elem = elem.Elem()
|
||||
}
|
||||
|
||||
// clear the slice in case default values exist
|
||||
if !dest.IsNil() {
|
||||
dest.SetLen(0)
|
||||
}
|
||||
|
||||
// parse the values one-by-one
|
||||
for _, s := range values {
|
||||
v := reflect.New(elem)
|
||||
if err := scalar.ParseValue(v.Elem(), s); err != nil {
|
||||
return err
|
||||
}
|
||||
if !ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
dest.Set(reflect.Append(dest, v))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setMap parses a sequence of name=value strings and inserts them into a map.
|
||||
// The map is always cleared first.
|
||||
func setMap(dest reflect.Value, values []string) error {
|
||||
// determine the key and value type
|
||||
var keyIsPtr bool
|
||||
keyType := dest.Type().Key()
|
||||
if keyType.Kind() == reflect.Ptr && !keyType.Implements(textUnmarshalerType) {
|
||||
keyIsPtr = true
|
||||
keyType = keyType.Elem()
|
||||
}
|
||||
|
||||
var valIsPtr bool
|
||||
valType := dest.Type().Elem()
|
||||
if valType.Kind() == reflect.Ptr && !valType.Implements(textUnmarshalerType) {
|
||||
valIsPtr = true
|
||||
valType = valType.Elem()
|
||||
}
|
||||
|
||||
// clear the slice in case default values exist
|
||||
if !dest.IsNil() {
|
||||
for _, k := range dest.MapKeys() {
|
||||
dest.SetMapIndex(k, reflect.Value{})
|
||||
}
|
||||
}
|
||||
|
||||
// allocate the map if it is not allocated
|
||||
if dest.IsNil() {
|
||||
dest.Set(reflect.MakeMap(dest.Type()))
|
||||
}
|
||||
|
||||
// parse the values one-by-one
|
||||
for _, s := range values {
|
||||
// split at the first equals sign
|
||||
pos := strings.Index(s, "=")
|
||||
if pos == -1 {
|
||||
return fmt.Errorf("cannot parse %q into a map, expected format key=value", s)
|
||||
}
|
||||
|
||||
// parse the key
|
||||
k := reflect.New(keyType)
|
||||
if err := scalar.ParseValue(k.Elem(), s[:pos]); err != nil {
|
||||
return err
|
||||
}
|
||||
if !keyIsPtr {
|
||||
k = k.Elem()
|
||||
}
|
||||
|
||||
// parse the value
|
||||
v := reflect.New(valType)
|
||||
if err := scalar.ParseValue(v.Elem(), s[pos+1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if !valIsPtr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
// add it to the map
|
||||
dest.SetMapIndex(k, v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// appendSliceOrMap parses a string and appends it to an existing slice or map.
|
||||
func appendToSliceOrMap(dest reflect.Value, value string) error {
|
||||
if !dest.CanSet() {
|
||||
return fmt.Errorf("field is not writable")
|
||||
}
|
||||
|
||||
t := dest.Type()
|
||||
if t.Kind() == reflect.Ptr {
|
||||
dest = dest.Elem()
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
switch t.Kind() {
|
||||
case reflect.Slice:
|
||||
return appendToSlice(dest, value)
|
||||
case reflect.Map:
|
||||
return appendToMap(dest, value)
|
||||
default:
|
||||
return fmt.Errorf("cannot insert multiple values into a %v", t)
|
||||
}
|
||||
}
|
||||
|
||||
// appendSlice parses a string and appends the result into a slice.
|
||||
func appendToSlice(dest reflect.Value, s string) error {
|
||||
var ptr bool
|
||||
elem := dest.Type().Elem()
|
||||
if elem.Kind() == reflect.Ptr && !elem.Implements(textUnmarshalerType) {
|
||||
ptr = true
|
||||
elem = elem.Elem()
|
||||
}
|
||||
|
||||
// parse the value and append
|
||||
v := reflect.New(elem)
|
||||
if err := scalar.ParseValue(v.Elem(), s); err != nil {
|
||||
return err
|
||||
}
|
||||
if !ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
dest.Set(reflect.Append(dest, v))
|
||||
return nil
|
||||
}
|
||||
|
||||
// appendToMap parses a name=value string and inserts it into a map.
|
||||
// If clear is true then any values already in the map are removed.
|
||||
func appendToMap(dest reflect.Value, s string) error {
|
||||
// determine the key and value type
|
||||
var keyIsPtr bool
|
||||
keyType := dest.Type().Key()
|
||||
if keyType.Kind() == reflect.Ptr && !keyType.Implements(textUnmarshalerType) {
|
||||
keyIsPtr = true
|
||||
keyType = keyType.Elem()
|
||||
}
|
||||
|
||||
var valIsPtr bool
|
||||
valType := dest.Type().Elem()
|
||||
if valType.Kind() == reflect.Ptr && !valType.Implements(textUnmarshalerType) {
|
||||
valIsPtr = true
|
||||
valType = valType.Elem()
|
||||
}
|
||||
|
||||
// allocate the map if it is not allocated
|
||||
if dest.IsNil() {
|
||||
dest.Set(reflect.MakeMap(dest.Type()))
|
||||
}
|
||||
|
||||
// split at the first equals sign
|
||||
pos := strings.Index(s, "=")
|
||||
if pos == -1 {
|
||||
return fmt.Errorf("cannot parse %q into a map, expected format key=value", s)
|
||||
}
|
||||
|
||||
// parse the key
|
||||
k := reflect.New(keyType)
|
||||
if err := scalar.ParseValue(k.Elem(), s[:pos]); err != nil {
|
||||
return err
|
||||
}
|
||||
if !keyIsPtr {
|
||||
k = k.Elem()
|
||||
}
|
||||
|
||||
// parse the value
|
||||
v := reflect.New(valType)
|
||||
if err := scalar.ParseValue(v.Elem(), s[pos+1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if !valIsPtr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
// add it to the map
|
||||
dest.SetMapIndex(k, v)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAppendToSlice(t *testing.T) {
|
||||
xs := []int{10}
|
||||
err := appendToSlice(reflect.ValueOf(&xs).Elem(), "3")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{10, 3}, xs)
|
||||
}
|
||||
|
||||
func TestSetSlice(t *testing.T) {
|
||||
xs := []int{100}
|
||||
entries := []string{"1", "2", "3"}
|
||||
err := setSlice(reflect.ValueOf(&xs).Elem(), entries)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 2, 3}, xs)
|
||||
}
|
||||
|
||||
func TestSetSliceInvalid(t *testing.T) {
|
||||
xs := []int{100}
|
||||
entries := []string{"invalid"}
|
||||
err := setSlice(reflect.ValueOf(&xs).Elem(), entries)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetSlicePtr(t *testing.T) {
|
||||
var xs []*int
|
||||
entries := []string{"1", "2", "3"}
|
||||
err := setSlice(reflect.ValueOf(&xs).Elem(), entries)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, xs, 3)
|
||||
assert.Equal(t, 1, *xs[0])
|
||||
assert.Equal(t, 2, *xs[1])
|
||||
assert.Equal(t, 3, *xs[2])
|
||||
}
|
||||
|
||||
func TestSetSliceTextUnmarshaller(t *testing.T) {
|
||||
// textUnmarshaler is a struct that captures the length of the string passed to it
|
||||
var xs []*textUnmarshaler
|
||||
entries := []string{"a", "aa", "aaa"}
|
||||
err := setSlice(reflect.ValueOf(&xs).Elem(), entries)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, xs, 3)
|
||||
assert.Equal(t, 1, xs[0].val)
|
||||
assert.Equal(t, 2, xs[1].val)
|
||||
assert.Equal(t, 3, xs[2].val)
|
||||
}
|
||||
|
||||
func TestAppendToMap(t *testing.T) {
|
||||
m := map[string]int{"foo": 10}
|
||||
err := appendToMap(reflect.ValueOf(&m).Elem(), "a=1")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m, 2)
|
||||
assert.Equal(t, 1, m["a"])
|
||||
assert.Equal(t, 10, m["foo"])
|
||||
}
|
||||
|
||||
func TestSetMapAfterClearing(t *testing.T) {
|
||||
m := map[string]int{"foo": 10}
|
||||
entries := []string{"a=1", "b=2"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m, 2)
|
||||
assert.Equal(t, 1, m["a"])
|
||||
assert.Equal(t, 2, m["b"])
|
||||
}
|
||||
|
||||
func TestSetMapWithKeyPointer(t *testing.T) {
|
||||
// textUnmarshaler is a struct that captures the length of the string passed to it
|
||||
var m map[*string]int
|
||||
entries := []string{"abc=123"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m, 1)
|
||||
}
|
||||
|
||||
func TestSetMapWithValuePointer(t *testing.T) {
|
||||
// textUnmarshaler is a struct that captures the length of the string passed to it
|
||||
var m map[string]*int
|
||||
entries := []string{"abc=123"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m, 1)
|
||||
assert.Equal(t, 123, *m["abc"])
|
||||
}
|
||||
|
||||
func TestSetMapTextUnmarshaller(t *testing.T) {
|
||||
// textUnmarshaler is a struct that captures the length of the string passed to it
|
||||
var m map[textUnmarshaler]*textUnmarshaler
|
||||
entries := []string{"a=123", "aa=12", "aaa=1"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m, 3)
|
||||
assert.Equal(t, &textUnmarshaler{3}, m[textUnmarshaler{1}])
|
||||
assert.Equal(t, &textUnmarshaler{2}, m[textUnmarshaler{2}])
|
||||
assert.Equal(t, &textUnmarshaler{1}, m[textUnmarshaler{3}])
|
||||
}
|
||||
|
||||
func TestSetMapInvalidKey(t *testing.T) {
|
||||
var m map[int]int
|
||||
entries := []string{"invalid=123"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetMapInvalidValue(t *testing.T) {
|
||||
var m map[int]int
|
||||
entries := []string{"123=invalid"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetMapMalformed(t *testing.T) {
|
||||
// textUnmarshaler is a struct that captures the length of the string passed to it
|
||||
var m map[string]string
|
||||
entries := []string{"missing_equals_sign"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetSliceOrMapErrors(t *testing.T) {
|
||||
var err error
|
||||
var dest reflect.Value
|
||||
|
||||
// converting a slice to a reflect.Value in this way will make it read only
|
||||
var cannotSet []int
|
||||
dest = reflect.ValueOf(cannotSet)
|
||||
err = setSliceOrMap(dest, nil)
|
||||
assert.Error(t, err)
|
||||
|
||||
// check what happens when we pass in something that is not a slice or a map
|
||||
var notSliceOrMap string
|
||||
dest = reflect.ValueOf(¬SliceOrMap).Elem()
|
||||
err = setSliceOrMap(dest, nil)
|
||||
assert.Error(t, err)
|
||||
|
||||
// check what happens when we pass in a pointer to something that is not a slice or a map
|
||||
var stringPtr *string
|
||||
dest = reflect.ValueOf(&stringPtr).Elem()
|
||||
err = setSliceOrMap(dest, nil)
|
||||
assert.Error(t, err)
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package arg
|
||||
|
||||
// Subcommand returns the user struct for the subcommand selected by
|
||||
// the command line arguments most recently processed by the parser.
|
||||
// The return value is always a pointer to a struct. If no subcommand
|
||||
// was specified then it returns the top-level arguments struct. If
|
||||
// no command line arguments have been processed by this parser then it
|
||||
// returns nil.
|
||||
func (p *Parser) Subcommand() interface{} {
|
||||
if p.leaf == nil || p.leaf.parent == nil {
|
||||
return nil
|
||||
}
|
||||
return p.val(p.leaf.dest).Interface()
|
||||
}
|
||||
|
||||
// SubcommandNames returns the sequence of subcommands specified by the
|
||||
// user. If no subcommands were given then it returns an empty slice.
|
||||
func (p *Parser) SubcommandNames() []string {
|
||||
if p.leaf == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// make a list of ancestor commands
|
||||
var ancestors []string
|
||||
cur := p.leaf
|
||||
for cur.parent != nil { // we want to exclude the root
|
||||
ancestors = append(ancestors, cur.name)
|
||||
cur = cur.parent
|
||||
}
|
||||
|
||||
// reverse the list
|
||||
out := make([]string, len(ancestors))
|
||||
for i := 0; i < len(ancestors); i++ {
|
||||
out[i] = ancestors[len(ancestors)-i-1]
|
||||
}
|
||||
return out
|
||||
}
|
|
@ -0,0 +1,411 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// This file contains tests for parse.go but I decided to put them here
|
||||
// since that file is getting large
|
||||
|
||||
func TestSubcommandNotAPointer(t *testing.T) {
|
||||
var args struct {
|
||||
A string `arg:"subcommand"`
|
||||
}
|
||||
_, err := NewParser(&args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSubcommandNotAPointerToStruct(t *testing.T) {
|
||||
var args struct {
|
||||
A struct{} `arg:"subcommand"`
|
||||
}
|
||||
_, err := NewParser(&args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestPositionalAndSubcommandNotAllowed(t *testing.T) {
|
||||
var args struct {
|
||||
A string `arg:"positional"`
|
||||
B *struct{} `arg:"subcommand"`
|
||||
}
|
||||
_, err := NewParser(&args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMinimalSubcommand(t *testing.T) {
|
||||
type listCmd struct {
|
||||
}
|
||||
var args struct {
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
p, err := parse(&args, "list")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List, p.Subcommand())
|
||||
assert.Equal(t, []string{"list"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
func TestSubcommandNamesBeforeParsing(t *testing.T) {
|
||||
type listCmd struct{}
|
||||
var args struct {
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, p.Subcommand())
|
||||
assert.Nil(t, p.SubcommandNames())
|
||||
}
|
||||
|
||||
func TestNoSuchSubcommand(t *testing.T) {
|
||||
type listCmd struct {
|
||||
}
|
||||
var args struct {
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
_, err := parse(&args, "invalid")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNamedSubcommand(t *testing.T) {
|
||||
type listCmd struct {
|
||||
}
|
||||
var args struct {
|
||||
List *listCmd `arg:"subcommand:ls"`
|
||||
}
|
||||
p, err := parse(&args, "ls")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List, p.Subcommand())
|
||||
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
func TestEmptySubcommand(t *testing.T) {
|
||||
type listCmd struct {
|
||||
}
|
||||
var args struct {
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
p, err := parse(&args, "")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.List)
|
||||
assert.Nil(t, p.Subcommand())
|
||||
assert.Empty(t, p.SubcommandNames())
|
||||
}
|
||||
|
||||
func TestTwoSubcommands(t *testing.T) {
|
||||
type getCmd struct {
|
||||
}
|
||||
type listCmd struct {
|
||||
}
|
||||
var args struct {
|
||||
Get *getCmd `arg:"subcommand"`
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
p, err := parse(&args, "list")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List, p.Subcommand())
|
||||
assert.Equal(t, []string{"list"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
func TestSubcommandsWithOptions(t *testing.T) {
|
||||
type getCmd struct {
|
||||
Name string
|
||||
}
|
||||
type listCmd struct {
|
||||
Limit int
|
||||
}
|
||||
type cmd struct {
|
||||
Verbose bool
|
||||
Get *getCmd `arg:"subcommand"`
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list --limit 3")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List.Limit, 3)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list --limit 3 --verbose")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List.Limit, 3)
|
||||
assert.True(t, args.Verbose)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list --verbose --limit 3")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List.Limit, 3)
|
||||
assert.True(t, args.Verbose)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "--verbose list --limit 3")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List.Limit, 3)
|
||||
assert.True(t, args.Verbose)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "get")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Nil(t, args.List)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "get --name test")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Nil(t, args.List)
|
||||
assert.Equal(t, args.Get.Name, "test")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubcommandsWithEnvVars(t *testing.T) {
|
||||
type getCmd struct {
|
||||
Name string `arg:"env"`
|
||||
}
|
||||
type listCmd struct {
|
||||
Limit int `arg:"env"`
|
||||
}
|
||||
type cmd struct {
|
||||
Verbose bool
|
||||
Get *getCmd `arg:"subcommand"`
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list", "LIMIT=123")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.List)
|
||||
assert.Equal(t, 123, args.List.Limit)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list", "LIMIT=not_an_integer")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedSubcommands(t *testing.T) {
|
||||
type child struct{}
|
||||
type parent struct {
|
||||
Child *child `arg:"subcommand"`
|
||||
}
|
||||
type grandparent struct {
|
||||
Parent *parent `arg:"subcommand"`
|
||||
}
|
||||
type root struct {
|
||||
Grandparent *grandparent `arg:"subcommand"`
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := parse(&args, "grandparent parent child")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.Grandparent)
|
||||
require.NotNil(t, args.Grandparent.Parent)
|
||||
require.NotNil(t, args.Grandparent.Parent.Child)
|
||||
assert.Equal(t, args.Grandparent.Parent.Child, p.Subcommand())
|
||||
assert.Equal(t, []string{"grandparent", "parent", "child"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := parse(&args, "grandparent parent")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.Grandparent)
|
||||
require.NotNil(t, args.Grandparent.Parent)
|
||||
require.Nil(t, args.Grandparent.Parent.Child)
|
||||
assert.Equal(t, args.Grandparent.Parent, p.Subcommand())
|
||||
assert.Equal(t, []string{"grandparent", "parent"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := parse(&args, "grandparent")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.Grandparent)
|
||||
require.Nil(t, args.Grandparent.Parent)
|
||||
assert.Equal(t, args.Grandparent, p.Subcommand())
|
||||
assert.Equal(t, []string{"grandparent"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := parse(&args, "")
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, args.Grandparent)
|
||||
assert.Nil(t, p.Subcommand())
|
||||
assert.Empty(t, p.SubcommandNames())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubcommandsWithPositionals(t *testing.T) {
|
||||
type listCmd struct {
|
||||
Pattern string `arg:"positional"`
|
||||
}
|
||||
type cmd struct {
|
||||
Format string
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "", args.List.Pattern)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list --format json")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "", args.List.Pattern)
|
||||
assert.Equal(t, "json", args.Format)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list somepattern")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "somepattern", args.List.Pattern)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list somepattern --format json")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "somepattern", args.List.Pattern)
|
||||
assert.Equal(t, "json", args.Format)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "list --format json somepattern")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "somepattern", args.List.Pattern)
|
||||
assert.Equal(t, "json", args.Format)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "--format json list somepattern")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "somepattern", args.List.Pattern)
|
||||
assert.Equal(t, "json", args.Format)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "--format json")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.List)
|
||||
assert.Equal(t, "json", args.Format)
|
||||
}
|
||||
}
|
||||
func TestSubcommandsWithMultiplePositionals(t *testing.T) {
|
||||
type getCmd struct {
|
||||
Items []string `arg:"positional"`
|
||||
}
|
||||
type cmd struct {
|
||||
Limit int
|
||||
Get *getCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "get")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Empty(t, args.Get.Items)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "get --limit 5")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Empty(t, args.Get.Items)
|
||||
assert.Equal(t, 5, args.Limit)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "get item1")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Equal(t, []string{"item1"}, args.Get.Items)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "get item1 item2 item3")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Equal(t, []string{"item1", "item2", "item3"}, args.Get.Items)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
_, err := parse(&args, "get item1 --limit 5 item2")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Equal(t, []string{"item1", "item2"}, args.Get.Items)
|
||||
assert.Equal(t, 5, args.Limit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValForNilStruct(t *testing.T) {
|
||||
type subcmd struct{}
|
||||
var cmd struct {
|
||||
Sub *subcmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&cmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
typ := reflect.TypeOf(cmd)
|
||||
subField, _ := typ.FieldByName("Sub")
|
||||
|
||||
v := p.val(path{fields: []reflect.StructField{subField, subField}})
|
||||
assert.False(t, v.IsValid())
|
||||
}
|
|
@ -0,0 +1,331 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// the width of the left column
|
||||
const colWidth = 25
|
||||
|
||||
// to allow monkey patching in tests
|
||||
var (
|
||||
stdout io.Writer = os.Stdout
|
||||
stderr io.Writer = os.Stderr
|
||||
osExit = os.Exit
|
||||
)
|
||||
|
||||
// Fail prints usage information to stderr and exits with non-zero status
|
||||
func (p *Parser) Fail(msg string) {
|
||||
p.failWithSubcommand(msg, p.cmd)
|
||||
}
|
||||
|
||||
// FailSubcommand prints usage information for a specified subcommand to stderr,
|
||||
// then exits with non-zero status. To write usage information for a top-level
|
||||
// subcommand, provide just the name of that subcommand. To write usage
|
||||
// information for a subcommand that is nested under another subcommand, provide
|
||||
// a sequence of subcommand names starting with the top-level subcommand and so
|
||||
// on down the tree.
|
||||
func (p *Parser) FailSubcommand(msg string, subcommand ...string) error {
|
||||
cmd, err := p.lookupCommand(subcommand...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.failWithSubcommand(msg, cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
// failWithSubcommand prints usage information for the given subcommand to stderr and exits with non-zero status
|
||||
func (p *Parser) failWithSubcommand(msg string, cmd *Command) {
|
||||
p.writeUsageForSubcommand(stderr, cmd)
|
||||
fmt.Fprintln(stderr, "error:", msg)
|
||||
osExit(-1)
|
||||
}
|
||||
|
||||
// WriteUsage writes usage information for the top-level command
|
||||
func (p *Parser) WriteUsage(w io.Writer) {
|
||||
p.writeUsageForSubcommand(w, p.cmd)
|
||||
}
|
||||
|
||||
// WriteUsageForSubcommand writes the usage information for a specified
|
||||
// subcommand. To write usage information for a top-level subcommand, provide
|
||||
// just the name of that subcommand. To write usage information for a subcommand
|
||||
// that is nested under another subcommand, provide a sequence of subcommand
|
||||
// names starting with the top-level subcommand and so on down the tree.
|
||||
func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) error {
|
||||
cmd, err := p.lookupCommand(subcommand...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.writeUsageForSubcommand(w, cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeUsageForSubcommand writes usage information for the given subcommand
|
||||
func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *Command) {
|
||||
var positionals, longOptions, shortOptions []*Argument
|
||||
for _, arg := range cmd.args {
|
||||
switch {
|
||||
case arg.positional:
|
||||
positionals = append(positionals, arg)
|
||||
case arg.long != "":
|
||||
longOptions = append(longOptions, arg)
|
||||
case arg.short != "":
|
||||
shortOptions = append(shortOptions, arg)
|
||||
}
|
||||
}
|
||||
|
||||
if p.version != "" {
|
||||
fmt.Fprintln(w, p.version)
|
||||
}
|
||||
|
||||
// make a list of ancestor commands so that we print with full context
|
||||
var ancestors []string
|
||||
ancestor := cmd
|
||||
for ancestor != nil {
|
||||
ancestors = append(ancestors, ancestor.name)
|
||||
ancestor = ancestor.parent
|
||||
}
|
||||
|
||||
// print the beginning of the usage string
|
||||
fmt.Fprint(w, "Usage:")
|
||||
for i := len(ancestors) - 1; i >= 0; i-- {
|
||||
fmt.Fprint(w, " "+ancestors[i])
|
||||
}
|
||||
|
||||
// write the option component of the usage message
|
||||
for _, arg := range shortOptions {
|
||||
// prefix with a space
|
||||
fmt.Fprint(w, " ")
|
||||
if !arg.required {
|
||||
fmt.Fprint(w, "[")
|
||||
}
|
||||
fmt.Fprint(w, synopsis(arg, "-"+arg.short))
|
||||
if !arg.required {
|
||||
fmt.Fprint(w, "]")
|
||||
}
|
||||
}
|
||||
|
||||
for _, arg := range longOptions {
|
||||
// prefix with a space
|
||||
fmt.Fprint(w, " ")
|
||||
if !arg.required {
|
||||
fmt.Fprint(w, "[")
|
||||
}
|
||||
fmt.Fprint(w, synopsis(arg, "--"+arg.long))
|
||||
if !arg.required {
|
||||
fmt.Fprint(w, "]")
|
||||
}
|
||||
}
|
||||
|
||||
// When we parse positionals, we check that:
|
||||
// 1. required positionals come before non-required positionals
|
||||
// 2. there is at most one multiple-value positional
|
||||
// 3. if there is a multiple-value positional then it comes after all other positionals
|
||||
// Here we merely print the usage string, so we do not explicitly re-enforce those rules
|
||||
|
||||
// write the positionals in following form:
|
||||
// REQUIRED1 REQUIRED2
|
||||
// REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]
|
||||
// REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]
|
||||
// REQUIRED1 REQUIRED2 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]
|
||||
// REQUIRED1 REQUIRED2 [OPTIONAL1 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]]
|
||||
var closeBrackets int
|
||||
for _, arg := range positionals {
|
||||
fmt.Fprint(w, " ")
|
||||
if !arg.required {
|
||||
fmt.Fprint(w, "[")
|
||||
closeBrackets += 1
|
||||
}
|
||||
if arg.cardinality == multiple {
|
||||
fmt.Fprintf(w, "%s [%s ...]", arg.placeholder, arg.placeholder)
|
||||
} else {
|
||||
fmt.Fprint(w, arg.placeholder)
|
||||
}
|
||||
}
|
||||
fmt.Fprint(w, strings.Repeat("]", closeBrackets))
|
||||
|
||||
// if the program supports subcommands, give a hint to the user about their existence
|
||||
if len(cmd.subcommands) > 0 {
|
||||
fmt.Fprint(w, " <command> [<args>]")
|
||||
}
|
||||
|
||||
fmt.Fprint(w, "\n")
|
||||
}
|
||||
|
||||
func printTwoCols(w io.Writer, left, help string, defaultVal string, envVal string) {
|
||||
lhs := " " + left
|
||||
fmt.Fprint(w, lhs)
|
||||
if help != "" {
|
||||
if len(lhs)+2 < colWidth {
|
||||
fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs)))
|
||||
} else {
|
||||
fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
|
||||
}
|
||||
fmt.Fprint(w, help)
|
||||
}
|
||||
|
||||
bracketsContent := []string{}
|
||||
|
||||
if defaultVal != "" {
|
||||
bracketsContent = append(bracketsContent,
|
||||
fmt.Sprintf("default: %s", defaultVal),
|
||||
)
|
||||
}
|
||||
|
||||
if envVal != "" {
|
||||
bracketsContent = append(bracketsContent,
|
||||
fmt.Sprintf("env: %s", envVal),
|
||||
)
|
||||
}
|
||||
|
||||
if len(bracketsContent) > 0 {
|
||||
fmt.Fprintf(w, " [%s]", strings.Join(bracketsContent, ", "))
|
||||
}
|
||||
fmt.Fprint(w, "\n")
|
||||
}
|
||||
|
||||
// WriteHelp writes the usage string for the top-level command
|
||||
func (p *Parser) WriteHelp(w io.Writer) {
|
||||
p.writeHelpForSubcommand(w, p.cmd)
|
||||
}
|
||||
|
||||
// WriteHelpForSubcommand writes the usage string followed by the full help
|
||||
// string for a specified subcommand. To write help for a top-level subcommand,
|
||||
// provide just the name of that subcommand. To write help for a subcommand that
|
||||
// is nested under another subcommand, provide a sequence of subcommand names
|
||||
// starting with the top-level subcommand and so on down the tree.
|
||||
func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error {
|
||||
cmd, err := p.lookupCommand(subcommand...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.writeHelpForSubcommand(w, cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeHelp writes the usage string for the given subcommand
|
||||
func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *Command) {
|
||||
var positionals, longOptions, shortOptions []*Argument
|
||||
for _, arg := range cmd.args {
|
||||
switch {
|
||||
case arg.positional:
|
||||
positionals = append(positionals, arg)
|
||||
case arg.long != "":
|
||||
longOptions = append(longOptions, arg)
|
||||
case arg.short != "":
|
||||
shortOptions = append(shortOptions, arg)
|
||||
}
|
||||
}
|
||||
|
||||
if p.prologue != "" {
|
||||
fmt.Fprintln(w, p.prologue)
|
||||
}
|
||||
p.writeUsageForSubcommand(w, cmd)
|
||||
|
||||
// write the list of positionals
|
||||
if len(positionals) > 0 {
|
||||
fmt.Fprint(w, "\nPositional arguments:\n")
|
||||
for _, arg := range positionals {
|
||||
printTwoCols(w, arg.placeholder, arg.help, "", "")
|
||||
}
|
||||
}
|
||||
|
||||
// write the list of options with the short-only ones first to match the usage string
|
||||
if len(shortOptions)+len(longOptions) > 0 || cmd.parent == nil {
|
||||
fmt.Fprint(w, "\nOptions:\n")
|
||||
for _, arg := range shortOptions {
|
||||
p.printOption(w, arg)
|
||||
}
|
||||
for _, arg := range longOptions {
|
||||
p.printOption(w, arg)
|
||||
}
|
||||
}
|
||||
|
||||
// obtain a flattened list of options from all ancestors
|
||||
var globals []*Argument
|
||||
ancestor := cmd.parent
|
||||
for ancestor != nil {
|
||||
globals = append(globals, ancestor.args...)
|
||||
ancestor = ancestor.parent
|
||||
}
|
||||
|
||||
// write the list of global options
|
||||
if len(globals) > 0 {
|
||||
fmt.Fprint(w, "\nGlobal options:\n")
|
||||
for _, arg := range globals {
|
||||
p.printOption(w, arg)
|
||||
}
|
||||
}
|
||||
|
||||
// write the list of built in options
|
||||
p.printOption(w, &Argument{
|
||||
cardinality: zero,
|
||||
long: "help",
|
||||
short: "h",
|
||||
help: "display this help and exit",
|
||||
})
|
||||
if p.version != "" {
|
||||
p.printOption(w, &Argument{
|
||||
cardinality: zero,
|
||||
long: "version",
|
||||
help: "display version and exit",
|
||||
})
|
||||
}
|
||||
|
||||
// write the list of subcommands
|
||||
if len(cmd.subcommands) > 0 {
|
||||
fmt.Fprint(w, "\nCommands:\n")
|
||||
for _, subcmd := range cmd.subcommands {
|
||||
printTwoCols(w, subcmd.name, subcmd.help, "", "")
|
||||
}
|
||||
}
|
||||
|
||||
if p.epilogue != "" {
|
||||
fmt.Fprintln(w, "\n"+p.epilogue)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) printOption(w io.Writer, arg *Argument) {
|
||||
ways := make([]string, 0, 2)
|
||||
if arg.long != "" {
|
||||
ways = append(ways, synopsis(arg, "--"+arg.long))
|
||||
}
|
||||
if arg.short != "" {
|
||||
ways = append(ways, synopsis(arg, "-"+arg.short))
|
||||
}
|
||||
if len(ways) > 0 {
|
||||
printTwoCols(w, strings.Join(ways, ", "), arg.help, arg.defaultVal, arg.env)
|
||||
}
|
||||
}
|
||||
|
||||
// lookupCommand finds a subcommand based on a sequence of subcommand names. The
|
||||
// first string should be a top-level subcommand, the next should be a child
|
||||
// subcommand of that subcommand, and so on. If no strings are given then the
|
||||
// root command is returned. If no such subcommand exists then an error is
|
||||
// returned.
|
||||
func (p *Parser) lookupCommand(path ...string) (*Command, error) {
|
||||
cmd := p.cmd
|
||||
for _, name := range path {
|
||||
var found *Command
|
||||
for _, child := range cmd.subcommands {
|
||||
if child.name == name {
|
||||
found = child
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
|
||||
}
|
||||
cmd = found
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func synopsis(arg *Argument, form string) string {
|
||||
if arg.cardinality == zero {
|
||||
return form
|
||||
}
|
||||
return form + " " + arg.placeholder
|
||||
}
|
|
@ -0,0 +1,607 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type NameDotName struct {
|
||||
Head, Tail string
|
||||
}
|
||||
|
||||
func (n *NameDotName) UnmarshalText(b []byte) error {
|
||||
s := string(b)
|
||||
pos := strings.Index(s, ".")
|
||||
if pos == -1 {
|
||||
return fmt.Errorf("missing period in %s", s)
|
||||
}
|
||||
n.Head = s[:pos]
|
||||
n.Tail = s[pos+1:]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NameDotName) MarshalText() (text []byte, err error) {
|
||||
text = []byte(fmt.Sprintf("%s.%s", n.Head, n.Tail))
|
||||
return
|
||||
}
|
||||
|
||||
func TestWriteUsage(t *testing.T) {
|
||||
expectedUsage := "Usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] [--testenv TESTENV] [--file FILE] INPUT [OUTPUT [OUTPUT ...]]"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] [--testenv TESTENV] [--file FILE] INPUT [OUTPUT [OUTPUT ...]]
|
||||
|
||||
Positional arguments:
|
||||
INPUT
|
||||
OUTPUT list of outputs
|
||||
|
||||
Options:
|
||||
--name NAME name to use [default: Foo Bar]
|
||||
--value VALUE secret value [default: 42]
|
||||
--verbose, -v verbosity level
|
||||
--dataset DATASET dataset to use
|
||||
--optimize OPTIMIZE, -O OPTIMIZE
|
||||
optimization level
|
||||
--ids IDS Ids
|
||||
--values VALUES Values
|
||||
--workers WORKERS, -w WORKERS
|
||||
number of workers to start [default: 10, env: WORKERS]
|
||||
--testenv TESTENV, -a TESTENV [env: TEST_ENV]
|
||||
--file FILE, -f FILE File with mandatory extension
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional,required"`
|
||||
Output []string `arg:"positional" help:"list of outputs"`
|
||||
Name string `help:"name to use" default:"Foo Bar"`
|
||||
Value int `help:"secret value" default:"42"`
|
||||
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||
Dataset string `help:"dataset to use"`
|
||||
Optimize int `arg:"-O" help:"optimization level"`
|
||||
Ids []int64 `help:"Ids"`
|
||||
Values []float64 `help:"Values"`
|
||||
Workers int `arg:"-w,env:WORKERS" help:"number of workers to start" default:"10"`
|
||||
TestEnv string `arg:"-a,env:TEST_ENV"`
|
||||
File *NameDotName `arg:"-f" help:"File with mandatory extension"`
|
||||
}
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
os.Args[0] = "example"
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
type MyEnum int
|
||||
|
||||
func (n *MyEnum) UnmarshalText(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *MyEnum) MarshalText() ([]byte, error) {
|
||||
return nil, errors.New("There was a problem")
|
||||
}
|
||||
|
||||
func TestUsageWithDefaults(t *testing.T) {
|
||||
expectedUsage := "Usage: example [--label LABEL] [--content CONTENT]"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example [--label LABEL] [--content CONTENT]
|
||||
|
||||
Options:
|
||||
--label LABEL [default: cat]
|
||||
--content CONTENT [default: dog]
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
Label string `default:"cat"`
|
||||
Content string `default:"dog"`
|
||||
}
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
args.Label = "should_ignore_this"
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestUsageLongPositionalWithHelp_legacyForm(t *testing.T) {
|
||||
expectedUsage := "Usage: example [VERYLONGPOSITIONALWITHHELP]"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example [VERYLONGPOSITIONALWITHHELP]
|
||||
|
||||
Positional arguments:
|
||||
VERYLONGPOSITIONALWITHHELP
|
||||
this positional argument is very long but cannot include commas
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
VeryLongPositionalWithHelp string `arg:"positional,help:this positional argument is very long but cannot include commas"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestUsageLongPositionalWithHelp_newForm(t *testing.T) {
|
||||
expectedUsage := "Usage: example [VERYLONGPOSITIONALWITHHELP]"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example [VERYLONGPOSITIONALWITHHELP]
|
||||
|
||||
Positional arguments:
|
||||
VERYLONGPOSITIONALWITHHELP
|
||||
this positional argument is very long, and includes: commas, colons etc
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
VeryLongPositionalWithHelp string `arg:"positional" help:"this positional argument is very long, and includes: commas, colons etc"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestUsageWithProgramName(t *testing.T) {
|
||||
expectedUsage := "Usage: myprogram"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: myprogram
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
p, err := NewParser(&struct{}{}, WithProgramName("myprogram"))
|
||||
require.NoError(t, err)
|
||||
|
||||
os.Args[0] = "example"
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
type versioned struct{}
|
||||
|
||||
// Version returns the version for this program
|
||||
func (versioned) Version() string {
|
||||
return "example 3.2.1"
|
||||
}
|
||||
|
||||
func TestUsageWithVersion(t *testing.T) {
|
||||
expectedUsage := "example 3.2.1\nUsage: example"
|
||||
|
||||
expectedHelp := `
|
||||
example 3.2.1
|
||||
Usage: example
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
--version display version and exit
|
||||
`
|
||||
p, err := NewParser(&versioned{}, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
type described struct{}
|
||||
|
||||
// Described returns the description for this program
|
||||
func (described) Description() string {
|
||||
return "this program does this and that"
|
||||
}
|
||||
|
||||
func TestUsageWithDescription(t *testing.T) {
|
||||
expectedUsage := "Usage: example"
|
||||
|
||||
expectedHelp := `
|
||||
this program does this and that
|
||||
Usage: example
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
p, err := NewParser(&described{}, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
type epilogued struct{}
|
||||
|
||||
// Epilogued returns the epilogue for this program
|
||||
func (epilogued) Epilogue() string {
|
||||
return "For more information visit github.com/alexflint/go-arg"
|
||||
}
|
||||
|
||||
func TestUsageWithEpilogue(t *testing.T) {
|
||||
expectedUsage := "Usage: example"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
|
||||
For more information visit github.com/alexflint/go-arg
|
||||
`
|
||||
os.Args[0] = "example"
|
||||
p, err := NewParser(&epilogued{}, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestUsageForRequiredPositionals(t *testing.T) {
|
||||
expectedUsage := "Usage: example REQUIRED1 REQUIRED2\n"
|
||||
var args struct {
|
||||
Required1 string `arg:"positional,required"`
|
||||
Required2 string `arg:"positional,required"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, usage.String())
|
||||
}
|
||||
|
||||
func TestUsageForMixedPositionals(t *testing.T) {
|
||||
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]\n"
|
||||
var args struct {
|
||||
Required1 string `arg:"positional,required"`
|
||||
Required2 string `arg:"positional,required"`
|
||||
Optional1 string `arg:"positional"`
|
||||
Optional2 string `arg:"positional"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, usage.String())
|
||||
}
|
||||
|
||||
func TestUsageForRepeatedPositionals(t *testing.T) {
|
||||
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]\n"
|
||||
var args struct {
|
||||
Required1 string `arg:"positional,required"`
|
||||
Required2 string `arg:"positional,required"`
|
||||
Repeated []string `arg:"positional,required"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, usage.String())
|
||||
}
|
||||
|
||||
func TestUsageForMixedAndRepeatedPositionals(t *testing.T) {
|
||||
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2 [REPEATED [REPEATED ...]]]]\n"
|
||||
var args struct {
|
||||
Required1 string `arg:"positional,required"`
|
||||
Required2 string `arg:"positional,required"`
|
||||
Optional1 string `arg:"positional"`
|
||||
Optional2 string `arg:"positional"`
|
||||
Repeated []string `arg:"positional"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, usage.String())
|
||||
}
|
||||
|
||||
func TestRequiredMultiplePositionals(t *testing.T) {
|
||||
expectedUsage := "Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]\n"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]
|
||||
|
||||
Positional arguments:
|
||||
REQUIREDMULTIPLE required multiple positional
|
||||
|
||||
Options:
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
RequiredMultiple []string `arg:"positional,required" help:"required multiple positional"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, usage.String())
|
||||
}
|
||||
|
||||
func TestUsageWithNestedSubcommands(t *testing.T) {
|
||||
expectedUsage := "Usage: example child nested [--enable] OUTPUT"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example child nested [--enable] OUTPUT
|
||||
|
||||
Positional arguments:
|
||||
OUTPUT
|
||||
|
||||
Options:
|
||||
--enable
|
||||
|
||||
Global options:
|
||||
--values VALUES Values
|
||||
--verbose, -v verbosity level
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
|
||||
var args struct {
|
||||
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||
Child *struct {
|
||||
Values []float64 `help:"Values"`
|
||||
Nested *struct {
|
||||
Enable bool
|
||||
Output string `arg:"positional,required"`
|
||||
} `arg:"subcommand:nested"`
|
||||
} `arg:"subcommand:child"`
|
||||
}
|
||||
|
||||
os.Args[0] = "example"
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
_ = p.Parse([]string{"child", "nested", "value"}, nil)
|
||||
|
||||
var help2 bytes.Buffer
|
||||
p.WriteHelpForSubcommand(&help2, "child", "nested")
|
||||
assert.Equal(t, expectedHelp[1:], help2.String())
|
||||
|
||||
var usage2 bytes.Buffer
|
||||
p.WriteUsageForSubcommand(&usage2, "child", "nested")
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String()))
|
||||
}
|
||||
|
||||
func TestNonexistentSubcommand(t *testing.T) {
|
||||
var args struct {
|
||||
sub *struct{} `arg:"subcommand"`
|
||||
}
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
var b bytes.Buffer
|
||||
|
||||
err = p.WriteUsageForSubcommand(&b, "does_not_exist")
|
||||
assert.Error(t, err)
|
||||
|
||||
err = p.WriteHelpForSubcommand(&b, "does_not_exist")
|
||||
assert.Error(t, err)
|
||||
|
||||
err = p.FailSubcommand("something went wrong", "does_not_exist")
|
||||
assert.Error(t, err)
|
||||
|
||||
err = p.WriteUsageForSubcommand(&b, "sub", "does_not_exist")
|
||||
assert.Error(t, err)
|
||||
|
||||
err = p.WriteHelpForSubcommand(&b, "sub", "does_not_exist")
|
||||
assert.Error(t, err)
|
||||
|
||||
err = p.FailSubcommand("something went wrong", "sub", "does_not_exist")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUsageWithoutLongNames(t *testing.T) {
|
||||
expectedUsage := "Usage: example [-a PLACEHOLDER] -b SHORTONLY2"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example [-a PLACEHOLDER] -b SHORTONLY2
|
||||
|
||||
Options:
|
||||
-a PLACEHOLDER some help [default: some val]
|
||||
-b SHORTONLY2 some help2
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
ShortOnly string `arg:"-a,--" help:"some help" default:"some val" placeholder:"PLACEHOLDER"`
|
||||
ShortOnly2 string `arg:"-b,--,required" help:"some help2"`
|
||||
}
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestUsageWithShortFirst(t *testing.T) {
|
||||
expectedUsage := "Usage: example [-c CAT] [--dog DOG]"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example [-c CAT] [--dog DOG]
|
||||
|
||||
Options:
|
||||
-c CAT
|
||||
--dog DOG
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
Dog string
|
||||
Cat string `arg:"-c,--"`
|
||||
}
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestUsageWithEnvOptions(t *testing.T) {
|
||||
expectedUsage := "Usage: example [-s SHORT]"
|
||||
|
||||
expectedHelp := `
|
||||
Usage: example [-s SHORT]
|
||||
|
||||
Options:
|
||||
-s SHORT [env: SHORT]
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
Short string `arg:"--,-s,env"`
|
||||
EnvOnly string `arg:"--,env"`
|
||||
EnvOnlyOverriden string `arg:"--,env:CUSTOM"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp[1:], help.String())
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||
}
|
||||
|
||||
func TestFail(t *testing.T) {
|
||||
originalStderr := stderr
|
||||
originalExit := osExit
|
||||
defer func() {
|
||||
stderr = originalStderr
|
||||
osExit = originalExit
|
||||
}()
|
||||
|
||||
var b bytes.Buffer
|
||||
stderr = &b
|
||||
|
||||
var exitCode int
|
||||
osExit = func(code int) { exitCode = code }
|
||||
|
||||
expectedStdout := `
|
||||
Usage: example [--foo FOO]
|
||||
error: something went wrong
|
||||
`
|
||||
|
||||
var args struct {
|
||||
Foo int
|
||||
}
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
p.Fail("something went wrong")
|
||||
|
||||
assert.Equal(t, expectedStdout[1:], b.String())
|
||||
assert.Equal(t, -1, exitCode)
|
||||
}
|
||||
|
||||
func TestFailSubcommand(t *testing.T) {
|
||||
originalStderr := stderr
|
||||
originalExit := osExit
|
||||
defer func() {
|
||||
stderr = originalStderr
|
||||
osExit = originalExit
|
||||
}()
|
||||
|
||||
var b bytes.Buffer
|
||||
stderr = &b
|
||||
|
||||
var exitCode int
|
||||
osExit = func(code int) { exitCode = code }
|
||||
|
||||
expectedStdout := `
|
||||
Usage: example sub
|
||||
error: something went wrong
|
||||
`
|
||||
|
||||
var args struct {
|
||||
Sub *struct{} `arg:"subcommand"`
|
||||
}
|
||||
p, err := NewParser(&args, WithProgramName("example"))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.FailSubcommand("something went wrong", "sub")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expectedStdout[1:], b.String())
|
||||
assert.Equal(t, -1, exitCode)
|
||||
}
|
Loading…
Reference in New Issue