Compare commits

..

No commits in common. "main" and "coveralls_badge" have entirely different histories.

25 changed files with 821 additions and 6312 deletions

1
.github/FUNDING.yml vendored
View File

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

BIN
.github/banner.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -1,37 +0,0 @@
name: Go
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build_and_test:
name: Build and test
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go: ['1.20', '1.21', '1.22']
steps:
- id: go
name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
- name: Checkout
uses: actions/checkout@v4
- name: Build
run: go build -v .
- name: Test
run: go test -v -coverprofile=profile.cov .
- name: Send coverage
run: bash <(curl -s https://codecov.io/bash) -f profile.cov

2
.gitignore vendored
View File

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

9
.travis.yml Normal file
View File

@ -0,0 +1,9 @@
language: go
go:
- tip
before_install:
- go get github.com/axw/gocov/gocov
- go get github.com/mattn/goveralls
- if ! go get github.com/golang/tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi
script:
- $HOME/gopath/bin/goveralls -service=travis-ci

View File

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

727
README.md
View File

@ -1,20 +1,10 @@
<h1 align="center">
<img src="./.github/banner.jpg" alt="go-arg" height="250px">
<br>
go-arg
</br>
</h1>
<h4 align="center">Struct-based argument parsing for Go</h4>
<p align="center">
<a href="https://sourcegraph.com/github.com/alexflint/go-arg?badge"><img src="https://sourcegraph.com/github.com/alexflint/go-arg/-/badge.svg" alt="Sourcegraph"></a>
<a href="https://pkg.go.dev/github.com/alexflint/go-arg"><img src="https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square" alt="Documentation"></a>
<a href="https://github.com/alexflint/go-arg/actions"><img src="https://github.com/alexflint/go-arg/workflows/Go/badge.svg" alt="Build Status"></a>
<a href="https://codecov.io/gh/alexflint/go-arg"><img src="https://codecov.io/gh/alexflint/go-arg/branch/master/graph/badge.svg" alt="Coverage Status"></a>
<a href="https://goreportcard.com/report/github.com/alexflint/go-arg"><img src="https://goreportcard.com/badge/github.com/alexflint/go-arg" alt="Go Report Card"></a>
</p>
<br>
[![GoDoc](https://godoc.org/github.com/alexflint/go-arg?status.svg)](https://godoc.org/github.com/alexflint/go-arg)
[![Build Status](https://travis-ci.org/alexflint/go-arg.svg?branch=master)](https://travis-ci.org/alexflint/go-arg)
[![Coverage Status](https://coveralls.io/repos/alexflint/go-arg/badge.svg?branch=master&service=github)](https://coveralls.io/github/alexflint/go-arg?branch=master)
Declare command line arguments for your program by defining a struct.
## Structured argument parsing for Go
Declare the command line arguments your program accepts by defining a struct.
```go
var args struct {
@ -30,26 +20,20 @@ $ ./example --foo=hello --bar
hello true
```
### Installation
```shell
go get github.com/alexflint/go-arg
```
### Required arguments
```go
var args struct {
ID int `arg:"required"`
Timeout time.Duration
Foo string `arg:"required"`
Bar bool
}
arg.MustParse(&args)
```
```shell
$ ./example
Usage: example --id ID [--timeout TIMEOUT]
error: --id is required
usage: example --foo FOO [--bar]
error: --foo is required
```
### Positional arguments
@ -64,121 +48,33 @@ fmt.Println("Input:", args.Input)
fmt.Println("Output:", args.Output)
```
```shell
```
$ ./example src.txt x.out y.out z.out
Input: src.txt
Output: [x.out y.out z.out]
```
### Environment variables
```go
var args struct {
Workers int `arg:"env"`
}
arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```shell
$ WORKERS=4 ./example
Workers: 4
```
```shell
$ 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)
```
```shell
$ NUM_WORKERS=4 ./example
Workers: 4
```
You can provide multiple values in environment variables using commas:
```go
var args struct {
Workers []int `arg:"env"`
}
arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```shell
$ WORKERS='1,99' ./example
Workers: [1 99]
```
Command line arguments take precedence over environment variables:
```go
var args struct {
Workers int `arg:"--count,env:NUM_WORKERS"`
}
arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```shell
$ NUM_WORKERS=6 ./example
Workers: 6
$ NUM_WORKERS=6 ./example --count 4
Workers: 4
```
Configuring a global environment variable name prefix is also possible:
```go
var args struct {
Workers int `arg:"--count,env:NUM_WORKERS"`
}
p, err := arg.NewParser(arg.Config{
EnvPrefix: "MYAPP_",
}, &args)
p.MustParse(os.Args[1:])
fmt.Println("Workers:", args.Workers)
```
```shell
$ MYAPP_NUM_WORKERS=6 ./example
Workers: 6
```
### Usage strings
```go
var args struct {
Input string `arg:"positional"`
Output []string `arg:"positional"`
Verbose bool `arg:"-v,--verbose" help:"verbosity level"`
Dataset string `help:"dataset to use"`
Optimize int `arg:"-O" help:"optimization level"`
Verbose bool `arg:"-v,help:verbosity level"`
Dataset string `arg:"help:dataset to use"`
Optimize int `arg:"-O,help:optimization level"`
}
arg.MustParse(&args)
```
```shell
$ ./example -h
Usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]]
usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]]
Positional arguments:
INPUT
OUTPUT
positional arguments:
input
output
Options:
options:
--verbose, -v verbosity level
--dataset DATASET dataset to use
--optimize OPTIMIZE, -O OPTIMIZE
@ -190,38 +86,14 @@ Options:
```go
var args struct {
Foo string `default:"abc"`
Foo string
Bar bool
}
args.Foo = "default value"
arg.MustParse(&args)
```
Command line arguments take precedence over environment variables, which take precedence over default values. This means that we check whether a certain option was provided on the command line, then if not, we check for an environment variable (only if an `env` tag was provided), then if none is found, we check for a `default` tag containing a default value.
```go
var args struct {
Test string `arg:"-t,env:TEST" default:"something"`
}
arg.MustParse(&args)
```
#### 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[1:])
```
### Arguments with multiple values
```go
var args struct {
Database string
@ -236,565 +108,22 @@ 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)
```
### Installation
```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]
go get github.com/alexflint/go-arg
```
### Arguments with keys and values
### Documentation
```go
var args struct {
UserIDs map[string]int
}
arg.MustParse(&args)
fmt.Println(args.UserIDs)
```
```shell
./example --userids john=123 mary=456
map[john:123 mary:456]
```
### Version strings
```go
type args struct {
...
}
func (args) Version() string {
return "someprogram 4.3.0"
}
func main() {
var args args
arg.MustParse(&args)
}
```
```shell
$ ./example --version
someprogram 4.3.0
```
> **Note**
> If a `--version` flag is defined in `args` or any subcommand, it overrides the built-in versioning.
### Custom validation
```go
var args struct {
Foo string
Bar string
}
p := arg.MustParse(&args)
if args.Foo == "" && args.Bar == "" {
p.Fail("you must provide either --foo or --bar")
}
```
```shell
./example
Usage: samples [--foo FOO] [--bar BAR]
error: you must provide either --foo or --bar
```
### Overriding option names
```go
var args struct {
Short string `arg:"-s"`
Long string `arg:"--custom-long-option"`
ShortAndLong string `arg:"-x,--my-option"`
OnlyShort string `arg:"-o,--"`
}
arg.MustParse(&args)
```
```shell
$ ./example --help
Usage: example [-o ONLYSHORT] [--short SHORT] [--custom-long-option CUSTOM-LONG-OPTION] [--my-option MY-OPTION]
Options:
--short SHORT, -s SHORT
--custom-long-option CUSTOM-LONG-OPTION
--my-option MY-OPTION, -x MY-OPTION
-o ONLYSHORT
--help, -h display this help and exit
```
### Embedded structs
The fields of embedded structs are treated just like regular fields:
```go
type DatabaseOptions struct {
Host string
Username string
Password string
}
type LogOptions struct {
LogFile string
Verbose bool
}
func main() {
var args struct {
DatabaseOptions
LogOptions
}
arg.MustParse(&args)
}
```
As usual, any field tagged with `arg:"-"` is ignored.
### Supported types
The following types may be used as arguments:
- built-in integer types: `int, int8, int16, int32, int64, byte, rune`
- built-in floating point types: `float32, float64`
- strings
- booleans
- URLs represented as `url.URL`
- time durations represented as `time.Duration`
- email addresses represented as `mail.Address`
- MAC addresses represented as `net.HardwareAddr`
- pointers to any of the above
- slices of any of the above
- maps using any of the above as keys and values
- any type that implements `encoding.TextUnmarshaler`
### Custom parsing
Implement `encoding.TextUnmarshaler` to define your own parsing logic.
```go
// Accepts command line arguments of the form "head.tail"
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 main() {
var args struct {
Name NameDotName
}
arg.MustParse(&args)
fmt.Printf("%#v\n", args.Name)
}
```
```shell
$ ./example --name=foo.bar
main.NameDotName{Head:"foo", Tail:"bar"}
$ ./example --name=oops
Usage: example [--name NAME]
error: error processing --name: missing period in "oops"
```
### Custom parsing with default values
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
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" help:"optimization level" placeholder:"LEVEL"`
MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" 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
optimization level
--maxjobs N, -j N maximum number of simultaneous jobs
--help, -h display this help and exit
```
### Description strings
A descriptive message can be added at the top of the help text by implementing
a `Description` function that returns a string.
```go
type args struct {
Foo string
}
func (args) Description() string {
return "this program does this and that"
}
func main() {
var args args
arg.MustParse(&args)
}
```
```shell
$ ./example -h
this program does this and that
Usage: example [--foo FOO]
Options:
--foo FOO
--help, -h display this help and exit
```
Similarly an epilogue can be added at the end of the help text by implementing
the `Epilogue` function.
```go
type args struct {
Foo string
}
func (args) Epilogue() string {
return "For more information visit github.com/alexflint/go-arg"
}
func main() {
var args args
arg.MustParse(&args)
}
```
```shell
$ ./example -h
Usage: example [--foo FOO]
Options:
--foo FOO
--help, -h display this help and exit
For more information visit github.com/alexflint/go-arg
```
### Subcommands
Subcommands are commonly used in tools that wish to group multiple functions into a single program. An example is the `git` tool:
```shell
$ git checkout [arguments specific to checking out code]
$ git commit [arguments specific to committing]
$ git push [arguments specific to pushing]
```
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:
```go
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
}
arg.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)
}
```
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
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:
```go
p := arg.MustParse(&args)
if p.Subcommand() == nil {
p.Fail("missing subcommand")
}
```
### Custom handling of --help and --version
The following reproduces the internal logic of `MustParse` for the simple case where
you are not using subcommands or --version. This allows you to respond
programatically to --help, and to any errors that come up.
```go
var args struct {
Something string
}
p, err := arg.NewParser(arg.Config{}, &args)
if err != nil {
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
}
err = p.Parse(os.Args[1:])
switch {
case err == arg.ErrHelp: // indicates that user wrote "--help" on command line
p.WriteHelp(os.Stdout)
os.Exit(0)
case err != nil:
fmt.Printf("error: %v\n", err)
p.WriteUsage(os.Stdout)
os.Exit(1)
}
```
```shell
$ go run ./example --help
Usage: ./example --something SOMETHING
Options:
--something SOMETHING
--help, -h display this help and exit
$ ./example --wrong
error: unknown argument --wrong
Usage: ./example --something SOMETHING
$ ./example
error: --something is required
Usage: ./example --something SOMETHING
```
To also handle --version programatically, use the following:
```go
type args struct {
Something string
}
func (args) Version() string {
return "1.2.3"
}
func main() {
var args args
p, err := arg.NewParser(arg.Config{}, &args)
if err != nil {
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
}
err = p.Parse(os.Args[1:])
switch {
case err == arg.ErrHelp: // found "--help" on command line
p.WriteHelp(os.Stdout)
os.Exit(0)
case err == arg.ErrVersion: // found "--version" on command line
fmt.Println(args.Version())
os.Exit(0)
case err != nil:
fmt.Printf("error: %v\n", err)
p.WriteUsage(os.Stdout)
os.Exit(1)
}
fmt.Printf("got %q\n", args.Something)
}
```
```shell
$ ./example --version
1.2.3
$ go run ./example --help
1.2.3
Usage: example --something SOMETHING
Options:
--something SOMETHING
--help, -h display this help and exit
$ ./example --wrong
1.2.3
error: unknown argument --wrong
Usage: example --something SOMETHING
$ ./example
error: --something is required
Usage: example --something SOMETHING
```
To generate subcommand-specific help messages, use the following most general version
(this also works in absence of subcommands but is a bit more complex):
```go
type fetchCmd struct {
Count int
}
type args struct {
Something string
Fetch *fetchCmd `arg:"subcommand"`
}
func (args) Version() string {
return "1.2.3"
}
func main() {
var args args
p, err := arg.NewParser(arg.Config{}, &args)
if err != nil {
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
}
err = p.Parse(os.Args[1:])
switch {
case err == arg.ErrHelp: // found "--help" on command line
p.WriteHelpForSubcommand(os.Stdout, p.SubcommandNames()...)
os.Exit(0)
case err == arg.ErrVersion: // found "--version" on command line
fmt.Println(args.Version())
os.Exit(0)
case err != nil:
fmt.Printf("error: %v\n", err)
p.WriteUsageForSubcommand(os.Stdout, p.SubcommandNames()...)
os.Exit(1)
}
}
```
```shell
$ ./example --version
1.2.3
$ ./example --help
1.2.3
Usage: example [--something SOMETHING] <command> [<args>]
Options:
--something SOMETHING
--help, -h display this help and exit
--version display version and exit
Commands:
fetch
$ ./example fetch --help
1.2.3
Usage: example fetch [--count COUNT]
Options:
--count COUNT
Global options:
--something SOMETHING
--help, -h display this help and exit
--version display version and exit
```
### API Documentation
https://pkg.go.dev/github.com/alexflint/go-arg
https://godoc.org/github.com/alexflint/go-arg
### Rationale
There are many command line argument parsing libraries for Go, including one in the standard library, so why build another?
The `flag` library that ships in the standard library seems awkward to me. Positional arguments must precede options, so `./prog x --foo=1` does what you expect but `./prog --foo=1 x` does not. It also does not allow arguments to have both long (`--foo`) and short (`-f`) forms.
The shortcomings of the `flag` library that ships in the standard library are well known. Positional arguments must preceed options, so `./prog x --foo=1` does what you expect but `./prog --foo=1 x` does not. Arguments cannot 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.
Many third-party argument parsing libraries are geared for writing sophisticated command line interfaces. The excellent `codegangsta/cli` is perfect for working with multiple sub-commands and nested flags, but is probably overkill for a simple script with a handful of 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 makes it possible to include commas inside help text.
The main idea behind `go-arg` is that Go already has an excellent way to describe data structures using Go structs, so there is no need to develop more levels of abstraction on top of this. Instead of one API to specify which arguments your program accepts, and then another API to get the values of those arguments, why not replace both with a single struct?

39
doc.go
View File

@ -1,39 +0,0 @@
// 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

87
example_test.go Normal file
View File

@ -0,0 +1,87 @@
package arg
import (
"fmt"
"os"
)
// This example demonstrates basic usage
func Example_Basic() {
// These are the args you would pass in on the command line
os.Args = []string{"./example", "--foo=hello", "--bar"}
var args struct {
Foo string
Bar bool
}
MustParse(&args)
fmt.Println(args.Foo, args.Bar)
}
// 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 = []string{"--help"}
var args struct {
Foo string
Bar bool
}
args.Foo = "default value"
MustParse(&args)
fmt.Println(args.Foo, args.Bar)
}
// This example demonstrates arguments that are required
func Example_RequiredArguments() {
// These are the args you would pass in on the command line
os.Args = []string{"--foo=1", "--bar"}
var args struct {
Foo string `arg:"required"`
Bar bool
}
MustParse(&args)
}
// This example demonstrates positional arguments
func Example_PositionalArguments() {
// These are the args you would pass in on the command line
os.Args = []string{"./example", "in", "out1", "out2", "out3"}
var args struct {
Input string `arg:"positional"`
Output []string `arg:"positional"`
}
MustParse(&args)
fmt.Println("Input:", args.Input)
fmt.Println("Output:", args.Output)
}
// This example demonstrates arguments that have multiple values
func Example_MultipleValues() {
// The args you would pass in on the command line
os.Args = []string{"--help"}
var args struct {
Database string
IDs []int64
}
MustParse(&args)
fmt.Printf("Fetching the following IDs from %s: %q", args.Database, args.IDs)
}
// This example shows the usage string generated by go-arg
func Example_UsageString() {
// These are the args you would pass in on the command line
os.Args = []string{"--help"}
var args struct {
Input string `arg:"positional"`
Output []string `arg:"positional"`
Verbose bool `arg:"-v,help:verbosity level"`
Dataset string `arg:"help:dataset to use"`
Optimize int `arg:"-O,help:optimization level"`
}
MustParse(&args)
}

980
parse.go

File diff suppressed because it is too large Load Diff

358
parse_test.go Normal file
View File

@ -0,0 +1,358 @@
package arg
import (
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func parse(cmdline string, dest interface{}) error {
p, err := NewParser(dest)
if err != nil {
return err
}
return p.Parse(strings.Split(cmdline, " "))
}
func TestStringSingle(t *testing.T) {
var args struct {
Foo string
}
err := parse("--foo bar", &args)
require.NoError(t, err)
assert.Equal(t, "bar", args.Foo)
}
func TestMixed(t *testing.T) {
var args struct {
Foo string `arg:"-f"`
Bar int
Baz uint `arg:"positional"`
Ham bool
Spam float32
}
args.Bar = 3
err := parse("123 -spam=1.2 -ham -f xyz", &args)
require.NoError(t, err)
assert.Equal(t, "xyz", args.Foo)
assert.Equal(t, 3, args.Bar)
assert.Equal(t, uint(123), args.Baz)
assert.Equal(t, true, args.Ham)
assert.EqualValues(t, 1.2, args.Spam)
}
func TestRequired(t *testing.T) {
var args struct {
Foo string `arg:"required"`
}
err := parse("", &args)
require.Error(t, err, "--foo is required")
}
func TestShortFlag(t *testing.T) {
var args struct {
Foo string `arg:"-f"`
}
err := parse("-f xyz", &args)
require.NoError(t, err)
assert.Equal(t, "xyz", args.Foo)
err = parse("-foo xyz", &args)
require.NoError(t, err)
assert.Equal(t, "xyz", args.Foo)
err = parse("--foo xyz", &args)
require.NoError(t, err)
assert.Equal(t, "xyz", args.Foo)
}
func TestInvalidShortFlag(t *testing.T) {
var args struct {
Foo string `arg:"-foo"`
}
err := parse("", &args)
assert.Error(t, err)
}
func TestLongFlag(t *testing.T) {
var args struct {
Foo string `arg:"--abc"`
}
err := parse("-abc xyz", &args)
require.NoError(t, err)
assert.Equal(t, "xyz", args.Foo)
err = parse("--abc xyz", &args)
require.NoError(t, err)
assert.Equal(t, "xyz", args.Foo)
}
func TestCaseSensitive(t *testing.T) {
var args struct {
Lower bool `arg:"-v"`
Upper bool `arg:"-V"`
}
err := parse("-v", &args)
require.NoError(t, err)
assert.True(t, args.Lower)
assert.False(t, args.Upper)
}
func TestCaseSensitive2(t *testing.T) {
var args struct {
Lower bool `arg:"-v"`
Upper bool `arg:"-V"`
}
err := parse("-V", &args)
require.NoError(t, err)
assert.False(t, args.Lower)
assert.True(t, args.Upper)
}
func TestPositional(t *testing.T) {
var args struct {
Input string `arg:"positional"`
Output string `arg:"positional"`
}
err := parse("foo", &args)
require.NoError(t, err)
assert.Equal(t, "foo", args.Input)
assert.Equal(t, "", args.Output)
}
func TestPositionalPointer(t *testing.T) {
var args struct {
Input string `arg:"positional"`
Output []*string `arg:"positional"`
}
err := parse("foo bar baz", &args)
require.NoError(t, err)
assert.Equal(t, "foo", args.Input)
bar := "bar"
baz := "baz"
assert.Equal(t, []*string{&bar, &baz}, args.Output)
}
func TestRequiredPositional(t *testing.T) {
var args struct {
Input string `arg:"positional"`
Output string `arg:"positional,required"`
}
err := parse("foo", &args)
assert.Error(t, err)
}
func TestTooManyPositional(t *testing.T) {
var args struct {
Input string `arg:"positional"`
Output string `arg:"positional"`
}
err := parse("foo bar baz", &args)
assert.Error(t, err)
}
func TestMultiple(t *testing.T) {
var args struct {
Foo []int
Bar []string
}
err := parse("--foo 1 2 3 --bar x y z", &args)
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, args.Foo)
assert.Equal(t, []string{"x", "y", "z"}, args.Bar)
}
func TestMultipleWithEq(t *testing.T) {
var args struct {
Foo []int
Bar []string
}
err := parse("--foo 1 2 3 --bar=x", &args)
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, args.Foo)
assert.Equal(t, []string{"x"}, args.Bar)
}
func TestExemptField(t *testing.T) {
var args struct {
Foo string
Bar interface{} `arg:"-"`
}
err := parse("--foo xyz", &args)
require.NoError(t, err)
assert.Equal(t, "xyz", args.Foo)
}
func TestUnknownField(t *testing.T) {
var args struct {
Foo string
}
err := parse("--bar xyz", &args)
assert.Error(t, err)
}
func TestMissingRequired(t *testing.T) {
var args struct {
Foo string `arg:"required"`
X []string `arg:"positional"`
}
err := parse("x", &args)
assert.Error(t, err)
}
func TestMissingValue(t *testing.T) {
var args struct {
Foo string
}
err := parse("--foo", &args)
assert.Error(t, err)
}
func TestInvalidInt(t *testing.T) {
var args struct {
Foo int
}
err := parse("--foo=xyz", &args)
assert.Error(t, err)
}
func TestInvalidUint(t *testing.T) {
var args struct {
Foo uint
}
err := parse("--foo=xyz", &args)
assert.Error(t, err)
}
func TestInvalidFloat(t *testing.T) {
var args struct {
Foo float64
}
err := parse("--foo xyz", &args)
require.Error(t, err)
}
func TestInvalidBool(t *testing.T) {
var args struct {
Foo bool
}
err := parse("--foo=xyz", &args)
require.Error(t, err)
}
func TestInvalidIntSlice(t *testing.T) {
var args struct {
Foo []int
}
err := parse("--foo 1 2 xyz", &args)
require.Error(t, err)
}
func TestInvalidPositional(t *testing.T) {
var args struct {
Foo int `arg:"positional"`
}
err := parse("xyz", &args)
require.Error(t, err)
}
func TestInvalidPositionalSlice(t *testing.T) {
var args struct {
Foo []int `arg:"positional"`
}
err := parse("1 2 xyz", &args)
require.Error(t, err)
}
func TestNoMoreOptions(t *testing.T) {
var args struct {
Foo string
Bar []string `arg:"positional"`
}
err := parse("abc -- --foo xyz", &args)
require.NoError(t, err)
assert.Equal(t, "", args.Foo)
assert.Equal(t, []string{"abc", "--foo", "xyz"}, args.Bar)
}
func TestHelpFlag(t *testing.T) {
var args struct {
Foo string
Bar interface{} `arg:"-"`
}
err := parse("--help", &args)
assert.Equal(t, ErrHelp, err)
}
func TestPanicOnNonPointer(t *testing.T) {
var args struct{}
assert.Panics(t, func() {
parse("", args)
})
}
func TestPanicOnNonStruct(t *testing.T) {
var args string
assert.Panics(t, func() {
parse("", &args)
})
}
func TestUnsupportedType(t *testing.T) {
var args struct {
Foo interface{}
}
err := parse("--foo", &args)
assert.Error(t, err)
}
func TestUnsupportedSliceElement(t *testing.T) {
var args struct {
Foo []interface{}
}
err := parse("--foo", &args)
assert.Error(t, err)
}
func TestUnknownTag(t *testing.T) {
var args struct {
Foo string `arg:"this_is_not_valid"`
}
err := parse("--foo xyz", &args)
assert.Error(t, err)
}
func TestParse(t *testing.T) {
var args struct {
Foo string
}
os.Args = []string{"example", "--foo", "bar"}
err := Parse(&args)
require.NoError(t, err)
assert.Equal(t, "bar", args.Foo)
}
func TestParseError(t *testing.T) {
var args struct {
Foo string `arg:"this_is_not_valid"`
}
os.Args = []string{"example", "--bar"}
err := Parse(&args)
assert.Error(t, err)
}
func TestMustParse(t *testing.T) {
var args struct {
Foo string
}
os.Args = []string{"example", "--foo", "bar"}
MustParse(&args)
assert.Equal(t, "bar", args.Foo)
}

View File

@ -1,112 +0,0 @@
package arg
import (
"encoding"
"fmt"
"reflect"
"unicode"
"unicode/utf8"
"go.wit.com/dev/alexflint/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 is a boolean or a pointer to a boolean
func isBoolean(t reflect.Type) bool {
switch {
case isTextUnmarshaler(t):
return false
case t.Kind() == reflect.Bool:
return true
case t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Bool:
return true
default:
return false
}
}
// isTextUnmarshaler returns true if the type or its pointer implements encoding.TextUnmarshaler
func isTextUnmarshaler(t reflect.Type) bool {
return t.Implements(textUnmarshalerType) || reflect.PtrTo(t).Implements(textUnmarshalerType)
}
// isExported returns true if the struct field name is exported
func isExported(field string) bool {
r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8
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.Ptr || t.Kind() == reflect.Slice || t.Kind() == reflect.Map || t.Kind() == reflect.Chan || t.Kind() == reflect.Interface {
return v.IsNil()
}
if !t.Comparable() {
return false
}
return v.Interface() == reflect.Zero(t).Interface()
}

View File

@ -1,123 +0,0 @@
package arg
import (
"fmt"
"reflect"
"strings"
"go.wit.com/dev/alexflint/scalar"
)
// setSliceOrMap parses a sequence of strings into a slice or map. If clear is
// true then any values already in the slice or map are first removed.
func setSliceOrMap(dest reflect.Value, values []string, clear bool) 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, clear)
case reflect.Map:
return setMap(dest, values, clear)
default:
return fmt.Errorf("setSliceOrMap cannot insert values into a %v", t)
}
}
// setSlice parses a sequence of strings and inserts them into a slice. If clear
// is true then any values already in the slice are removed.
func setSlice(dest reflect.Value, values []string, clear bool) 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 clear && !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.
// If clear is true then any values already in the map are removed.
func setMap(dest reflect.Value, values []string, clear bool) 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 clear && !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
}

View File

@ -1,43 +0,0 @@
package arg
import "fmt"
// Subcommand returns the user struct for the subcommand selected by
// the command line arguments most recently processed by the parser.
// The return value is always a pointer to a struct. If no subcommand
// 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 len(p.subcommand) == 0 {
return nil
}
cmd, err := p.lookupCommand(p.subcommand...)
if err != nil {
return nil
}
return p.val(cmd.dest).Interface()
}
// SubcommandNames returns the sequence of subcommands specified by the
// user. If no subcommands were given then it returns an empty slice.
func (p *Parser) SubcommandNames() []string {
return p.subcommand
}
// lookupCommand finds a subcommand based on a sequence of subcommand names. The
// first string should be a top-level subcommand, the next should be a child
// subcommand of that subcommand, and so on. If no strings are given then the
// root command is returned. If no such subcommand exists then an error is
// returned.
func (p *Parser) lookupCommand(path ...string) (*command, error) {
cmd := p.cmd
for _, name := range path {
found := findSubcommand(cmd.subcommands, name)
if found == nil {
return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
}
cmd = found
}
return cmd, nil
}

View File

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

View File

@ -1,546 +0,0 @@
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
mustParseExit = func(int) {}
mustParseOut = 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
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
// output:
// Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
//
// Positional arguments:
// SRC
// DST
//
// Options:
// --optimize LEVEL, -O LEVEL
// optimization level
// --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
mustParseExit = func(int) {}
mustParseOut = 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
mustParseExit = func(int) {}
mustParseOut = 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
exit := func(int) {}
p, err := NewParser(Config{Exit: exit}, &args)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = p.WriteHelpForSubcommand(os.Stdout, "list")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// 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
exit := func(int) {}
p, err := NewParser(Config{Exit: exit}, &args)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = p.WriteHelpForSubcommand(os.Stdout, "toplevel", "nested", "mostnested")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// output:
// Usage: example toplevel nested mostnested [--item ITEM]
//
// Options:
// --item ITEM
// --help, -h display this help and exit
}
// This example shows the error string generated by go-arg when an invalid option is provided
func Example_errorText() {
// These are the args you would pass in on the command line
os.Args = split("./example --optimize INVALID")
var args struct {
Input string `arg:"positional,required"`
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
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
// output:
// Usage: example [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]]
// error: error processing --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
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
// output:
// Usage: example get [--count COUNT]
// error: error processing --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
mustParseExit = func(int) {}
mustParseOut = 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:
}
func Example_envVarOnly() {
os.Args = split("./example")
_ = os.Setenv("AUTH_KEY", "my_key")
defer os.Unsetenv("AUTH_KEY")
var args struct {
AuthKey string `arg:"--,env:AUTH_KEY"`
}
MustParse(&args)
fmt.Println(args.AuthKey)
// output: my_key
}
func Example_envVarOnlyShouldIgnoreFlag() {
os.Args = split("./example --=my_key")
var args struct {
AuthKey string `arg:"--,env:AUTH_KEY"`
}
err := Parse(&args)
fmt.Println(err)
// output: unknown argument --=my_key
}
func Example_envVarOnlyShouldIgnoreShortFlag() {
os.Args = split("./example -=my_key")
var args struct {
AuthKey string `arg:"--,env:AUTH_KEY"`
}
err := Parse(&args)
fmt.Println(err)
// output: unknown argument -=my_key
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,112 +0,0 @@
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)))
}

View File

@ -1,152 +0,0 @@
package arg
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSetSliceWithoutClearing(t *testing.T) {
xs := []int{10}
entries := []string{"1", "2", "3"}
err := setSlice(reflect.ValueOf(&xs).Elem(), entries, false)
require.NoError(t, err)
assert.Equal(t, []int{10, 1, 2, 3}, xs)
}
func TestSetSliceAfterClearing(t *testing.T) {
xs := []int{100}
entries := []string{"1", "2", "3"}
err := setSlice(reflect.ValueOf(&xs).Elem(), entries, true)
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, true)
assert.Error(t, err)
}
func TestSetSlicePtr(t *testing.T) {
var xs []*int
entries := []string{"1", "2", "3"}
err := setSlice(reflect.ValueOf(&xs).Elem(), entries, true)
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, true)
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 TestSetMapWithoutClearing(t *testing.T) {
m := map[string]int{"foo": 10}
entries := []string{"a=1", "b=2"}
err := setMap(reflect.ValueOf(&m).Elem(), entries, false)
require.NoError(t, err)
require.Len(t, m, 3)
assert.Equal(t, 1, m["a"])
assert.Equal(t, 2, m["b"])
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, true)
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, true)
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, true)
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, true)
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, true)
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, true)
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, true)
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, false)
assert.Error(t, err)
// check what happens when we pass in something that is not a slice or a map
var notSliceOrMap string
dest = reflect.ValueOf(&notSliceOrMap).Elem()
err = setSliceOrMap(dest, nil, false)
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, false)
assert.Error(t, err)
}

View File

@ -1,508 +0,0 @@
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(Config{}, &args)
assert.Error(t, err)
}
func TestSubcommandNotAPointerToStruct(t *testing.T) {
var args struct {
A struct{} `arg:"subcommand"`
}
_, err := NewParser(Config{}, &args)
assert.Error(t, err)
}
func TestPositionalAndSubcommandNotAllowed(t *testing.T) {
var args struct {
A string `arg:"positional"`
B *struct{} `arg:"subcommand"`
}
_, err := NewParser(Config{}, &args)
assert.Error(t, err)
}
func TestMinimalSubcommand(t *testing.T) {
type listCmd struct {
}
var args struct {
List *listCmd `arg:"subcommand"`
}
p, err := pparse("list", &args)
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(Config{}, &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 := pparse("invalid", &args)
assert.Error(t, err)
}
func TestNamedSubcommand(t *testing.T) {
type listCmd struct {
}
var args struct {
List *listCmd `arg:"subcommand:ls"`
}
p, err := pparse("ls", &args)
require.NoError(t, err)
assert.NotNil(t, args.List)
assert.Equal(t, args.List, p.Subcommand())
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
}
func TestSubcommandAliases(t *testing.T) {
type listCmd struct {
}
var args struct {
List *listCmd `arg:"subcommand:list|ls"`
}
p, err := pparse("ls", &args)
require.NoError(t, err)
assert.NotNil(t, args.List)
assert.Equal(t, args.List, p.Subcommand())
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
}
func TestEmptySubcommand(t *testing.T) {
type listCmd struct {
}
var args struct {
List *listCmd `arg:"subcommand"`
}
p, err := pparse("", &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 := pparse("list", &args)
require.NoError(t, err)
assert.Nil(t, args.Get)
assert.NotNil(t, args.List)
assert.Equal(t, args.List, p.Subcommand())
assert.Equal(t, []string{"list"}, p.SubcommandNames())
}
func TestTwoSubcommandsWithAliases(t *testing.T) {
type getCmd struct {
}
type listCmd struct {
}
var args struct {
Get *getCmd `arg:"subcommand:get|g"`
List *listCmd `arg:"subcommand:list|ls"`
}
p, err := pparse("ls", &args)
require.NoError(t, err)
assert.Nil(t, args.Get)
assert.NotNil(t, args.List)
assert.Equal(t, args.List, p.Subcommand())
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
}
func TestSubcommandsWithOptions(t *testing.T) {
type getCmd struct {
Name string
}
type listCmd struct {
Limit int
}
type cmd struct {
Verbose bool
Get *getCmd `arg:"subcommand"`
List *listCmd `arg:"subcommand"`
}
{
var args cmd
err := parse("list", &args)
require.NoError(t, err)
assert.Nil(t, args.Get)
assert.NotNil(t, args.List)
}
{
var args cmd
err := parse("list --limit 3", &args)
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("list --limit 3 --verbose", &args)
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("list --verbose --limit 3", &args)
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("--verbose list --limit 3", &args)
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("get", &args)
require.NoError(t, err)
assert.NotNil(t, args.Get)
assert.Nil(t, args.List)
}
{
var args cmd
err := parse("get --name test", &args)
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
setenv(t, "LIMIT", "123")
err := parse("list", &args)
require.NoError(t, err)
require.NotNil(t, args.List)
assert.Equal(t, 123, args.List.Limit)
}
{
var args cmd
setenv(t, "LIMIT", "not_an_integer")
err := parse("list", &args)
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 := pparse("grandparent parent child", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.NotNil(t, args.Grandparent.Parent)
require.NotNil(t, args.Grandparent.Parent.Child)
assert.Equal(t, args.Grandparent.Parent.Child, p.Subcommand())
assert.Equal(t, []string{"grandparent", "parent", "child"}, p.SubcommandNames())
}
{
var args root
p, err := pparse("grandparent parent", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.NotNil(t, args.Grandparent.Parent)
require.Nil(t, args.Grandparent.Parent.Child)
assert.Equal(t, args.Grandparent.Parent, p.Subcommand())
assert.Equal(t, []string{"grandparent", "parent"}, p.SubcommandNames())
}
{
var args root
p, err := pparse("grandparent", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.Nil(t, args.Grandparent.Parent)
assert.Equal(t, args.Grandparent, p.Subcommand())
assert.Equal(t, []string{"grandparent"}, p.SubcommandNames())
}
{
var args root
p, err := pparse("", &args)
require.NoError(t, err)
require.Nil(t, args.Grandparent)
assert.Nil(t, p.Subcommand())
assert.Empty(t, p.SubcommandNames())
}
}
func TestNestedSubcommandsWithAliases(t *testing.T) {
type child struct{}
type parent struct {
Child *child `arg:"subcommand:child|ch"`
}
type grandparent struct {
Parent *parent `arg:"subcommand:parent|pa"`
}
type root struct {
Grandparent *grandparent `arg:"subcommand:grandparent|gp"`
}
{
var args root
p, err := pparse("gp parent child", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.NotNil(t, args.Grandparent.Parent)
require.NotNil(t, args.Grandparent.Parent.Child)
assert.Equal(t, args.Grandparent.Parent.Child, p.Subcommand())
assert.Equal(t, []string{"gp", "parent", "child"}, p.SubcommandNames())
}
{
var args root
p, err := pparse("grandparent pa", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.NotNil(t, args.Grandparent.Parent)
require.Nil(t, args.Grandparent.Parent.Child)
assert.Equal(t, args.Grandparent.Parent, p.Subcommand())
assert.Equal(t, []string{"grandparent", "pa"}, p.SubcommandNames())
}
{
var args root
p, err := pparse("grandparent", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.Nil(t, args.Grandparent.Parent)
assert.Equal(t, args.Grandparent, p.Subcommand())
assert.Equal(t, []string{"grandparent"}, p.SubcommandNames())
}
{
var args root
p, err := pparse("", &args)
require.NoError(t, err)
require.Nil(t, args.Grandparent)
assert.Nil(t, p.Subcommand())
assert.Empty(t, p.SubcommandNames())
}
}
func TestSubcommandsWithPositionals(t *testing.T) {
type listCmd struct {
Pattern string `arg:"positional"`
}
type cmd struct {
Format string
List *listCmd `arg:"subcommand"`
}
{
var args cmd
err := parse("list", &args)
require.NoError(t, err)
assert.NotNil(t, args.List)
assert.Equal(t, "", args.List.Pattern)
}
{
var args cmd
err := parse("list --format json", &args)
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("list somepattern", &args)
require.NoError(t, err)
assert.NotNil(t, args.List)
assert.Equal(t, "somepattern", args.List.Pattern)
}
{
var args cmd
err := parse("list somepattern --format json", &args)
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("list --format json somepattern", &args)
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("--format json list somepattern", &args)
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("--format json", &args)
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("get", &args)
require.NoError(t, err)
assert.NotNil(t, args.Get)
assert.Empty(t, args.Get.Items)
}
{
var args cmd
err := parse("get --limit 5", &args)
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("get item1", &args)
require.NoError(t, err)
assert.NotNil(t, args.Get)
assert.Equal(t, []string{"item1"}, args.Get.Items)
}
{
var args cmd
err := parse("get item1 item2 item3", &args)
require.NoError(t, err)
assert.NotNil(t, args.Get)
assert.Equal(t, []string{"item1", "item2", "item3"}, args.Get.Items)
}
{
var args cmd
err := parse("get item1 --limit 5 item2", &args)
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(Config{}, &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())
}
func TestSubcommandInvalidInternal(t *testing.T) {
// this situation should never arise in practice but still good to test for it
var cmd struct{}
p, err := NewParser(Config{}, &cmd)
require.NoError(t, err)
p.subcommand = []string{"should", "never", "happen"}
sub := p.Subcommand()
assert.Nil(t, sub)
}

File diff suppressed because it is too large Load Diff

342
usage.go
View File

@ -3,84 +3,34 @@ package arg
import (
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strings"
)
// the width of the left column
const colWidth = 25
// Fail prints usage information to p.Config.Out and exits with status code 2.
// Fail prints usage information to stdout and exits with non-zero status
func (p *Parser) Fail(msg string) {
p.FailSubcommand(msg)
}
// FailSubcommand prints usage information for a specified subcommand to p.Config.Out,
// then exits with status code 2. To write usage information for a top-level
// subcommand, provide just the name of that subcommand. To write usage
// information for a subcommand that is nested under another subcommand, provide
// a sequence of subcommand names starting with the top-level subcommand and so
// on down the tree.
func (p *Parser) FailSubcommand(msg string, subcommand ...string) error {
err := p.WriteUsageForSubcommand(p.config.Out, subcommand...)
if err != nil {
return err
}
fmt.Fprintln(p.config.Out, "error:", msg)
p.config.Exit(2)
return nil
p.WriteUsage(os.Stdout)
fmt.Println("error:", msg)
os.Exit(-1)
}
// WriteUsage writes usage information to the given writer
func (p *Parser) WriteUsage(w io.Writer) {
p.WriteUsageForSubcommand(w, p.subcommand...)
}
// 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
}
var positionals, longOptions, shortOptions []*spec
for _, spec := range cmd.specs {
switch {
case spec.positional:
var positionals, options []*spec
for _, spec := range p.spec {
if spec.positional {
positionals = append(positionals, spec)
case spec.long != "":
longOptions = append(longOptions, spec)
case spec.short != "":
shortOptions = append(shortOptions, spec)
} else {
options = append(options, spec)
}
}
// print the beginning of the usage string
fmt.Fprintf(w, "Usage: %s", p.cmd.name)
for _, s := range subcommand {
fmt.Fprint(w, " "+s)
}
fmt.Fprintf(w, "usage: %s ", filepath.Base(os.Args[0]))
// write the option component of the usage message
for _, spec := range shortOptions {
// prefix with a space
fmt.Fprint(w, " ")
if !spec.required {
fmt.Fprint(w, "[")
}
fmt.Fprint(w, synopsis(spec, "-"+spec.short))
if !spec.required {
fmt.Fprint(w, "]")
}
}
for _, spec := range longOptions {
// prefix with a space
fmt.Fprint(w, " ")
for _, spec := range options {
if !spec.required {
fmt.Fprint(w, "[")
}
@ -88,256 +38,70 @@ func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) erro
if !spec.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 _, spec := range positionals {
fmt.Fprint(w, " ")
if !spec.required {
fmt.Fprint(w, "[")
closeBrackets += 1
}
if spec.cardinality == multiple {
fmt.Fprintf(w, "%s [%s ...]", spec.placeholder, spec.placeholder)
}
// write the positional component of the usage message
for _, spec := range positionals {
up := strings.ToUpper(spec.long)
if spec.multiple {
fmt.Fprintf(w, "[%s [%s ...]]", up, up)
} else {
fmt.Fprint(w, spec.placeholder)
fmt.Fprint(w, up)
}
}
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")
return nil
}
// print prints a line like this:
//
// --option FOO A description of the option [default: 123]
//
// If the text on the left is longer than a certain threshold, the description is moved to the next line:
//
// --verylongoptionoption VERY_LONG_VARIABLE
// A description of the option [default: 123]
//
// If multiple "extras" are provided then they are put inside a single set of square brackets:
//
// --option FOO A description of the option [default: 123, env: FOO]
func print(w io.Writer, item, description string, bracketed ...string) {
lhs := " " + item
fmt.Fprint(w, lhs)
if description != "" {
if len(lhs)+2 < colWidth {
fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs)))
} else {
fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
}
fmt.Fprint(w, description)
}
var brack string
for _, s := range bracketed {
if s != "" {
if brack != "" {
brack += ", "
}
brack += s
}
}
if brack != "" {
fmt.Fprintf(w, " [%s]", brack)
fmt.Fprint(w, " ")
}
fmt.Fprint(w, "\n")
}
func withDefault(s string) string {
if s == "" {
return ""
}
return "default: " + s
}
func withEnv(env string) string {
if env == "" {
return ""
}
return "env: " + env
}
// WriteHelp writes the usage string followed by the full help string for each option
func (p *Parser) WriteHelp(w io.Writer) {
p.WriteHelpForSubcommand(w, p.subcommand...)
}
// WriteHelpForSubcommand writes the usage string followed by the full help
// string for a specified subcommand. To write help for a top-level subcommand,
// provide just the name of that subcommand. To write help for a subcommand that
// is nested under another subcommand, provide a sequence of subcommand names
// starting with the top-level subcommand and so on down the tree.
func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error {
cmd, err := p.lookupCommand(subcommand...)
if err != nil {
return err
}
var positionals, longOptions, shortOptions, envOnlyOptions []*spec
var hasVersionOption bool
for _, spec := range cmd.specs {
switch {
case spec.positional:
var positionals, options []*spec
for _, spec := range p.spec {
if spec.positional {
positionals = append(positionals, spec)
case spec.long != "":
longOptions = append(longOptions, spec)
if spec.long == "version" {
hasVersionOption = true
}
case spec.short != "":
shortOptions = append(shortOptions, spec)
case spec.short == "" && spec.long == "":
envOnlyOptions = append(envOnlyOptions, spec)
} else {
options = append(options, spec)
}
}
// obtain a flattened list of options from all ancestors
// also determine if any ancestor has a version option spec
var globals []*spec
ancestor := cmd.parent
for ancestor != nil {
for _, spec := range ancestor.specs {
if spec.long == "version" {
hasVersionOption = true
break
}
}
globals = append(globals, ancestor.specs...)
ancestor = ancestor.parent
}
if p.description != "" {
fmt.Fprintln(w, p.description)
}
if !hasVersionOption && p.version != "" {
fmt.Fprintln(w, p.version)
}
p.WriteUsageForSubcommand(w, subcommand...)
p.WriteUsage(w)
// write the list of positionals
if len(positionals) > 0 {
fmt.Fprint(w, "\nPositional arguments:\n")
fmt.Fprint(w, "\npositional arguments:\n")
for _, spec := range positionals {
print(w, spec.placeholder, spec.help, withDefault(spec.defaultString), withEnv(spec.env))
fmt.Fprintf(w, " %s\n", spec.long)
}
}
// 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 _, spec := range shortOptions {
p.printOption(w, spec)
}
for _, spec := range longOptions {
p.printOption(w, spec)
// write the list of options
if len(options) > 0 {
fmt.Fprint(w, "\noptions:\n")
const colWidth = 25
for _, spec := range options {
left := " " + synopsis(spec, "--"+spec.long)
if spec.short != "" {
left += ", " + synopsis(spec, "-"+spec.short)
}
fmt.Fprint(w, left)
if spec.help != "" {
if len(left)+2 < colWidth {
fmt.Fprint(w, strings.Repeat(" ", colWidth-len(left)))
} else {
fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
}
fmt.Fprint(w, spec.help)
}
fmt.Fprint(w, "\n")
}
}
// write the list of global options
if len(globals) > 0 {
fmt.Fprint(w, "\nGlobal options:\n")
for _, spec := range globals {
p.printOption(w, spec)
}
}
// write the list of built in options
p.printOption(w, &spec{
cardinality: zero,
long: "help",
short: "h",
help: "display this help and exit",
})
if !hasVersionOption && p.version != "" {
p.printOption(w, &spec{
cardinality: zero,
long: "version",
help: "display version and exit",
})
}
// write the list of environment only variables
if len(envOnlyOptions) > 0 {
fmt.Fprint(w, "\nEnvironment variables:\n")
for _, spec := range envOnlyOptions {
p.printEnvOnlyVar(w, spec)
}
}
// write the list of subcommands
if len(cmd.subcommands) > 0 {
fmt.Fprint(w, "\nCommands:\n")
for _, subcmd := range cmd.subcommands {
names := append([]string{subcmd.name}, subcmd.aliases...)
print(w, strings.Join(names, ", "), subcmd.help)
}
}
if p.epilogue != "" {
fmt.Fprintln(w, "\n"+p.epilogue)
}
return nil
}
func (p *Parser) printOption(w io.Writer, spec *spec) {
ways := make([]string, 0, 2)
if spec.long != "" {
ways = append(ways, synopsis(spec, "--"+spec.long))
}
if spec.short != "" {
ways = append(ways, synopsis(spec, "-"+spec.short))
}
if len(ways) > 0 {
print(w, strings.Join(ways, ", "), spec.help, withDefault(spec.defaultString), withEnv(spec.env))
}
}
func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) {
ways := make([]string, 0, 2)
if spec.required {
ways = append(ways, "Required.")
} else {
ways = append(ways, "Optional.")
}
if spec.help != "" {
ways = append(ways, spec.help)
}
print(w, spec.env, strings.Join(ways, " "), withDefault(spec.defaultString))
}
func synopsis(spec *spec, form string) string {
// if the user omits the placeholder tag then we pick one automatically,
// but if the user explicitly specifies an empty placeholder then we
// leave out the placeholder in the help message
if spec.cardinality == zero || spec.placeholder == "" {
if spec.dest.Kind() == reflect.Bool {
return form
} else {
return form + " " + strings.ToUpper(spec.long)
}
return form + " " + spec.placeholder
}

46
usage_test.go Normal file
View File

@ -0,0 +1,46 @@
package arg
import (
"bytes"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWriteUsage(t *testing.T) {
expectedUsage := "usage: example [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]] \n"
expectedHelp := `usage: example [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]]
positional arguments:
input
output
options:
--verbose, -v verbosity level
--dataset DATASET dataset to use
--optimize OPTIMIZE, -O OPTIMIZE
optimization level
`
var args struct {
Input string `arg:"positional"`
Output []string `arg:"positional"`
Verbose bool `arg:"-v,help:verbosity level"`
Dataset string `arg:"help:dataset to use"`
Optimize int `arg:"-O,help:optimization level"`
}
p, err := NewParser(&args)
require.NoError(t, err)
os.Args[0] = "example"
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, usage.String())
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp, help.String())
}