use go-arg fork
This commit is contained in:
1 parent
7628172afd
commit
efa0660945
15 files changed
+4609
-160
No files matched your search
+6
-11
@@ -8,9 +8,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Logging ConfigLogging `arg:"-"`
|
Logging ConfigLogging `arg:"group:Logging"`
|
||||||
Stream ConfigStream `arg:"-"`
|
Stream ConfigStream `arg:"group:Stream"`
|
||||||
Http ConfigHTTP `arg:"-"`
|
Http ConfigHTTP `arg:"group:Http"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigLogging struct {
|
type ConfigLogging struct {
|
||||||
@@ -26,9 +26,9 @@ type ConfigHTTP struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ConfigStream struct {
|
type ConfigStream struct {
|
||||||
VideoOut ConfigVideoOutputStream `arg:"-"`
|
VideoOut ConfigVideoOutputStream `arg:"group:VideoOut"`
|
||||||
AudioOut ConfigAudioOutputStream `arg:"-"`
|
AudioOut ConfigAudioOutputStream `arg:"group:AudioOut"`
|
||||||
AudioIn ConfigAudioInputStream `arg:"-"`
|
AudioIn ConfigAudioInputStream `arg:"group:AudioIn"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigVideoOutputStream struct {
|
type ConfigVideoOutputStream struct {
|
||||||
@@ -60,10 +60,5 @@ func (c *ConfigHTTP) Address() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) MustParse() {
|
func (c *Config) MustParse() {
|
||||||
arg.MustParse(&c.Logging)
|
|
||||||
arg.MustParse(&c.Stream.VideoOut)
|
|
||||||
arg.MustParse(&c.Stream.AudioOut)
|
|
||||||
arg.MustParse(&c.Stream.AudioIn)
|
|
||||||
arg.MustParse(&c.Http)
|
|
||||||
arg.MustParse(c)
|
arg.MustParse(c)
|
||||||
}
|
}
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
+37
@@ -0,0 +1,37 @@
|
|||||||
|
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.17', '1.18', '1.19']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- id: go
|
||||||
|
name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- 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
|
||||||
+117
-2
@@ -134,10 +134,10 @@ arg.MustParse(&args)
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ ./example -h
|
$ ./example -h
|
||||||
Usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]]
|
Usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]]
|
||||||
|
|
||||||
Positional arguments:
|
Positional arguments:
|
||||||
INPUT
|
INPUT
|
||||||
OUTPUT
|
OUTPUT
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
@@ -180,6 +180,24 @@ var args struct {
|
|||||||
arg.MustParse(&args)
|
arg.MustParse(&args)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Ignoring environment variables and/or default values
|
||||||
|
|
||||||
|
The values in an existing structure can be kept in-tact by ignoring environment
|
||||||
|
variables and/or default values.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Test string `arg:"-t,env:TEST" default:"something"`
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := arg.NewParser(arg.Config{
|
||||||
|
IgnoreEnv: true,
|
||||||
|
IgnoreDefault: true,
|
||||||
|
}, &args)
|
||||||
|
|
||||||
|
err = p.Parse(os.Args)
|
||||||
|
```
|
||||||
|
|
||||||
### Arguments with multiple values
|
### Arguments with multiple values
|
||||||
```go
|
```go
|
||||||
var args struct {
|
var args struct {
|
||||||
@@ -444,6 +462,9 @@ Options:
|
|||||||
|
|
||||||
### Description strings
|
### 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
|
```go
|
||||||
type args struct {
|
type args struct {
|
||||||
Foo string
|
Foo string
|
||||||
@@ -469,6 +490,35 @@ Options:
|
|||||||
--help, -h display this help and exit
|
--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
|
||||||
|
|
||||||
*Introduced in version 1.1.0*
|
*Introduced in version 1.1.0*
|
||||||
@@ -533,6 +583,71 @@ if p.Subcommand() == nil {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Option groups
|
||||||
|
|
||||||
|
Option groups are a hybrid between subcommands and embedded structs. Option
|
||||||
|
groups create logical collections of related arguments with a help description,
|
||||||
|
and can be embedded in other groups and subcommands. Option groups can combine
|
||||||
|
configuration structs of multiple modules without requiring embedding.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Repository struct {
|
||||||
|
URL string `arg:"--host" help:"URL of the repository" default:"docker.io"`
|
||||||
|
User string `arg:"--user,env:REPO_USERNAME" help:"username to connect as"`
|
||||||
|
Password string `arg:"--,env:REPO_PASSWORD" help:"password to connect with"`
|
||||||
|
}
|
||||||
|
type BuildCmd struct {
|
||||||
|
Context string
|
||||||
|
Tag string
|
||||||
|
}
|
||||||
|
type PushCmd struct {
|
||||||
|
Repo Repository `arg:"group:Repository" help:"Change the default registry to push to."`
|
||||||
|
Tag string `help:"Tag"`
|
||||||
|
}
|
||||||
|
var args struct {
|
||||||
|
Build *BuildCmd `arg:"subcommand:build"`
|
||||||
|
Push *PushCmd `arg:"subcommand:push"`
|
||||||
|
Quiet bool `arg:"-q" help:"Quiet"` // this flag is global to all subcommands
|
||||||
|
}
|
||||||
|
|
||||||
|
arg.MustParse(&args)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case args.Build != nil:
|
||||||
|
fmt.Printf("build %s as %q\n", args.Build.Context, args.Build.Tag)
|
||||||
|
case args.Push != nil:
|
||||||
|
fmt.Printf("push %q to %q\n", args.Push.Tag, args.Push.Repo.URL)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The push command help message would look like:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usage: example push [--tag TAG] [--host HOST] [--user USER]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--tag TAG Tag
|
||||||
|
|
||||||
|
Repository options:
|
||||||
|
|
||||||
|
Change the default registry to push to.
|
||||||
|
|
||||||
|
--host HOST URL of the repository [default: docker.io]
|
||||||
|
--user USER username to connect as [env: REPO_USERNAME]
|
||||||
|
(environment only) password to connect with [env: REPO_PASSWORD]
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
--quiet, -q Quiet
|
||||||
|
--help, -h display this help and exit
|
||||||
|
```
|
||||||
|
|
||||||
|
Some additional rules apply when working with option groups:
|
||||||
|
* The `group` tag can only be used with fields that are structs or pointers to structs.
|
||||||
|
* Specifying default values in nested struct pointers _always_ result in an initialized struct.
|
||||||
|
* Option groups may not contain any positionals.
|
||||||
|
* Option groups cannot contain sub-commands.
|
||||||
|
```
|
||||||
|
|
||||||
### API Documentation
|
### API Documentation
|
||||||
|
|
||||||
https://godoc.org/github.com/alexflint/go-arg
|
https://godoc.org/github.com/alexflint/go-arg
|
||||||
|
|||||||
+545
@@ -0,0 +1,545 @@
|
|||||||
|
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) {}
|
||||||
|
|
||||||
|
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) {}
|
||||||
|
|
||||||
|
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) {}
|
||||||
|
|
||||||
|
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 argument groups
|
||||||
|
func Example_helpTextWithGroups() {
|
||||||
|
os.Args = split("./example push --help")
|
||||||
|
|
||||||
|
type Repository struct {
|
||||||
|
URL string `arg:"--host" help:"URL of the repository" default:"docker.io"`
|
||||||
|
User string `arg:"--user,env:REPO_USERNAME" help:"username to connect as"`
|
||||||
|
Password string `arg:"--,env:REPO_PASSWORD" help:"password to connect with"`
|
||||||
|
}
|
||||||
|
type BuildCmd struct {
|
||||||
|
Context string
|
||||||
|
Tag string
|
||||||
|
}
|
||||||
|
type PushCmd struct {
|
||||||
|
Repo Repository `arg:"group:Repository" help:"Change the default registry to push to."`
|
||||||
|
Tag string `help:"Tag"`
|
||||||
|
}
|
||||||
|
var args struct {
|
||||||
|
Build *BuildCmd `arg:"subcommand:build"`
|
||||||
|
Push *PushCmd `arg:"subcommand:push"`
|
||||||
|
Quiet bool `arg:"-q" help:"Quiet"` // this flag is global to all subcommands
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is only necessary when running inside golang's runnable example harness
|
||||||
|
mustParseExit = func(int) {}
|
||||||
|
|
||||||
|
MustParse(&args)
|
||||||
|
|
||||||
|
// output:
|
||||||
|
// Usage: example push [--tag TAG] [--host HOST] [--user USER]
|
||||||
|
//
|
||||||
|
// Options:
|
||||||
|
// --tag TAG Tag
|
||||||
|
//
|
||||||
|
// Repository options:
|
||||||
|
//
|
||||||
|
// Change the default registry to push to.
|
||||||
|
//
|
||||||
|
// --host HOST URL of the repository [default: docker.io]
|
||||||
|
// --user USER username to connect as [env: REPO_USERNAME]
|
||||||
|
// (environment only) password to connect with [env: REPO_PASSWORD]
|
||||||
|
//
|
||||||
|
// Global options:
|
||||||
|
// --quiet, -q Quiet
|
||||||
|
// --help, -h display this help and exit
|
||||||
|
}
|
||||||
|
|
||||||
|
// This example shows the usage string generated by go-arg when using subcommands
|
||||||
|
func Example_helpTextWhenUsingSubcommand() {
|
||||||
|
// These are the args you would pass in on the command line
|
||||||
|
os.Args = split("./example get --help")
|
||||||
|
|
||||||
|
type getCmd struct {
|
||||||
|
Item string `arg:"positional,required" help:"item to fetch"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type listCmd struct {
|
||||||
|
Format string `help:"output format"`
|
||||||
|
Limit int
|
||||||
|
}
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Verbose bool
|
||||||
|
Get *getCmd `arg:"subcommand" help:"fetch an item and print it"`
|
||||||
|
List *listCmd `arg:"subcommand" help:"list available items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is only necessary when running inside golang's runnable example harness
|
||||||
|
mustParseExit = func(int) {}
|
||||||
|
|
||||||
|
MustParse(&args)
|
||||||
|
|
||||||
|
// output:
|
||||||
|
// Usage: example get ITEM
|
||||||
|
//
|
||||||
|
// Positional arguments:
|
||||||
|
// ITEM item to fetch
|
||||||
|
//
|
||||||
|
// Global options:
|
||||||
|
// --verbose
|
||||||
|
// --help, -h display this help and exit
|
||||||
|
}
|
||||||
|
|
||||||
|
// This example shows how to print help for an explicit subcommand
|
||||||
|
func Example_writeHelpForSubcommand() {
|
||||||
|
// 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) {}
|
||||||
|
|
||||||
|
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) {}
|
||||||
|
|
||||||
|
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) {}
|
||||||
|
|
||||||
|
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:
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
module github.com/alexflint/go-arg
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alexflint/go-scalar v1.2.0
|
||||||
|
github.com/stretchr/testify v1.7.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
go 1.18
|
||||||
+15
@@ -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=
|
||||||
+290
-105
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -43,18 +44,19 @@ func (p path) Child(f reflect.StructField) path {
|
|||||||
|
|
||||||
// spec represents a command line option
|
// spec represents a command line option
|
||||||
type spec struct {
|
type spec struct {
|
||||||
dest path
|
dest path
|
||||||
field reflect.StructField // the struct field from which this option was created
|
field reflect.StructField // the struct field from which this option was created
|
||||||
long string // the --long form for this option, or empty if none
|
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
|
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)
|
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
|
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
|
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
|
separate bool // if true, each slice and map entry will have its own --flag
|
||||||
help string // the help text for this option
|
help string // the help text for this option
|
||||||
env string // the name of the environment variable for this option, or empty for none
|
env string // the name of the environment variable for this option, or empty for none
|
||||||
defaultVal string // default value for this option
|
defaultValue reflect.Value // default value for this option
|
||||||
placeholder string // name of the data in help
|
defaultString string // default value for this option, in string form to be displayed in help text
|
||||||
|
placeholder string // name of the data in help
|
||||||
}
|
}
|
||||||
|
|
||||||
// command represents a named subcommand, or the top-level command
|
// command represents a named subcommand, or the top-level command
|
||||||
@@ -62,38 +64,54 @@ type command struct {
|
|||||||
name string
|
name string
|
||||||
help string
|
help string
|
||||||
dest path
|
dest path
|
||||||
specs []*spec
|
options []*spec
|
||||||
subcommands []*command
|
subcommands []*command
|
||||||
|
groups []*command
|
||||||
parent *command
|
parent *command
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// specs gets all the specs from this command plus all nested option groups,
|
||||||
|
// recursively through descendants
|
||||||
|
func (cmd command) specs() []*spec {
|
||||||
|
var specs []*spec
|
||||||
|
specs = append(specs, cmd.options...)
|
||||||
|
for _, grpcmd := range cmd.groups {
|
||||||
|
specs = append(specs, grpcmd.specs()...)
|
||||||
|
}
|
||||||
|
return specs
|
||||||
|
}
|
||||||
|
|
||||||
// ErrHelp indicates that -h or --help were provided
|
// ErrHelp indicates that -h or --help were provided
|
||||||
var ErrHelp = errors.New("help requested by user")
|
var ErrHelp = errors.New("help requested by user")
|
||||||
|
|
||||||
// ErrVersion indicates that --version was provided
|
// ErrVersion indicates that --version was provided
|
||||||
var ErrVersion = errors.New("version requested by user")
|
var ErrVersion = errors.New("version requested by user")
|
||||||
|
|
||||||
|
// for monkey patching in example code
|
||||||
|
var mustParseExit = os.Exit
|
||||||
|
|
||||||
// MustParse processes command line arguments and exits upon failure
|
// MustParse processes command line arguments and exits upon failure
|
||||||
func MustParse(dest ...interface{}) *Parser {
|
func MustParse(dest ...interface{}) *Parser {
|
||||||
p, err := NewParser(Config{}, dest...)
|
return mustParse(Config{Exit: mustParseExit}, dest...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mustParse is a helper that facilitates testing
|
||||||
|
func mustParse(config Config, dest ...interface{}) *Parser {
|
||||||
|
if config.Exit == nil {
|
||||||
|
config.Exit = os.Exit
|
||||||
|
}
|
||||||
|
if config.Out == nil {
|
||||||
|
config.Out = os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := NewParser(config, dest...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(stdout, err)
|
fmt.Fprintln(config.Out, err)
|
||||||
osExit(-1)
|
config.Exit(-1)
|
||||||
return nil // just in case osExit was monkey-patched
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
err = p.Parse(flags())
|
|
||||||
switch {
|
|
||||||
case err == ErrHelp:
|
|
||||||
p.writeHelpForSubcommand(stdout, p.lastCmd)
|
|
||||||
osExit(0)
|
|
||||||
case err == ErrVersion:
|
|
||||||
fmt.Fprintln(stdout, p.version)
|
|
||||||
osExit(0)
|
|
||||||
case err != nil:
|
|
||||||
p.failWithSubcommand(err.Error(), p.lastCmd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.MustParse(flags())
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +139,20 @@ type Config struct {
|
|||||||
|
|
||||||
// IgnoreEnv instructs the library not to read environment variables
|
// IgnoreEnv instructs the library not to read environment variables
|
||||||
IgnoreEnv bool
|
IgnoreEnv bool
|
||||||
|
|
||||||
|
// IgnoreDefault instructs the library not to reset the variables to the
|
||||||
|
// default values, including pointers to sub commands
|
||||||
|
IgnoreDefault bool
|
||||||
|
|
||||||
|
// StrictSubcommands intructs the library not to allow global commands after
|
||||||
|
// subcommand
|
||||||
|
StrictSubcommands bool
|
||||||
|
|
||||||
|
// Exit is called to terminate the process with an error code (defaults to os.Exit)
|
||||||
|
Exit func(int)
|
||||||
|
|
||||||
|
// Out is where help text, usage text, and failure messages are printed (defaults to os.Stdout)
|
||||||
|
Out io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parser represents a set of command line options with destination values
|
// Parser represents a set of command line options with destination values
|
||||||
@@ -130,6 +162,7 @@ type Parser struct {
|
|||||||
config Config
|
config Config
|
||||||
version string
|
version string
|
||||||
description string
|
description string
|
||||||
|
epilogue string
|
||||||
|
|
||||||
// the following field changes during processing of command line arguments
|
// the following field changes during processing of command line arguments
|
||||||
lastCmd *command
|
lastCmd *command
|
||||||
@@ -151,6 +184,14 @@ type Described interface {
|
|||||||
Description() string
|
Description() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Epilogued is the interface that the destination struct should implement to
|
||||||
|
// add an epilogue string at the bottom of the help message.
|
||||||
|
type Epilogued interface {
|
||||||
|
// Epilogue returns the string that will be printed on a line by itself
|
||||||
|
// at the end of the help message.
|
||||||
|
Epilogue() string
|
||||||
|
}
|
||||||
|
|
||||||
// walkFields calls a function for each field of a struct, recursively expanding struct fields.
|
// 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) {
|
func walkFields(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool) {
|
||||||
walkFieldsImpl(t, visit, nil)
|
walkFieldsImpl(t, visit, nil)
|
||||||
@@ -174,6 +215,14 @@ func walkFieldsImpl(t reflect.Type, visit func(field reflect.StructField, owner
|
|||||||
|
|
||||||
// NewParser constructs a parser from a list of destination structs
|
// NewParser constructs a parser from a list of destination structs
|
||||||
func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
||||||
|
// fill in defaults
|
||||||
|
if config.Exit == nil {
|
||||||
|
config.Exit = os.Exit
|
||||||
|
}
|
||||||
|
if config.Out == nil {
|
||||||
|
config.Out = os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
// first pick a name for the command for use in the usage text
|
// first pick a name for the command for use in the usage text
|
||||||
var name string
|
var name string
|
||||||
switch {
|
switch {
|
||||||
@@ -197,34 +246,36 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// process each of the destination values
|
// process each of the destination values
|
||||||
for i, dest := range dests {
|
for _, dest := range dests {
|
||||||
t := reflect.TypeOf(dest)
|
t := reflect.TypeOf(dest)
|
||||||
if t.Kind() != reflect.Ptr {
|
if t.Kind() != reflect.Ptr {
|
||||||
panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", t))
|
panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", t))
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd, err := cmdFromStruct(name, path{root: i}, t)
|
err := p.cmd.parseFieldsFromStructPointer(t, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// add nonzero field values as defaults
|
// for backwards compatibility, add nonzero field values as defaults
|
||||||
for _, spec := range cmd.specs {
|
// this applies only to the top-level command, not to subcommands (this inconsistency
|
||||||
if v := p.val(spec.dest); v.IsValid() && !isZero(v) {
|
// is the reason that this method for setting default values was deprecated)
|
||||||
if defaultVal, ok := v.Interface().(encoding.TextMarshaler); ok {
|
for _, spec := range p.cmd.specs() {
|
||||||
str, err := defaultVal.MarshalText()
|
// get the value
|
||||||
if err != nil {
|
defaultString, defaultValue, err := p.defaultVal(spec.dest)
|
||||||
return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
spec.defaultVal = string(str)
|
|
||||||
} else {
|
|
||||||
spec.defaultVal = fmt.Sprintf("%v", v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
p.cmd.specs = append(p.cmd.specs, cmd.specs...)
|
// if the value is the "zero value" (e.g. nil pointer, empty struct) then ignore
|
||||||
p.cmd.subcommands = append(p.cmd.subcommands, cmd.subcommands...)
|
if defaultString == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// store as a default
|
||||||
|
spec.defaultString = defaultString
|
||||||
|
spec.defaultValue = defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
if dest, ok := dest.(Versioned); ok {
|
if dest, ok := dest.(Versioned); ok {
|
||||||
p.version = dest.Version()
|
p.version = dest.Version()
|
||||||
@@ -232,29 +283,56 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
|||||||
if dest, ok := dest.(Described); ok {
|
if dest, ok := dest.(Described); ok {
|
||||||
p.description = dest.Description()
|
p.description = dest.Description()
|
||||||
}
|
}
|
||||||
|
if dest, ok := dest.(Epilogued); ok {
|
||||||
|
p.epilogue = dest.Epilogue()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &p, nil
|
return &p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
// parseFieldsFromStructPointer ensures the destination structure is a pointer
|
||||||
|
// to a struct. This function should be called when parsing commands or
|
||||||
|
// subcommands as they can only be a struct pointer.
|
||||||
|
func (cmd *command) parseFieldsFromStructPointer(t reflect.Type, insideGroup bool) error {
|
||||||
// commands can only be created from pointers to structs
|
// commands can only be created from pointers to structs
|
||||||
if t.Kind() != reflect.Ptr {
|
if t.Kind() != reflect.Ptr {
|
||||||
return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a %s",
|
return fmt.Errorf("subcommands must be pointers to structs but %s is a %s",
|
||||||
dest, t.Kind())
|
cmd.dest, t.Kind())
|
||||||
}
|
}
|
||||||
|
|
||||||
t = t.Elem()
|
t = t.Elem()
|
||||||
if t.Kind() != reflect.Struct {
|
if t.Kind() != reflect.Struct {
|
||||||
return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a pointer to %s",
|
return fmt.Errorf("subcommands must be pointers to structs but %s is a pointer to %s",
|
||||||
dest, t.Kind())
|
cmd.dest, t.Kind())
|
||||||
|
}
|
||||||
|
return cmd.parseStruct(t, insideGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFieldsFromStructOrStructPointer ensures the destination structure is
|
||||||
|
// either a pointer to a struct, or a struct. This function should be called
|
||||||
|
// when parsing option groups as they can only be a struct, or a pointer to one.
|
||||||
|
func (cmd *command) parseFieldsFromStructOrStructPointer(t reflect.Type, insideGroup bool) error {
|
||||||
|
// option groups can only be created from structs or pointers to structs
|
||||||
|
typeHint := ""
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
typeHint = "a pointer to "
|
||||||
|
t = t.Elem()
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := command{
|
if t.Kind() != reflect.Struct {
|
||||||
name: name,
|
return fmt.Errorf("option groups must be structs or pointers to structs, but %s is %s%s",
|
||||||
dest: dest,
|
cmd.dest, typeHint, t.Kind())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return cmd.parseStruct(t, insideGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseStruct populates the command instance based on the type and annotations
|
||||||
|
// of the target struct. As these command instances are used for either (sub)
|
||||||
|
// commands or option groups, please refer to the parseFieldsFromStructPointer
|
||||||
|
// or parseFieldsFromStructOrStructPointer respectively.
|
||||||
|
func (cmd *command) parseStruct(t reflect.Type, insideGroup bool) error {
|
||||||
var errs []string
|
var errs []string
|
||||||
walkFields(t, func(field reflect.StructField, t reflect.Type) bool {
|
walkFields(t, func(field reflect.StructField, t reflect.Type) bool {
|
||||||
// check for the ignore switch in the tag
|
// check for the ignore switch in the tag
|
||||||
@@ -276,7 +354,7 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// duplicate the entire path to avoid slice overwrites
|
// duplicate the entire path to avoid slice overwrites
|
||||||
subdest := dest.Child(field)
|
subdest := cmd.dest.Child(field)
|
||||||
spec := spec{
|
spec := spec{
|
||||||
dest: subdest,
|
dest: subdest,
|
||||||
field: field,
|
field: field,
|
||||||
@@ -288,13 +366,7 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
|||||||
spec.help = help
|
spec.help = help
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultVal, hasDefault := field.Tag.Lookup("default")
|
|
||||||
if hasDefault {
|
|
||||||
spec.defaultVal = defaultVal
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look at the tag
|
// Look at the tag
|
||||||
var isSubcommand bool // tracks whether this field is a subcommand
|
|
||||||
for _, key := range strings.Split(tag, ",") {
|
for _, key := range strings.Split(tag, ",") {
|
||||||
if key == "" {
|
if key == "" {
|
||||||
continue
|
continue
|
||||||
@@ -319,11 +391,6 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
|||||||
}
|
}
|
||||||
spec.short = key[1:]
|
spec.short = key[1:]
|
||||||
case key == "required":
|
case key == "required":
|
||||||
if hasDefault {
|
|
||||||
errs = append(errs, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified",
|
|
||||||
t.Name(), field.Name))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
spec.required = true
|
spec.required = true
|
||||||
case key == "positional":
|
case key == "positional":
|
||||||
spec.positional = true
|
spec.positional = true
|
||||||
@@ -339,24 +406,55 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
|||||||
spec.env = strings.ToUpper(field.Name)
|
spec.env = strings.ToUpper(field.Name)
|
||||||
}
|
}
|
||||||
case key == "subcommand":
|
case key == "subcommand":
|
||||||
|
subCmd := command{
|
||||||
|
name: value,
|
||||||
|
dest: subdest,
|
||||||
|
parent: cmd,
|
||||||
|
help: field.Tag.Get("help"),
|
||||||
|
}
|
||||||
|
cmd.subcommands = append(cmd.subcommands, &subCmd)
|
||||||
|
|
||||||
|
if insideGroup {
|
||||||
|
errs = append(errs, fmt.Sprintf("%s.%s: %s subcommands cannot be part of option groups",
|
||||||
|
t.Name(), field.Name, field.Type.String()))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// decide on a name for the subcommand
|
// decide on a name for the subcommand
|
||||||
cmdname := value
|
if subCmd.name == "" {
|
||||||
if cmdname == "" {
|
subCmd.name = strings.ToLower(field.Name)
|
||||||
cmdname = strings.ToLower(field.Name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse the subcommand recursively
|
// parse the subcommand recursively
|
||||||
subcmd, err := cmdFromStruct(cmdname, subdest, field.Type)
|
err := subCmd.parseFieldsFromStructPointer(field.Type, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err.Error())
|
errs = append(errs, err.Error())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
subcmd.parent = &cmd
|
return true
|
||||||
subcmd.help = field.Tag.Get("help")
|
case key == "group":
|
||||||
|
// parse the option group recursively
|
||||||
|
optGrp := command{
|
||||||
|
name: value,
|
||||||
|
dest: subdest,
|
||||||
|
parent: cmd,
|
||||||
|
help: field.Tag.Get("help"),
|
||||||
|
}
|
||||||
|
cmd.groups = append(cmd.groups, &optGrp)
|
||||||
|
|
||||||
cmd.subcommands = append(cmd.subcommands, subcmd)
|
// decide on a name for the group
|
||||||
isSubcommand = true
|
if optGrp.name == "" {
|
||||||
|
optGrp.name = strings.Title(field.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := optGrp.parseFieldsFromStructOrStructPointer(field.Type, true)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
default:
|
default:
|
||||||
errs = append(errs, fmt.Sprintf("unrecognized tag '%s' on field %s", key, tag))
|
errs = append(errs, fmt.Sprintf("unrecognized tag '%s' on field %s", key, tag))
|
||||||
return false
|
return false
|
||||||
@@ -372,47 +470,77 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
|||||||
spec.placeholder = strings.ToUpper(spec.field.Name)
|
spec.placeholder = strings.ToUpper(spec.field.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether this field is supported. It's good to do this here rather than
|
// check whether this field is supported. It's good to do this here rather than
|
||||||
// wait until ParseValue because it means that a program with invalid argument
|
// wait until ParseValue because it means that a program with invalid argument
|
||||||
// fields will always fail regardless of whether the arguments it received
|
// fields will always fail regardless of whether the arguments it received
|
||||||
// exercised those fields.
|
// exercised those fields.
|
||||||
if !isSubcommand {
|
var err error
|
||||||
cmd.specs = append(cmd.specs, &spec)
|
spec.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
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
defaultString, hasDefault := field.Tag.Lookup("default")
|
||||||
spec.cardinality, err = cardinalityOf(field.Type)
|
if hasDefault {
|
||||||
if err != nil {
|
// we do not support default values for maps and slices
|
||||||
errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported",
|
if spec.cardinality == multiple {
|
||||||
t.Name(), field.Name, field.Type.String()))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if spec.cardinality == multiple && hasDefault {
|
|
||||||
errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields",
|
errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields",
|
||||||
t.Name(), field.Name))
|
t.Name(), field.Name))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// a required field cannot also have a default value
|
||||||
|
if spec.required {
|
||||||
|
errs = append(errs, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified",
|
||||||
|
t.Name(), field.Name))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the default value
|
||||||
|
spec.defaultString = defaultString
|
||||||
|
if field.Type.Kind() == reflect.Ptr {
|
||||||
|
// here we have a field of type *T and we create a new T, no need to dereference
|
||||||
|
// in order for the value to be settable
|
||||||
|
spec.defaultValue = reflect.New(field.Type.Elem())
|
||||||
|
} else {
|
||||||
|
// here we have a field of type T and we create a new T and then dereference it
|
||||||
|
// so that the resulting value is settable
|
||||||
|
spec.defaultValue = reflect.New(field.Type).Elem()
|
||||||
|
}
|
||||||
|
err := scalar.ParseValue(spec.defaultValue, defaultString)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Sprintf("%s.%s: error processing default value: %v", t.Name(), field.Name, err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add the spec to the list of specs
|
||||||
|
cmd.options = append(cmd.options, &spec)
|
||||||
|
|
||||||
// if this was an embedded field then we already returned true up above
|
// if this was an embedded field then we already returned true up above
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return nil, errors.New(strings.Join(errs, "\n"))
|
return errors.New(strings.Join(errs, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// check that we don't have both positionals and subcommands
|
// check that we don't have both positionals and subcommands
|
||||||
var hasPositional bool
|
var hasPositional bool
|
||||||
for _, spec := range cmd.specs {
|
for _, spec := range cmd.options {
|
||||||
if spec.positional {
|
if spec.positional {
|
||||||
hasPositional = true
|
hasPositional = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if hasPositional && len(cmd.subcommands) > 0 {
|
if hasPositional && len(cmd.subcommands) > 0 {
|
||||||
return nil, fmt.Errorf("%s cannot have both subcommands and positional arguments", dest)
|
return fmt.Errorf("%s cannot have both subcommands and positional arguments",
|
||||||
|
cmd.dest)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cmd, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse processes the given command line option, storing the results in the field
|
// Parse processes the given command line option, storing the results in the field
|
||||||
@@ -433,6 +561,20 @@ func (p *Parser) Parse(args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Parser) MustParse(args []string) {
|
||||||
|
err := p.Parse(args)
|
||||||
|
switch {
|
||||||
|
case err == ErrHelp:
|
||||||
|
p.writeHelpForSubcommand(p.config.Out, p.lastCmd)
|
||||||
|
p.config.Exit(0)
|
||||||
|
case err == ErrVersion:
|
||||||
|
fmt.Fprintln(p.config.Out, p.version)
|
||||||
|
p.config.Exit(0)
|
||||||
|
case err != nil:
|
||||||
|
p.failWithSubcommand(err.Error(), p.lastCmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// process environment vars for the given arguments
|
// process environment vars for the given arguments
|
||||||
func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error {
|
func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error {
|
||||||
for _, spec := range specs {
|
for _, spec := range specs {
|
||||||
@@ -489,8 +631,7 @@ func (p *Parser) process(args []string) error {
|
|||||||
p.lastCmd = curCmd
|
p.lastCmd = curCmd
|
||||||
|
|
||||||
// make a copy of the specs because we will add to this list each time we expand a subcommand
|
// make a copy of the specs because we will add to this list each time we expand a subcommand
|
||||||
specs := make([]*spec, len(curCmd.specs))
|
specs := curCmd.specs()
|
||||||
copy(specs, curCmd.specs)
|
|
||||||
|
|
||||||
// deal with environment vars
|
// deal with environment vars
|
||||||
if !p.config.IgnoreEnv {
|
if !p.config.IgnoreEnv {
|
||||||
@@ -525,16 +666,20 @@ func (p *Parser) process(args []string) error {
|
|||||||
return fmt.Errorf("invalid subcommand: %s", arg)
|
return fmt.Errorf("invalid subcommand: %s", arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// instantiate the field to point to a new struct
|
// ensure the command struct exists (is not a nil pointer)
|
||||||
v := p.val(subcmd.dest)
|
p.val(subcmd.dest)
|
||||||
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
|
// add the new options to the set of allowed options
|
||||||
specs = append(specs, subcmd.specs...)
|
if p.config.StrictSubcommands {
|
||||||
|
specs = make([]*spec, len(subcmd.specs()))
|
||||||
|
copy(specs, subcmd.specs())
|
||||||
|
} else {
|
||||||
|
specs = append(specs, subcmd.specs()...)
|
||||||
|
}
|
||||||
|
|
||||||
// capture environment vars for these new options
|
// capture environment vars for these new options
|
||||||
if !p.config.IgnoreEnv {
|
if !p.config.IgnoreEnv {
|
||||||
err := p.captureEnvVars(subcmd.specs, wasPresent)
|
err := p.captureEnvVars(subcmd.specs(), wasPresent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -659,11 +804,14 @@ func (p *Parser) process(args []string) error {
|
|||||||
}
|
}
|
||||||
return errors.New(msg)
|
return errors.New(msg)
|
||||||
}
|
}
|
||||||
if spec.defaultVal != "" {
|
|
||||||
err := scalar.ParseValue(p.val(spec.dest), spec.defaultVal)
|
if spec.defaultValue.IsValid() && !p.config.IgnoreDefault {
|
||||||
if err != nil {
|
// One issue here is that if the user now modifies the value then
|
||||||
return fmt.Errorf("error processing default value for %s: %v", name, err)
|
// the default value stored in the spec will be corrupted. There
|
||||||
}
|
// is no general way to "deep-copy" values in Go, and we still
|
||||||
|
// support the old-style method for specifying defaults as
|
||||||
|
// Go values assigned directly to the struct field, so we are stuck.
|
||||||
|
p.val(spec.dest).Set(spec.defaultValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,20 +836,57 @@ func isFlag(s string) bool {
|
|||||||
return strings.HasPrefix(s, "-") && strings.TrimLeft(s, "-") != ""
|
return strings.HasPrefix(s, "-") && strings.TrimLeft(s, "-") != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// val returns a reflect.Value corresponding to the current value for the
|
// defaultVal returns the string representation of the value at dest if it is
|
||||||
// given path
|
// reachable without traversing nil pointers, but only if it does not represent
|
||||||
func (p *Parser) val(dest path) reflect.Value {
|
// the default value for the type.
|
||||||
|
func (p *Parser) defaultVal(dest path) (string, reflect.Value, error) {
|
||||||
v := p.roots[dest.root]
|
v := p.roots[dest.root]
|
||||||
for _, field := range dest.fields {
|
for _, field := range dest.fields {
|
||||||
if v.Kind() == reflect.Ptr {
|
if v.Kind() == reflect.Ptr {
|
||||||
if v.IsNil() {
|
if v.IsNil() {
|
||||||
return reflect.Value{}
|
return "", v, nil
|
||||||
}
|
}
|
||||||
v = v.Elem()
|
v = v.Elem()
|
||||||
}
|
}
|
||||||
|
|
||||||
v = v.FieldByIndex(field.Index)
|
v = v.FieldByIndex(field.Index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !v.IsValid() || isZero(v) {
|
||||||
|
return "", v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if defaultVal, ok := v.Interface().(encoding.TextMarshaler); ok {
|
||||||
|
str, err := defaultVal.MarshalText()
|
||||||
|
if err != nil {
|
||||||
|
return "", v, fmt.Errorf("%v: error marshaling default value to string: %w", dest, err)
|
||||||
|
}
|
||||||
|
return string(str), v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%v", v), v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// val returns a reflect.Value corresponding to the current value for the
|
||||||
|
// given path initiating nil pointers in the path
|
||||||
|
func (p *Parser) val(dest path) reflect.Value {
|
||||||
|
v := p.roots[dest.root]
|
||||||
|
for _, field := range dest.fields {
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
if v.IsNil() {
|
||||||
|
v.Set(reflect.New(v.Type().Elem()))
|
||||||
|
}
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
v = v.FieldByIndex(field.Index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't return a nil-pointer
|
||||||
|
if v.Kind() == reflect.Ptr && v.IsNil() {
|
||||||
|
v.Set(reflect.New(v.Type().Elem()))
|
||||||
|
}
|
||||||
|
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1991
File diff suppressed because it is too large.
Load diff
+11
-6
@@ -13,9 +13,9 @@ import (
|
|||||||
var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()
|
var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()
|
||||||
|
|
||||||
// cardinality tracks how many tokens are expected for a given spec
|
// cardinality tracks how many tokens are expected for a given spec
|
||||||
// - zero is a boolean, which does to expect any value
|
// - zero is a boolean, which does to expect any value
|
||||||
// - one is an ordinary option that will be parsed from a single token
|
// - 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
|
// - multiple is a slice or map that can accept zero or more tokens
|
||||||
type cardinality int
|
type cardinality int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -74,10 +74,10 @@ func cardinalityOf(t reflect.Type) (cardinality, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// isBoolean returns true if the type can be parsed from a single string
|
// isBoolean returns true if the type is a boolean or a pointer to a boolean
|
||||||
func isBoolean(t reflect.Type) bool {
|
func isBoolean(t reflect.Type) bool {
|
||||||
switch {
|
switch {
|
||||||
case t.Implements(textUnmarshalerType):
|
case isTextUnmarshaler(t):
|
||||||
return false
|
return false
|
||||||
case t.Kind() == reflect.Bool:
|
case t.Kind() == reflect.Bool:
|
||||||
return true
|
return true
|
||||||
@@ -88,6 +88,11 @@ func isBoolean(t reflect.Type) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isTextUnmarshaler returns true if the type or its pointer implements encoding.TextUnmarshaler
|
||||||
|
func isTextUnmarshaler(t reflect.Type) bool {
|
||||||
|
return t.Implements(textUnmarshalerType) || reflect.PtrTo(t).Implements(textUnmarshalerType)
|
||||||
|
}
|
||||||
|
|
||||||
// isExported returns true if the struct field name is exported
|
// isExported returns true if the struct field name is exported
|
||||||
func isExported(field string) bool {
|
func isExported(field string) bool {
|
||||||
r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8
|
r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8
|
||||||
@@ -97,7 +102,7 @@ func isExported(field string) bool {
|
|||||||
// isZero returns true if v contains the zero value for its type
|
// isZero returns true if v contains the zero value for its type
|
||||||
func isZero(v reflect.Value) bool {
|
func isZero(v reflect.Value) bool {
|
||||||
t := v.Type()
|
t := v.Type()
|
||||||
if t.Kind() == reflect.Slice || t.Kind() == reflect.Map {
|
if t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice || t.Kind() == reflect.Map || t.Kind() == reflect.Chan || t.Kind() == reflect.Interface {
|
||||||
return v.IsNil()
|
return v.IsNil()
|
||||||
}
|
}
|
||||||
if !t.Comparable() {
|
if !t.Comparable() {
|
||||||
|
|||||||
+112
@@ -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)))
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
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(¬SliceOrMap).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)
|
||||||
|
}
|
||||||
+408
@@ -0,0 +1,408 @@
|
|||||||
|
package arg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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 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 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 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := NewParser(Config{}, &cmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Nil(t, cmd.Sub)
|
||||||
|
}
|
||||||
+65
-36
@@ -3,20 +3,12 @@ package arg
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// the width of the left column
|
// the width of the left column
|
||||||
const colWidth = 25
|
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
|
// Fail prints usage information to stderr and exits with non-zero status
|
||||||
func (p *Parser) Fail(msg string) {
|
func (p *Parser) Fail(msg string) {
|
||||||
p.failWithSubcommand(msg, p.cmd)
|
p.failWithSubcommand(msg, p.cmd)
|
||||||
@@ -39,9 +31,9 @@ func (p *Parser) FailSubcommand(msg string, subcommand ...string) error {
|
|||||||
|
|
||||||
// failWithSubcommand prints usage information for the given subcommand to stderr and exits with non-zero status
|
// failWithSubcommand prints usage information for the given subcommand to stderr and exits with non-zero status
|
||||||
func (p *Parser) failWithSubcommand(msg string, cmd *command) {
|
func (p *Parser) failWithSubcommand(msg string, cmd *command) {
|
||||||
p.writeUsageForSubcommand(stderr, cmd)
|
p.writeUsageForSubcommand(p.config.Out, cmd)
|
||||||
fmt.Fprintln(stderr, "error:", msg)
|
fmt.Fprintln(p.config.Out, "error:", msg)
|
||||||
osExit(-1)
|
p.config.Exit(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteUsage writes usage information to the given writer
|
// WriteUsage writes usage information to the given writer
|
||||||
@@ -70,7 +62,7 @@ func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) erro
|
|||||||
// writeUsageForSubcommand writes usage information for the given subcommand
|
// writeUsageForSubcommand writes usage information for the given subcommand
|
||||||
func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *command) {
|
func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *command) {
|
||||||
var positionals, longOptions, shortOptions []*spec
|
var positionals, longOptions, shortOptions []*spec
|
||||||
for _, spec := range cmd.specs {
|
for _, spec := range cmd.specs() {
|
||||||
switch {
|
switch {
|
||||||
case spec.positional:
|
case spec.positional:
|
||||||
positionals = append(positionals, spec)
|
positionals = append(positionals, spec)
|
||||||
@@ -216,24 +208,18 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
|
|||||||
|
|
||||||
// writeHelp writes the usage string for the given subcommand
|
// writeHelp writes the usage string for the given subcommand
|
||||||
func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
|
func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
|
||||||
var positionals, longOptions, shortOptions []*spec
|
|
||||||
for _, spec := range cmd.specs {
|
|
||||||
switch {
|
|
||||||
case spec.positional:
|
|
||||||
positionals = append(positionals, spec)
|
|
||||||
case spec.long != "":
|
|
||||||
longOptions = append(longOptions, spec)
|
|
||||||
case spec.short != "":
|
|
||||||
shortOptions = append(shortOptions, spec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.description != "" {
|
if p.description != "" {
|
||||||
fmt.Fprintln(w, p.description)
|
fmt.Fprintln(w, p.description)
|
||||||
}
|
}
|
||||||
p.writeUsageForSubcommand(w, cmd)
|
p.writeUsageForSubcommand(w, cmd)
|
||||||
|
|
||||||
// write the list of positionals
|
// write the list of positionals
|
||||||
|
var positionals []*spec
|
||||||
|
for _, spec := range cmd.options {
|
||||||
|
if spec.positional {
|
||||||
|
positionals = append(positionals, spec)
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(positionals) > 0 {
|
if len(positionals) > 0 {
|
||||||
fmt.Fprint(w, "\nPositional arguments:\n")
|
fmt.Fprint(w, "\nPositional arguments:\n")
|
||||||
for _, spec := range positionals {
|
for _, spec := range positionals {
|
||||||
@@ -242,26 +228,18 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// write the list of options with the short-only ones first to match the usage string
|
// 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 {
|
p.writeHelpForArguments(w, cmd, "Options", "")
|
||||||
fmt.Fprint(w, "\nOptions:\n")
|
|
||||||
for _, spec := range shortOptions {
|
|
||||||
p.printOption(w, spec)
|
|
||||||
}
|
|
||||||
for _, spec := range longOptions {
|
|
||||||
p.printOption(w, spec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// obtain a flattened list of options from all ancestors
|
// obtain a flattened list of options from all ancestors
|
||||||
var globals []*spec
|
var globals []*spec
|
||||||
ancestor := cmd.parent
|
ancestor := cmd.parent
|
||||||
for ancestor != nil {
|
for ancestor != nil {
|
||||||
globals = append(globals, ancestor.specs...)
|
globals = append(globals, ancestor.specs()...)
|
||||||
ancestor = ancestor.parent
|
ancestor = ancestor.parent
|
||||||
}
|
}
|
||||||
|
|
||||||
// write the list of global options
|
// write the list of global options
|
||||||
if len(globals) > 0 {
|
if len(globals) > 0 || len(cmd.groups) > 0 {
|
||||||
fmt.Fprint(w, "\nGlobal options:\n")
|
fmt.Fprint(w, "\nGlobal options:\n")
|
||||||
for _, spec := range globals {
|
for _, spec := range globals {
|
||||||
p.printOption(w, spec)
|
p.printOption(w, spec)
|
||||||
@@ -290,6 +268,54 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
|
|||||||
printTwoCols(w, subcmd.name, subcmd.help, "", "")
|
printTwoCols(w, subcmd.name, subcmd.help, "", "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.epilogue != "" {
|
||||||
|
fmt.Fprintln(w, "\n"+p.epilogue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeHelpForArguments writes the list of short, long, and environment-only
|
||||||
|
// options in order.
|
||||||
|
func (p *Parser) writeHelpForArguments(w io.Writer, cmd *command, header, help string) {
|
||||||
|
var positionals, longOptions, shortOptions, envOnly []*spec
|
||||||
|
for _, spec := range cmd.options {
|
||||||
|
switch {
|
||||||
|
case spec.positional:
|
||||||
|
positionals = append(positionals, spec)
|
||||||
|
case spec.long != "":
|
||||||
|
longOptions = append(longOptions, spec)
|
||||||
|
case spec.short != "":
|
||||||
|
shortOptions = append(shortOptions, spec)
|
||||||
|
case spec.env != "":
|
||||||
|
envOnly = append(envOnly, spec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.parent != nil && len(shortOptions)+len(longOptions)+len(envOnly) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the list of options with the short-only ones first to match the usage string
|
||||||
|
fmt.Fprintf(w, "\n%v:\n", header)
|
||||||
|
if help != "" {
|
||||||
|
fmt.Fprintf(w, "\n%v\n\n", help)
|
||||||
|
}
|
||||||
|
for _, spec := range shortOptions {
|
||||||
|
p.printOption(w, spec)
|
||||||
|
}
|
||||||
|
for _, spec := range longOptions {
|
||||||
|
p.printOption(w, spec)
|
||||||
|
}
|
||||||
|
for _, spec := range envOnly {
|
||||||
|
p.printOption(w, spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the list of argument groups
|
||||||
|
if len(cmd.groups) > 0 {
|
||||||
|
for _, grpCmd := range cmd.groups {
|
||||||
|
p.writeHelpForArguments(w, grpCmd, fmt.Sprintf("%s options", grpCmd.name), grpCmd.help)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) printOption(w io.Writer, spec *spec) {
|
func (p *Parser) printOption(w io.Writer, spec *spec) {
|
||||||
@@ -300,8 +326,11 @@ func (p *Parser) printOption(w io.Writer, spec *spec) {
|
|||||||
if spec.short != "" {
|
if spec.short != "" {
|
||||||
ways = append(ways, synopsis(spec, "-"+spec.short))
|
ways = append(ways, synopsis(spec, "-"+spec.short))
|
||||||
}
|
}
|
||||||
|
if spec.env != "" && len(ways) == 0 {
|
||||||
|
ways = append(ways, "(environment only)")
|
||||||
|
}
|
||||||
if len(ways) > 0 {
|
if len(ways) > 0 {
|
||||||
printTwoCols(w, strings.Join(ways, ", "), spec.help, spec.defaultVal, spec.env)
|
printTwoCols(w, strings.Join(ways, ", "), spec.help, spec.defaultString, spec.env)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+846
@@ -0,0 +1,846 @@
|
|||||||
|
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 [default: [3.14 42 256]]
|
||||||
|
--workers WORKERS, -w WORKERS
|
||||||
|
number of workers to start [default: 10, env: WORKERS]
|
||||||
|
--testenv TESTENV, -a TESTENV [env: TEST_ENV]
|
||||||
|
--file FILE, -f FILE File with mandatory extension [default: scratch.txt]
|
||||||
|
--help, -h display this help and exit
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Input string `arg:"positional,required"`
|
||||||
|
Output []string `arg:"positional" help:"list of outputs"`
|
||||||
|
Name string `help:"name to use"`
|
||||||
|
Value int `help:"secret value"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
args.Name = "Foo Bar"
|
||||||
|
args.Value = 42
|
||||||
|
args.Values = []float64{3.14, 42, 256}
|
||||||
|
args.File = &NameDotName{"scratch", "txt"}
|
||||||
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
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
|
||||||
|
Content string `default:"dog"`
|
||||||
|
}
|
||||||
|
args.Label = "cat"
|
||||||
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
|
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 TestUsageCannotMarshalToString(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
Name *MyEnum
|
||||||
|
}
|
||||||
|
v := MyEnum(42)
|
||||||
|
args.Name = &v
|
||||||
|
_, err := NewParser(Config{Program: "example"}, &args)
|
||||||
|
assert.EqualError(t, err, `args.Name: error marshaling default value to string: There was a problem`)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(Config{Program: "example"}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var help bytes.Buffer
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func 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(Config{Program: "example"}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var help bytes.Buffer
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageWithProgramName(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: myprogram"
|
||||||
|
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: myprogram
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--help, -h display this help and exit
|
||||||
|
`
|
||||||
|
config := Config{
|
||||||
|
Program: "myprogram",
|
||||||
|
}
|
||||||
|
p, err := NewParser(config, &struct{}{})
|
||||||
|
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
|
||||||
|
`
|
||||||
|
os.Args[0] = "example"
|
||||||
|
p, err := NewParser(Config{}, &versioned{})
|
||||||
|
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
|
||||||
|
`
|
||||||
|
os.Args[0] = "example"
|
||||||
|
p, err := NewParser(Config{}, &described{})
|
||||||
|
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(Config{}, &epilogued{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var help bytes.Buffer
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageForRequiredPositionals(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example REQUIRED1 REQUIRED2\n"
|
||||||
|
var args struct {
|
||||||
|
Required1 string `arg:"positional,required"`
|
||||||
|
Required2 string `arg:"positional,required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, usage.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageForMixedPositionals(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]\n"
|
||||||
|
var args struct {
|
||||||
|
Required1 string `arg:"positional,required"`
|
||||||
|
Required2 string `arg:"positional,required"`
|
||||||
|
Optional1 string `arg:"positional"`
|
||||||
|
Optional2 string `arg:"positional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, usage.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageForRepeatedPositionals(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]\n"
|
||||||
|
var args struct {
|
||||||
|
Required1 string `arg:"positional,required"`
|
||||||
|
Required2 string `arg:"positional,required"`
|
||||||
|
Repeated []string `arg:"positional,required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, usage.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageForMixedAndRepeatedPositionals(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2 [REPEATED [REPEATED ...]]]]\n"
|
||||||
|
var args struct {
|
||||||
|
Required1 string `arg:"positional,required"`
|
||||||
|
Required2 string `arg:"positional,required"`
|
||||||
|
Optional1 string `arg:"positional"`
|
||||||
|
Optional2 string `arg:"positional"`
|
||||||
|
Repeated []string `arg:"positional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, usage.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiredMultiplePositionals(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]\n"
|
||||||
|
|
||||||
|
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(Config{Program: "example"}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var help bytes.Buffer
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, 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(Config{}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_ = p.Parse([]string{"child", "nested", "value"})
|
||||||
|
|
||||||
|
var help bytes.Buffer
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
var help2 bytes.Buffer
|
||||||
|
p.WriteHelpForSubcommand(&help2, "child", "nested")
|
||||||
|
assert.Equal(t, expectedHelp[1:], help2.String())
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
|
||||||
|
var usage2 bytes.Buffer
|
||||||
|
p.WriteUsageForSubcommand(&usage2, "child", "nested")
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonexistentSubcommand(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
sub *struct{} `arg:"subcommand"`
|
||||||
|
}
|
||||||
|
p, err := NewParser(Config{}, &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 TestUsageWithOptionGroup(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example [--verbose] [--insecure] [--host HOST] [--port PORT] [--user USER] OUTPUT"
|
||||||
|
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example [--verbose] [--insecure] [--host HOST] [--port PORT] [--user USER] OUTPUT
|
||||||
|
|
||||||
|
Positional arguments:
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--verbose, -v verbosity level
|
||||||
|
|
||||||
|
Database options:
|
||||||
|
|
||||||
|
This block represents related arguments.
|
||||||
|
|
||||||
|
--insecure, -i disable tls
|
||||||
|
--host HOST hostname to connect to [default: localhost, env: DB_HOST]
|
||||||
|
--port PORT port to connect to [default: 3306, env: DB_PORT]
|
||||||
|
--user USER username to connect as [env: DB_USERNAME]
|
||||||
|
(environment only) password to connect with [env: DB_PASSWORD]
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
--help, -h display this help and exit
|
||||||
|
`
|
||||||
|
|
||||||
|
type database struct {
|
||||||
|
Insecure bool `arg:"-i,--insecure" help:"disable tls"`
|
||||||
|
Host string `arg:"--host,env:DB_HOST" help:"hostname to connect to" default:"localhost"`
|
||||||
|
Port string `arg:"--port,env:DB_PORT" help:"port to connect to" default:"3306"`
|
||||||
|
User string `arg:"--user,env:DB_USERNAME" help:"username to connect as"`
|
||||||
|
Password string `arg:"--,env:DB_PASSWORD" help:"password to connect with"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||||
|
Database *database `arg:"group" help:"This block represents related arguments."`
|
||||||
|
Output string `arg:"positional,required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Args[0] = "example"
|
||||||
|
p, err := NewParser(Config{}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_ = p.Parse([]string{})
|
||||||
|
|
||||||
|
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 TestUsageWithoutSubcommandAndOptionGroup(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example [-s] [--global] <command> [<args>]"
|
||||||
|
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example [-s] [--global] <command> [<args>]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--global, -g global option
|
||||||
|
|
||||||
|
Global group options:
|
||||||
|
|
||||||
|
This block represents related arguments.
|
||||||
|
|
||||||
|
-s global something
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
--help, -h display this help and exit
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
foo Command A
|
||||||
|
bar Command B
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Global bool `arg:"-g" help:"global option"`
|
||||||
|
GlobalGroup *struct {
|
||||||
|
Something bool `arg:"-s,--" help:"global something"`
|
||||||
|
} `arg:"group:Global group" help:"This block represents related arguments."`
|
||||||
|
CommandA *struct {
|
||||||
|
OptionA bool `arg:"-a,--" help:"option for sub A"`
|
||||||
|
GroupA *struct {
|
||||||
|
GroupA bool `arg:"--group-a" help:"group belonging to cmd A"`
|
||||||
|
} `arg:"group:Group A" help:"This block belongs to command A."`
|
||||||
|
} `arg:"subcommand:foo" help:"Command A"`
|
||||||
|
CommandB *struct {
|
||||||
|
OptionB bool `arg:"-b,--" help:"option for sub B"`
|
||||||
|
GroupB *struct {
|
||||||
|
GroupB bool `arg:"--group-b" help:"group belonging to cmd B"`
|
||||||
|
NestedGroup *struct {
|
||||||
|
NestedGroup bool `arg:"--nested-group" help:"nested group belonging to group B of cmd B"`
|
||||||
|
} `arg:"group:Nested Group" help:"This block belongs to group B of command B."`
|
||||||
|
} `arg:"group:Group B" help:"This block belongs to command B."`
|
||||||
|
} `arg:"subcommand:bar" help:"Command B"`
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Args[0] = "example"
|
||||||
|
p, err := NewParser(Config{}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_ = p.Parse([]string{})
|
||||||
|
|
||||||
|
var help bytes.Buffer
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
var help2 bytes.Buffer
|
||||||
|
p.WriteHelpForSubcommand(&help2)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help2.String())
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
|
||||||
|
var usage2 bytes.Buffer
|
||||||
|
p.WriteUsageForSubcommand(&usage2)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageWithSubcommandAndOptionGroup(t *testing.T) {
|
||||||
|
|
||||||
|
expectedUsage := "Usage: example bar [-b] [--group-b] [--nested-group]"
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example bar [-b] [--group-b] [--nested-group]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-b option for sub B
|
||||||
|
|
||||||
|
Group B options:
|
||||||
|
|
||||||
|
This block belongs to command B.
|
||||||
|
|
||||||
|
--group-b group belonging to cmd B
|
||||||
|
|
||||||
|
Nested Group options:
|
||||||
|
|
||||||
|
This block belongs to group B of command B.
|
||||||
|
|
||||||
|
--nested-group nested group belonging to group B of cmd B
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
--global, -g global option
|
||||||
|
-s global something
|
||||||
|
--help, -h display this help and exit
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Global bool `arg:"-g" help:"global option"`
|
||||||
|
GlobalGroup *struct {
|
||||||
|
Something bool `arg:"-s,--" help:"global something"`
|
||||||
|
} `arg:"group:Global group" help:"This block represents related arguments."`
|
||||||
|
CommandA *struct {
|
||||||
|
OptionA bool `arg:"-a,--" help:"option for sub A"`
|
||||||
|
GroupA *struct {
|
||||||
|
GroupA bool `arg:"--group-a" help:"group belonging to cmd A"`
|
||||||
|
} `arg:"group:Group A" help:"This block belongs to command A."`
|
||||||
|
} `arg:"subcommand:foo" help:"Command A"`
|
||||||
|
CommandB *struct {
|
||||||
|
OptionB bool `arg:"-b,--" help:"option for sub B"`
|
||||||
|
GroupB *struct {
|
||||||
|
GroupB bool `arg:"--group-b" help:"group belonging to cmd B"`
|
||||||
|
NestedGroup *struct {
|
||||||
|
NestedGroup bool `arg:"--nested-group" help:"nested group belonging to group B of cmd B"`
|
||||||
|
} `arg:"group:Nested Group" help:"This block belongs to group B of command B."`
|
||||||
|
} `arg:"group:Group B" help:"This block belongs to command B."`
|
||||||
|
} `arg:"subcommand:bar" help:"Command B"`
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Args[0] = "example"
|
||||||
|
p, err := NewParser(Config{}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_ = p.Parse([]string{"bar"})
|
||||||
|
|
||||||
|
var help bytes.Buffer
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
var help2 bytes.Buffer
|
||||||
|
p.WriteHelpForSubcommand(&help2, "bar")
|
||||||
|
assert.Equal(t, expectedHelp[1:], help2.String())
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
|
||||||
|
var usage2 bytes.Buffer
|
||||||
|
p.WriteUsageForSubcommand(&usage2, "bar")
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
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(Config{Program: "example"}, &args)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var help bytes.Buffer
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func 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(Config{Program: "example"}, &args)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var help bytes.Buffer
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageWithEnvOptions(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example [-s SHORT]"
|
||||||
|
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example [-s SHORT]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-s SHORT [env: SHORT]
|
||||||
|
(environment only) [env: ENVONLY]
|
||||||
|
(environment only) [env: CUSTOM]
|
||||||
|
--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(Config{Program: "example"}, &args)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var help bytes.Buffer
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFail(t *testing.T) {
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
var exitCode int
|
||||||
|
exit := func(code int) { exitCode = code }
|
||||||
|
|
||||||
|
expectedStdout := `
|
||||||
|
Usage: example [--foo FOO]
|
||||||
|
error: something went wrong
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Foo int
|
||||||
|
}
|
||||||
|
p, err := NewParser(Config{Program: "example", Exit: exit, Out: &stdout}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
p.Fail("something went wrong")
|
||||||
|
|
||||||
|
assert.Equal(t, expectedStdout[1:], stdout.String())
|
||||||
|
assert.Equal(t, -1, exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFailSubcommand(t *testing.T) {
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
var exitCode int
|
||||||
|
exit := func(code int) { exitCode = code }
|
||||||
|
|
||||||
|
expectedStdout := `
|
||||||
|
Usage: example sub
|
||||||
|
error: something went wrong
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Sub *struct{} `arg:"subcommand"`
|
||||||
|
}
|
||||||
|
p, err := NewParser(Config{Program: "example", Exit: exit, Out: &stdout}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = p.FailSubcommand("something went wrong", "sub")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, expectedStdout[1:], stdout.String())
|
||||||
|
assert.Equal(t, -1, exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
type lengthOf struct {
|
||||||
|
Length int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *lengthOf) UnmarshalText(b []byte) error {
|
||||||
|
p.Length = len(b)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelpShowsDefaultValueFromOriginalTag(t *testing.T) {
|
||||||
|
// check that the usage text prints the original string from the default tag, not
|
||||||
|
// the serialization of the parsed value
|
||||||
|
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example [--test TEST]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--test TEST [default: some_default_value]
|
||||||
|
--help, -h display this help and exit
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Test *lengthOf `default:"some_default_value"`
|
||||||
|
}
|
||||||
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var help bytes.Buffer
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
}
|
||||||
Reference in new issue
Block a user