Compare commits

...

51 Commits

Author SHA1 Message Date
Jeff Carr 119b334118 fix module path
Signed-off-by: Jeff Carr <jcarr@wit.com>
2024-01-14 10:55:20 -06:00
Jeff Carr b698745ef7 correct makefile
Signed-off-by: Jeff Carr <jcarr@wit.com>
2024-01-08 03:01:13 -06:00
Jeff Carr 625ef41f20 go.mod for 'go.wit.com/arg'
Signed-off-by: Jeff Carr <jcarr@wit.com>
2024-01-02 05:32:40 -06:00
Jeff Carr 2f59cd70aa screw github
Signed-off-by: Jeff Carr <jcarr@wit.com>
2024-01-02 05:21:25 -06:00
Jeff Carr f9208bdffb add Makefile
Signed-off-by: Jeff Carr <jcarr@wit.com>
2024-01-01 19:28:06 -06:00
Jeff Carr 42d240c6c1 add Register() 2024-01-01 19:22:04 -06:00
Alex Flint bf629a16cb
Merge pull request #231 from alexflint/subcommand-aliases
add subcommand aliases
2023-10-10 18:36:46 -04:00
Alex Flint e7a4f77ed0 add a unittest for an internally messed up subcommand path 2023-10-08 20:24:18 -04:00
Alex Flint 960d38c3ce add some more tests for subcommand aliases 2023-10-08 20:14:34 -04:00
Alex Flint 0142b0b842 add subcommand aliases 2023-10-08 20:09:05 -04:00
Alex Flint 5ec29ce755
Merge pull request #229 from alexflint/dependabot/go_modules/gopkg.in/yaml.v3-3.0.0
Bump gopkg.in/yaml.v3 from 3.0.0-20200313102051-9f266ea9e77c to 3.0.0
2023-09-10 15:05:06 -07:00
dependabot[bot] 8e9f60aafc
Bump gopkg.in/yaml.v3 from 3.0.0-20200313102051-9f266ea9e77c to 3.0.0
Bumps gopkg.in/yaml.v3 from 3.0.0-20200313102051-9f266ea9e77c to 3.0.0.

---
updated-dependencies:
- dependency-name: gopkg.in/yaml.v3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-30 13:58:31 +00:00
Alex Flint 660b9045e1
Merge pull request #223 from hhromic/fix-version-flag
Improve handling of version flag
2023-07-14 15:52:33 -04:00
Hugo Hromic c73f38cd54
Improve handling of version flag
* Only use/show builtin `--version` flag if args are versioned with a non-empty `Version()`
* If args define a `--version` flag, honor it and disable/hide the builtin version flag
* Only return `ErrVersion` when using the builtin version flag
2023-07-14 20:12:52 +01:00
Alex Flint 463902ef7d
Merge pull request #222 from IljaN/env-only-args
Support for parameters which can only be passed via env
2023-07-02 10:07:10 -04:00
Ilja Neumann 259c83fd5a Remove usage of additional envOnly struct variable 2023-06-29 21:26:34 +02:00
Ilja Neumann 18623d869b help,usage and error messages and tests 2023-06-03 12:47:47 +02:00
Ilja Neumann b928a1839a Parse env-only vars 2023-06-03 09:50:42 +02:00
Ilja Neumann ccf62e0ffc don't print env-vars in usage line 2023-06-03 03:33:10 +02:00
Pablo Diaz 5f10667949 fixed tests 2023-06-03 02:39:56 +02:00
Pablo Diaz c3cac76438 added tests and fixed usage 2023-06-03 02:39:56 +02:00
Pablo Diaz 0280e6e591 ignores short and long parameters 2023-06-03 02:39:42 +02:00
Alex Flint e25b4707a7
Merge pull request #211 from alexflint/clean-up-osexit-stderr-stdout
clean up customizable stdout, stderr, and exit in parser config
2023-02-08 06:56:56 -08:00
Alex Flint df28e7154b clean up customizable stdout, stderr, and exit in parser config 2023-02-08 09:49:03 -05:00
Alex Flint 5dbdd5d0c5
Merge pull request #210 from cabuda/master
feat: support more env than terminal
2023-02-08 06:13:50 -08:00
duxinlong efae1938fd feat: support more env than terminal
Change-Id: I7f35e90b8f19f4ea781832885d35e2f1e275207a
2023-02-08 12:01:48 +00:00
Alex Flint c0a8e20a0a
Merge pull request #205 from dmzkrsk/strict-subgroup-parsing
add strict subcommand parsing
2023-01-27 08:35:12 -08:00
Alexey Trofimov 5036dce2d6 fix typo 2023-01-18 11:52:13 +03:00
Alexey Trofimov cef66fd2f6 add strict subcommand parsing 2023-01-18 11:50:50 +03:00
Alex Flint 727f8533ac
Merge pull request #185 from alexflint/default-value-issue
Do not turn values intro strings and then back into values when processing default values
2022-10-29 12:29:07 -07:00
Alex Flint 3489ea5b2e in a second place: use reflect.Ptr not reflect.Pointer since the latter was added in Go 1.18 2022-10-29 15:23:56 -04:00
Alex Flint 763072452f use reflect.Ptr not reflect.Pointer since the latter was added in Go 1.18 2022-10-29 15:21:21 -04:00
Alex Flint 3d95a706a6 Merge remote-tracking branch 'origin/master' into default-value-issue 2022-10-29 15:19:23 -04:00
Alex Flint d949871b67 add further comment about backwards-compatible method for setting default values 2022-10-29 15:13:57 -04:00
Alex Flint 9d5e97ac8a drop unnecessary test 2022-10-29 15:12:53 -04:00
Alex Flint 67f7183b85 remove unused textMarshalerType and isTextMarshaler 2022-10-29 15:10:11 -04:00
Alex Flint 522dbbcea8 add test for the new default value parsing logic as it shows up in help messages 2022-10-29 15:08:48 -04:00
Alex Flint 27c832b934 store both a default value and a string representation of that default value in the spec for each option 2022-10-29 14:47:13 -04:00
Alex Flint 197e226c77 drop unnecessary use of templates in this test 2022-10-29 12:28:06 -04:00
Alex Flint dbc2ba5d0c
Merge pull request #198 from daenney/mustparse
Implement MustParse on Parser
2022-10-10 08:41:57 -07:00
Daniele Sluijters 4fc9666f79 Implement MustParse on Parse
This moves most of the body of the MustParse function into a MustParse
method on a Parser. The MustParse function is now implemented by calling
the MustParse function on the Parser it implicitly creates.

Closes: #194
2022-10-10 17:25:14 +02:00
Alex Flint 11f9b624a9
Merge pull request #196 from alexflint/bump_go_versions
Update to latest version of Go in CI
2022-10-02 13:17:43 -07:00
Alex Flint 7f4979a06e update to latest 3 versions of Go for CI 2022-10-02 13:16:32 -07:00
Alex Flint 0c21f821f8
Merge pull request #195 from alexflint/bump_scalar_dep
Update to latest go-scalar
2022-10-02 13:07:36 -07:00
Alex Flint ea0f540c40 update to latest go-scalar, add test for hex, oct, and binary integer literals 2022-10-02 13:05:04 -07:00
Alex Flint 74af96c6cc
Merge pull request #191 from SebastiaanPasterkamp/add-epilog-to-help
Feat: Add epilogue after help text
2022-09-27 12:48:05 -07:00
Sebastiaan Pasterkamp c8b9567d1b Feat: Add epilog after help text
Similar to the Description at the top of the
help text an Epilog is added at the bottom.

Resolves #189
2022-09-17 12:55:00 +02:00
Alex Flint ebd7a68a06
Merge pull request #172 from SebastiaanPasterkamp/ignore-default-option
Add 'IgnoreDefault' option
2022-06-11 09:06:03 -04:00
Alex Flint 23b2b67fe2 fix issue #184 2022-06-09 11:21:29 -04:00
Sebastiaan Pasterkamp b48371a62f Simplify sub-command initialization w/o IgnoreDefault 2022-06-05 17:54:46 +02:00
Sebastiaan Pasterkamp a87d80089a Add 'IgnoreDefault' option 2022-01-02 15:17:09 +01:00
13 changed files with 1068 additions and 310 deletions

View File

@ -15,17 +15,17 @@ jobs:
strategy:
fail-fast: false
matrix:
go: ['1.13', '1.14', '1.15', '1.16']
go: ['1.17', '1.18', '1.19']
steps:
- id: go
name: Set up Go
uses: actions/setup-go@v1
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go }}
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Build
run: go build -v .

23
Makefile Normal file
View File

@ -0,0 +1,23 @@
# git remote add gitwit git@git.wit.org:wit/arg.git
redomod:
rm -f go.*
GO111MODULE= go mod init
GO111MODULE= go mod tidy
github:
git push origin register
git push origin devel
git push origin jcarr
git push origin --tags
# git push github register
# git push github devel
# git push github --tags
@echo
@echo check https://git.wit.org/wit/arg
@echo
# init-github:
# git push -u github master
# git push -u github devel
# git push github --tags

View File

@ -134,10 +134,10 @@ arg.MustParse(&args)
```shell
$ ./example -h
Usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]]
Usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]]
Positional arguments:
INPUT
INPUT
OUTPUT
Options:
@ -180,6 +180,24 @@ var args struct {
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
```go
var args struct {
@ -444,6 +462,9 @@ Options:
### Description strings
A descriptive message can be added at the top of the help text by implementing
a `Description` function that returns a string.
```go
type args struct {
Foo string
@ -469,6 +490,35 @@ Options:
--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
*Introduced in version 1.1.0*

View File

@ -162,8 +162,7 @@ func Example_helpText() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stdout = os.Stdout
mustParseExit = func(int) {}
MustParse(&args)
@ -195,8 +194,7 @@ func Example_helpPlaceholder() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stdout = os.Stdout
mustParseExit = func(int) {}
MustParse(&args)
@ -236,8 +234,7 @@ func Example_helpTextWithSubcommand() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stdout = os.Stdout
mustParseExit = func(int) {}
MustParse(&args)
@ -274,8 +271,7 @@ func Example_helpTextWhenUsingSubcommand() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stdout = os.Stdout
mustParseExit = func(int) {}
MustParse(&args)
@ -311,10 +307,9 @@ func Example_writeHelpForSubcommand() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stdout = os.Stdout
exit := func(int) {}
p, err := NewParser(Config{}, &args)
p, err := NewParser(Config{Exit: exit}, &args)
if err != nil {
fmt.Println(err)
os.Exit(1)
@ -360,10 +355,9 @@ func Example_writeHelpForSubcommandNested() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stdout = os.Stdout
exit := func(int) {}
p, err := NewParser(Config{}, &args)
p, err := NewParser(Config{Exit: exit}, &args)
if err != nil {
fmt.Println(err)
os.Exit(1)
@ -397,8 +391,7 @@ func Example_errorText() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stderr = os.Stdout
mustParseExit = func(int) {}
MustParse(&args)
@ -421,8 +414,7 @@ func Example_errorTextForSubcommand() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stderr = os.Stdout
mustParseExit = func(int) {}
MustParse(&args)
@ -457,8 +449,7 @@ func Example_subcommand() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stderr = os.Stdout
mustParseExit = func(int) {}
MustParse(&args)
@ -505,3 +496,45 @@ func Example_allSupportedTypes() {
// output:
}
func Example_envVarOnly() {
os.Args = split("./example")
_ = os.Setenv("AUTH_KEY", "my_key")
defer os.Unsetenv("AUTH_KEY")
var args struct {
AuthKey string `arg:"--,env:AUTH_KEY"`
}
MustParse(&args)
fmt.Println(args.AuthKey)
// output: my_key
}
func Example_envVarOnlyShouldIgnoreFlag() {
os.Args = split("./example --=my_key")
var args struct {
AuthKey string `arg:"--,env:AUTH_KEY"`
}
err := Parse(&args)
fmt.Println(err)
// output: unknown argument --=my_key
}
func Example_envVarOnlyShouldIgnoreShortFlag() {
os.Args = split("./example -=my_key")
var args struct {
AuthKey string `arg:"--,env:AUTH_KEY"`
}
err := Parse(&args)
fmt.Println(err)
// output: unknown argument -=my_key
}

14
go.mod
View File

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

14
go.sum
View File

@ -1,15 +1,13 @@
github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM=
github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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.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=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

304
parse.go
View File

@ -5,6 +5,7 @@ import (
"encoding/csv"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
@ -43,23 +44,25 @@ func (p path) Child(f reflect.StructField) path {
// spec represents a command line option
type spec struct {
dest path
field reflect.StructField // the struct field from which this option was created
long string // the --long form for this option, or empty if none
short string // the -s short form for this option, or empty if none
cardinality cardinality // determines how many tokens will be present (possible values: zero, one, multiple)
required bool // if true, this option must be present on the command line
positional bool // if true, this option will be looked for in the positional flags
separate bool // if true, each slice and map entry will have its own --flag
help string // the help text for this option
env string // the name of the environment variable for this option, or empty for none
defaultVal string // default value for this option
placeholder string // name of the data in help
dest path
field reflect.StructField // the struct field from which this option was created
long string // the --long form for this option, or empty if none
short string // the -s short form for this option, or empty if none
cardinality cardinality // determines how many tokens will be present (possible values: zero, one, multiple)
required bool // if true, this option must be present on the command line
positional bool // if true, this option will be looked for in the positional flags
separate bool // if true, each slice and map entry will have its own --flag
help string // the help text for this option
env string // the name of the environment variable for this option, or empty for none
defaultValue reflect.Value // default value for this option
defaultString string // default value for this option, in string form to be displayed in help text
placeholder string // name of the data in help
}
// command represents a named subcommand, or the top-level command
type command struct {
name string
aliases []string
help string
dest path
specs []*spec
@ -67,33 +70,55 @@ type command struct {
parent *command
}
// ErrHelp indicates that -h or --help were provided
// ErrHelp indicates that the builtin -h or --help were provided
var ErrHelp = errors.New("help requested by user")
// ErrVersion indicates that --version was provided
// ErrVersion indicates that the builtin --version was provided
var ErrVersion = errors.New("version requested by user")
// for monkey patching in example code
var mustParseExit = os.Exit
// This stores the args sent from modules
var register []interface{}
/*
Use this in your packages to register
variables with go-arg. Then add this to your init()
package 'foo'
function init() {
args.Register(&argsFoo)
}
*/
func Register(dest ...interface{}) {
register = append(register, dest...)
}
// MustParse processes command line arguments and exits upon failure
func MustParse(dest ...interface{}) *Parser {
p, err := NewParser(Config{}, dest...)
register = append(register, dest...)
return mustParse(Config{Exit: mustParseExit}, register...)
}
// mustParse is a helper that facilitates testing
func mustParse(config Config, dest ...interface{}) *Parser {
if config.Exit == nil {
config.Exit = os.Exit
}
if config.Out == nil {
config.Out = os.Stdout
}
p, err := NewParser(config, dest...)
if err != nil {
fmt.Fprintln(stdout, err)
osExit(-1)
return nil // just in case osExit was monkey-patched
}
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)
fmt.Fprintln(config.Out, err)
config.Exit(-1)
return nil
}
p.MustParse(flags())
return p
}
@ -121,6 +146,20 @@ type Config struct {
// IgnoreEnv instructs the library not to read environment variables
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
@ -130,9 +169,10 @@ type Parser struct {
config Config
version string
description string
epilogue string
// the following field changes during processing of command line arguments
lastCmd *command
subcommand []string
}
// Versioned is the interface that the destination struct should implement to
@ -151,6 +191,14 @@ type Described interface {
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.
func walkFields(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool) {
walkFieldsImpl(t, visit, nil)
@ -174,6 +222,14 @@ func walkFieldsImpl(t reflect.Type, visit func(field reflect.StructField, owner
// NewParser constructs a parser from a list of destination structs
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
var name string
switch {
@ -208,18 +264,31 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
return nil, err
}
// add nonzero field values as defaults
// for backwards compatibility, add nonzero field values as defaults
// this applies only to the top-level command, not to subcommands (this inconsistency
// is the reason that this method for setting default values was deprecated)
for _, spec := range cmd.specs {
if v := p.val(spec.dest); v.IsValid() && !isZero(v) {
if defaultVal, ok := v.Interface().(encoding.TextMarshaler); ok {
str, err := defaultVal.MarshalText()
if err != nil {
return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
}
spec.defaultVal = string(str)
} else {
spec.defaultVal = fmt.Sprintf("%v", v)
// get the value
v := p.val(spec.dest)
// if the value is the "zero value" (e.g. nil pointer, empty struct) then ignore
if isZero(v) {
continue
}
// store as a default
spec.defaultValue = v
// we need a string to display in help text
// if MarshalText is implemented then use that
if m, ok := v.Interface().(encoding.TextMarshaler); ok {
s, err := m.MarshalText()
if err != nil {
return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
}
spec.defaultString = string(s)
} else {
spec.defaultString = fmt.Sprintf("%v", v)
}
}
@ -232,6 +301,9 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
if dest, ok := dest.(Described); ok {
p.description = dest.Description()
}
if dest, ok := dest.(Epilogued); ok {
p.epilogue = dest.Epilogue()
}
}
return &p, nil
@ -288,13 +360,9 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
spec.help = help
}
defaultVal, hasDefault := field.Tag.Lookup("default")
if hasDefault {
spec.defaultVal = defaultVal
}
// Look at the tag
var isSubcommand bool // tracks whether this field is a subcommand
for _, key := range strings.Split(tag, ",") {
if key == "" {
continue
@ -312,18 +380,13 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
case strings.HasPrefix(key, "--"):
spec.long = key[2:]
case strings.HasPrefix(key, "-"):
if len(key) != 2 {
if len(key) > 2 {
errs = append(errs, fmt.Sprintf("%s.%s: short arguments must be one character only",
t.Name(), field.Name))
return false
}
spec.short = key[1:]
case key == "required":
if hasDefault {
errs = append(errs, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified",
t.Name(), field.Name))
return false
}
spec.required = true
case key == "positional":
spec.positional = true
@ -340,18 +403,24 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
}
case key == "subcommand":
// decide on a name for the subcommand
cmdname := value
if cmdname == "" {
cmdname = strings.ToLower(field.Name)
var cmdnames []string
if value == "" {
cmdnames = []string{strings.ToLower(field.Name)}
} else {
cmdnames = strings.Split(value, "|")
}
for i := range cmdnames {
cmdnames[i] = strings.TrimSpace(cmdnames[i])
}
// parse the subcommand recursively
subcmd, err := cmdFromStruct(cmdname, subdest, field.Type)
subcmd, err := cmdFromStruct(cmdnames[0], subdest, field.Type)
if err != nil {
errs = append(errs, err.Error())
return false
}
subcmd.aliases = cmdnames[1:]
subcmd.parent = &cmd
subcmd.help = field.Tag.Get("help")
@ -372,27 +441,60 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
spec.placeholder = strings.ToUpper(spec.field.Name)
}
// Check whether this field is supported. It's good to do this here rather than
// if this is a subcommand then we've done everything we need to do
if isSubcommand {
return false
}
// check whether this field is supported. It's good to do this here rather than
// wait until ParseValue because it means that a program with invalid argument
// fields will always fail regardless of whether the arguments it received
// exercised those fields.
if !isSubcommand {
cmd.specs = append(cmd.specs, &spec)
var err error
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
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
}
if spec.cardinality == multiple && hasDefault {
defaultString, hasDefault := field.Tag.Lookup("default")
if hasDefault {
// we do not support default values for maps and slices
if spec.cardinality == multiple {
errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields",
t.Name(), field.Name))
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.specs = append(cmd.specs, &spec)
// if this was an embedded field then we already returned true up above
return false
})
@ -433,6 +535,20 @@ func (p *Parser) Parse(args []string) error {
return err
}
func (p *Parser) MustParse(args []string) {
err := p.Parse(args)
switch {
case err == ErrHelp:
p.WriteHelpForSubcommand(p.config.Out, p.subcommand...)
p.config.Exit(0)
case err == ErrVersion:
fmt.Fprintln(p.config.Out, p.version)
p.config.Exit(0)
case err != nil:
p.FailSubcommand(err.Error(), p.subcommand...)
}
}
// process environment vars for the given arguments
func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error {
for _, spec := range specs {
@ -486,7 +602,7 @@ func (p *Parser) process(args []string) error {
// union of specs for the chain of subcommands encountered so far
curCmd := p.cmd
p.lastCmd = curCmd
p.subcommand = nil
// 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))
@ -500,6 +616,15 @@ func (p *Parser) process(args []string) error {
}
}
// determine if the current command has a version option spec
var hasVersionOption bool
for _, spec := range curCmd.specs {
if spec.long == "version" {
hasVersionOption = true
break
}
}
// process each string from the command line
var allpositional bool
var positionals []string
@ -527,10 +652,17 @@ func (p *Parser) process(args []string) error {
// instantiate the field to point to a new struct
v := p.val(subcmd.dest)
v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers
}
// add the new options to the set of allowed options
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
if !p.config.IgnoreEnv {
@ -541,7 +673,7 @@ func (p *Parser) process(args []string) error {
}
curCmd = subcmd
p.lastCmd = curCmd
p.subcommand = append(p.subcommand, arg)
continue
}
@ -550,7 +682,9 @@ func (p *Parser) process(args []string) error {
case "-h", "--help":
return ErrHelp
case "--version":
return ErrVersion
if !hasVersionOption && p.version != "" {
return ErrVersion
}
}
// check for an equals sign, as in "--foo=bar"
@ -564,7 +698,7 @@ func (p *Parser) process(args []string) error {
// lookup the spec for this option (note that the "specs" slice changes as
// we expand subcommands so it is better not to use a map)
spec := findOption(specs, opt)
if spec == nil {
if spec == nil || opt == "" {
return fmt.Errorf("unknown argument %s", arg)
}
wasPresent[spec] = true
@ -653,17 +787,26 @@ func (p *Parser) process(args []string) error {
}
if spec.required {
if spec.short == "" && spec.long == "" {
msg := fmt.Sprintf("environment variable %s is required", spec.env)
return errors.New(msg)
}
msg := fmt.Sprintf("%s is required", name)
if spec.env != "" {
msg += " (or environment variable " + spec.env + ")"
}
return errors.New(msg)
}
if spec.defaultVal != "" {
err := scalar.ParseValue(p.val(spec.dest), spec.defaultVal)
if err != nil {
return fmt.Errorf("error processing default value for %s: %v", name, err)
}
if spec.defaultValue.IsValid() && !p.config.IgnoreDefault {
// One issue here is that if the user now modifies the value then
// 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)
}
}
@ -724,6 +867,11 @@ func findSubcommand(cmds []*command, name string) *command {
if cmd.name == name {
return cmd
}
for _, alias := range cmd.aliases {
if alias == name {
return cmd
}
}
}
return nil
}

View File

@ -2,6 +2,7 @@ package arg
import (
"bytes"
"encoding/json"
"fmt"
"net"
"net/mail"
@ -95,6 +96,21 @@ func TestInt(t *testing.T) {
assert.EqualValues(t, 8, *args.Ptr)
}
func TestHexOctBin(t *testing.T) {
var args struct {
Hex int
Oct int
Bin int
Underscored int
}
err := parse("--hex 0xA --oct 0o10 --bin 0b101 --underscored 123_456", &args)
require.NoError(t, err)
assert.EqualValues(t, 10, args.Hex)
assert.EqualValues(t, 8, args.Oct)
assert.EqualValues(t, 5, args.Bin)
assert.EqualValues(t, 123456, args.Underscored)
}
func TestNegativeInt(t *testing.T) {
var args struct {
Foo int
@ -211,6 +227,14 @@ func TestRequiredWithEnv(t *testing.T) {
require.Error(t, err, "--foo is required (or environment variable FOO)")
}
func TestRequiredWithEnvOnly(t *testing.T) {
var args struct {
Foo string `arg:"required,--,-,env:FOO"`
}
_, err := parseWithEnv("", []string{}, &args)
require.Error(t, err, "environment variable FOO is required")
}
func TestShortFlag(t *testing.T) {
var args struct {
Foo string `arg:"-f"`
@ -816,6 +840,37 @@ func TestEnvironmentVariableIgnored(t *testing.T) {
assert.Equal(t, "", args.Foo)
}
func TestDefaultValuesIgnored(t *testing.T) {
var args struct {
Foo string `default:"bad"`
}
p, err := NewParser(Config{IgnoreDefault: true}, &args)
require.NoError(t, err)
err = p.Parse(nil)
assert.NoError(t, err)
assert.Equal(t, "", args.Foo)
}
func TestRequiredEnvironmentOnlyVariableIsMissing(t *testing.T) {
var args struct {
Foo string `arg:"required,--,env:FOO"`
}
_, err := parseWithEnv("", []string{""}, &args)
assert.Error(t, err)
}
func TestOptionalEnvironmentOnlyVariable(t *testing.T) {
var args struct {
Foo string `arg:"env:FOO"`
}
_, err := parseWithEnv("", []string{}, &args)
assert.NoError(t, err)
}
func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
var args struct {
Sub *struct {
@ -828,10 +883,51 @@ func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
require.NoError(t, err)
err = p.Parse([]string{"sub"})
assert.NoError(t, err)
require.NoError(t, err)
require.NotNil(t, args.Sub)
assert.Equal(t, "", args.Sub.Foo)
}
func TestParserMustParseEmptyArgs(t *testing.T) {
// this mirrors TestEmptyArgs
p, err := NewParser(Config{}, &struct{}{})
require.NoError(t, err)
assert.NotNil(t, p)
p.MustParse(nil)
}
func TestParserMustParse(t *testing.T) {
tests := []struct {
name string
args versioned
cmdLine []string
code int
output string
}{
{name: "help", args: struct{}{}, cmdLine: []string{"--help"}, code: 0, output: "display this help and exit"},
{name: "version", args: versioned{}, cmdLine: []string{"--version"}, code: 0, output: "example 3.2.1"},
{name: "invalid", args: struct{}{}, cmdLine: []string{"invalid"}, code: -1, output: ""},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
var exitCode int
var stdout bytes.Buffer
exit := func(code int) { exitCode = code }
p, err := NewParser(Config{Exit: exit, Out: &stdout}, &tt.args)
require.NoError(t, err)
assert.NotNil(t, p)
p.MustParse(tt.cmdLine)
assert.NotNil(t, exitCode)
assert.Equal(t, tt.code, exitCode)
assert.Contains(t, stdout.String(), tt.output)
})
}
}
type textUnmarshaler struct {
val int
}
@ -1285,11 +1381,55 @@ func TestReuseParser(t *testing.T) {
assert.Error(t, err)
}
func TestVersion(t *testing.T) {
func TestNoVersion(t *testing.T) {
var args struct{}
err := parse("--version", &args)
assert.Equal(t, ErrVersion, err)
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
err = p.Parse([]string{"--version"})
assert.Error(t, err)
assert.NotEqual(t, ErrVersion, err)
}
func TestBuiltinVersion(t *testing.T) {
var args struct{}
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
p.version = "example 3.2.1"
err = p.Parse([]string{"--version"})
assert.Equal(t, ErrVersion, err)
}
func TestArgsVersion(t *testing.T) {
var args struct {
Version bool `arg:"--version"`
}
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
err = p.Parse([]string{"--version"})
require.NoError(t, err)
require.Equal(t, args.Version, true)
}
func TestArgsAndBuiltinVersion(t *testing.T) {
var args struct {
Version bool `arg:"--version"`
}
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
p.version = "example 3.2.1"
err = p.Parse([]string{"--version"})
require.NoError(t, err)
require.Equal(t, args.Version, true)
}
func TestMultipleTerminates(t *testing.T) {
@ -1320,13 +1460,21 @@ func TestDefaultOptionValues(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 123, args.A)
assert.Equal(t, 123, *args.B)
if assert.NotNil(t, args.B) {
assert.Equal(t, 123, *args.B)
}
assert.Equal(t, "xyz", args.C)
assert.Equal(t, "abc", *args.D)
if assert.NotNil(t, args.D) {
assert.Equal(t, "abc", *args.D)
}
assert.Equal(t, 4.56, args.E)
assert.Equal(t, 1.23, *args.F)
assert.True(t, args.G)
if assert.NotNil(t, args.F) {
assert.Equal(t, 1.23, *args.F)
}
assert.True(t, args.G)
if assert.NotNil(t, args.H) {
assert.True(t, *args.H)
}
}
func TestDefaultUnparseable(t *testing.T) {
@ -1335,7 +1483,7 @@ func TestDefaultUnparseable(t *testing.T) {
}
err := parse("", &args)
assert.EqualError(t, err, `error processing default value for --a: strconv.ParseInt: parsing "x": invalid syntax`)
assert.EqualError(t, err, `.A: error processing default value: strconv.ParseInt: parsing "x": invalid syntax`)
}
func TestDefaultPositionalValues(t *testing.T) {
@ -1354,13 +1502,21 @@ func TestDefaultPositionalValues(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 456, args.A)
assert.Equal(t, 789, *args.B)
if assert.NotNil(t, args.B) {
assert.Equal(t, 789, *args.B)
}
assert.Equal(t, "abc", args.C)
assert.Equal(t, "abc", *args.D)
if assert.NotNil(t, args.D) {
assert.Equal(t, "abc", *args.D)
}
assert.Equal(t, 1.23, args.E)
assert.Equal(t, 1.23, *args.F)
assert.True(t, args.G)
if assert.NotNil(t, args.F) {
assert.Equal(t, 1.23, *args.F)
}
assert.True(t, args.G)
if assert.NotNil(t, args.H) {
assert.True(t, *args.H)
}
}
func TestDefaultValuesNotAllowedWithRequired(t *testing.T) {
@ -1374,7 +1530,7 @@ func TestDefaultValuesNotAllowedWithRequired(t *testing.T) {
func TestDefaultValuesNotAllowedWithSlice(t *testing.T) {
var args struct {
A []int `default:"123"` // required not allowed with default!
A []int `default:"invalid"` // default values not allowed with slices
}
err := parse("", &args)
@ -1391,68 +1547,193 @@ func TestUnexportedFieldsSkipped(t *testing.T) {
}
func TestMustParseInvalidParser(t *testing.T) {
originalExit := osExit
originalStdout := stdout
defer func() {
osExit = originalExit
stdout = originalStdout
}()
var exitCode int
osExit = func(code int) { exitCode = code }
stdout = &bytes.Buffer{}
var stdout bytes.Buffer
exit := func(code int) { exitCode = code }
var args struct {
CannotParse struct{}
}
parser := MustParse(&args)
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
assert.Nil(t, parser)
assert.Equal(t, -1, exitCode)
}
func TestMustParsePrintsHelp(t *testing.T) {
originalExit := osExit
originalStdout := stdout
originalArgs := os.Args
defer func() {
osExit = originalExit
stdout = originalStdout
os.Args = originalArgs
}()
var exitCode *int
osExit = func(code int) { exitCode = &code }
os.Args = []string{"someprogram", "--help"}
stdout = &bytes.Buffer{}
var exitCode int
var stdout bytes.Buffer
exit := func(code int) { exitCode = code }
var args struct{}
parser := MustParse(&args)
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
assert.NotNil(t, parser)
require.NotNil(t, exitCode)
assert.Equal(t, 0, *exitCode)
assert.Equal(t, 0, exitCode)
}
func TestMustParsePrintsVersion(t *testing.T) {
originalExit := osExit
originalStdout := stdout
originalArgs := os.Args
defer func() {
osExit = originalExit
stdout = originalStdout
os.Args = originalArgs
}()
var exitCode *int
osExit = func(code int) { exitCode = &code }
var exitCode int
var stdout bytes.Buffer
exit := func(code int) { exitCode = code }
os.Args = []string{"someprogram", "--version"}
var b bytes.Buffer
stdout = &b
var args versioned
parser := MustParse(&args)
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
require.NotNil(t, parser)
require.NotNil(t, exitCode)
assert.Equal(t, 0, *exitCode)
assert.Equal(t, "example 3.2.1\n", b.String())
assert.Equal(t, 0, exitCode)
assert.Equal(t, "example 3.2.1\n", stdout.String())
}
type mapWithUnmarshalText struct {
val map[string]string
}
func (v *mapWithUnmarshalText) UnmarshalText(data []byte) error {
return json.Unmarshal(data, &v.val)
}
func TestTextUnmarshalerEmpty(t *testing.T) {
// based on https://github.com/alexflint/go-arg/issues/184
var args struct {
Config mapWithUnmarshalText `arg:"--config"`
}
err := parse("", &args)
require.NoError(t, err)
assert.Empty(t, args.Config)
}
func TestTextUnmarshalerEmptyPointer(t *testing.T) {
// a slight variant on https://github.com/alexflint/go-arg/issues/184
var args struct {
Config *mapWithUnmarshalText `arg:"--config"`
}
err := parse("", &args)
require.NoError(t, err)
assert.Nil(t, args.Config)
}
// similar to the above but also implements MarshalText
type mapWithMarshalText struct {
val map[string]string
}
func (v *mapWithMarshalText) MarshalText(data []byte) error {
return json.Unmarshal(data, &v.val)
}
func (v *mapWithMarshalText) UnmarshalText(data []byte) error {
return json.Unmarshal(data, &v.val)
}
func TestTextMarshalerUnmarshalerEmpty(t *testing.T) {
// based on https://github.com/alexflint/go-arg/issues/184
var args struct {
Config mapWithMarshalText `arg:"--config"`
}
err := parse("", &args)
require.NoError(t, err)
assert.Empty(t, args.Config)
}
func TestTextMarshalerUnmarshalerEmptyPointer(t *testing.T) {
// a slight variant on https://github.com/alexflint/go-arg/issues/184
var args struct {
Config *mapWithMarshalText `arg:"--config"`
}
err := parse("", &args)
require.NoError(t, err)
assert.Nil(t, args.Config)
}
func TestSubcommandGlobalFlag_Before(t *testing.T) {
var args struct {
Global bool `arg:"-g"`
Sub *struct {
} `arg:"subcommand"`
}
p, err := NewParser(Config{StrictSubcommands: false}, &args)
require.NoError(t, err)
err = p.Parse([]string{"-g", "sub"})
assert.NoError(t, err)
assert.True(t, args.Global)
}
func TestSubcommandGlobalFlag_InCommand(t *testing.T) {
var args struct {
Global bool `arg:"-g"`
Sub *struct {
} `arg:"subcommand"`
}
p, err := NewParser(Config{StrictSubcommands: false}, &args)
require.NoError(t, err)
err = p.Parse([]string{"sub", "-g"})
assert.NoError(t, err)
assert.True(t, args.Global)
}
func TestSubcommandGlobalFlag_Before_Strict(t *testing.T) {
var args struct {
Global bool `arg:"-g"`
Sub *struct {
} `arg:"subcommand"`
}
p, err := NewParser(Config{StrictSubcommands: true}, &args)
require.NoError(t, err)
err = p.Parse([]string{"-g", "sub"})
assert.NoError(t, err)
assert.True(t, args.Global)
}
func TestSubcommandGlobalFlag_InCommand_Strict(t *testing.T) {
var args struct {
Global bool `arg:"-g"`
Sub *struct {
} `arg:"subcommand"`
}
p, err := NewParser(Config{StrictSubcommands: true}, &args)
require.NoError(t, err)
err = p.Parse([]string{"sub", "-g"})
assert.Error(t, err)
}
func TestSubcommandGlobalFlag_InCommand_Strict_Inner(t *testing.T) {
var args struct {
Global bool `arg:"-g"`
Sub *struct {
Guard bool `arg:"-g"`
} `arg:"subcommand"`
}
p, err := NewParser(Config{StrictSubcommands: true}, &args)
require.NoError(t, err)
err = p.Parse([]string{"sub", "-g"})
require.NoError(t, err)
assert.False(t, args.Global)
require.NotNil(t, args.Sub)
assert.True(t, args.Sub.Guard)
}

View File

@ -13,9 +13,9 @@ import (
var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()
// cardinality tracks how many tokens are expected for a given spec
// - zero is a boolean, which does to expect any value
// - one is an ordinary option that will be parsed from a single token
// - multiple is a slice or map that can accept zero or more tokens
// - zero is a boolean, which does to expect any value
// - one is an ordinary option that will be parsed from a single token
// - multiple is a slice or map that can accept zero or more tokens
type cardinality int
const (
@ -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 {
switch {
case t.Implements(textUnmarshalerType):
case isTextUnmarshaler(t):
return false
case t.Kind() == reflect.Bool:
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
func isExported(field string) bool {
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
func isZero(v reflect.Value) bool {
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()
}
if !t.Comparable() {

View File

@ -1,5 +1,7 @@
package arg
import "fmt"
// Subcommand returns the user struct for the subcommand selected by
// the command line arguments most recently processed by the parser.
// The return value is always a pointer to a struct. If no subcommand
@ -7,31 +9,35 @@ package arg
// no command line arguments have been processed by this parser then it
// returns nil.
func (p *Parser) Subcommand() interface{} {
if p.lastCmd == nil || p.lastCmd.parent == nil {
if len(p.subcommand) == 0 {
return nil
}
return p.val(p.lastCmd.dest).Interface()
cmd, err := p.lookupCommand(p.subcommand...)
if err != nil {
return nil
}
return p.val(cmd.dest).Interface()
}
// SubcommandNames returns the sequence of subcommands specified by the
// user. If no subcommands were given then it returns an empty slice.
func (p *Parser) SubcommandNames() []string {
if p.lastCmd == nil {
return nil
}
// make a list of ancestor commands
var ancestors []string
cur := p.lastCmd
for cur.parent != nil { // we want to exclude the root
ancestors = append(ancestors, cur.name)
cur = cur.parent
}
// reverse the list
out := make([]string, len(ancestors))
for i := 0; i < len(ancestors); i++ {
out[i] = ancestors[len(ancestors)-i-1]
}
return out
return p.subcommand
}
// lookupCommand finds a subcommand based on a sequence of subcommand names. The
// first string should be a top-level subcommand, the next should be a child
// subcommand of that subcommand, and so on. If no strings are given then the
// root command is returned. If no such subcommand exists then an error is
// returned.
func (p *Parser) lookupCommand(path ...string) (*command, error) {
cmd := p.cmd
for _, name := range path {
found := findSubcommand(cmd.subcommands, name)
if found == nil {
return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
}
cmd = found
}
return cmd, nil
}

View File

@ -83,6 +83,19 @@ func TestNamedSubcommand(t *testing.T) {
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
}
func TestSubcommandAliases(t *testing.T) {
type listCmd struct {
}
var args struct {
List *listCmd `arg:"subcommand:list|ls"`
}
p, err := pparse("ls", &args)
require.NoError(t, err)
assert.NotNil(t, args.List)
assert.Equal(t, args.List, p.Subcommand())
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
}
func TestEmptySubcommand(t *testing.T) {
type listCmd struct {
}
@ -113,6 +126,23 @@ func TestTwoSubcommands(t *testing.T) {
assert.Equal(t, []string{"list"}, p.SubcommandNames())
}
func TestTwoSubcommandsWithAliases(t *testing.T) {
type getCmd struct {
}
type listCmd struct {
}
var args struct {
Get *getCmd `arg:"subcommand:get|g"`
List *listCmd `arg:"subcommand:list|ls"`
}
p, err := pparse("ls", &args)
require.NoError(t, err)
assert.Nil(t, args.Get)
assert.NotNil(t, args.List)
assert.Equal(t, args.List, p.Subcommand())
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
}
func TestSubcommandsWithOptions(t *testing.T) {
type getCmd struct {
Name string
@ -275,6 +305,60 @@ func TestNestedSubcommands(t *testing.T) {
}
}
func TestNestedSubcommandsWithAliases(t *testing.T) {
type child struct{}
type parent struct {
Child *child `arg:"subcommand:child|ch"`
}
type grandparent struct {
Parent *parent `arg:"subcommand:parent|pa"`
}
type root struct {
Grandparent *grandparent `arg:"subcommand:grandparent|gp"`
}
{
var args root
p, err := pparse("gp parent child", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.NotNil(t, args.Grandparent.Parent)
require.NotNil(t, args.Grandparent.Parent.Child)
assert.Equal(t, args.Grandparent.Parent.Child, p.Subcommand())
assert.Equal(t, []string{"gp", "parent", "child"}, p.SubcommandNames())
}
{
var args root
p, err := pparse("grandparent pa", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.NotNil(t, args.Grandparent.Parent)
require.Nil(t, args.Grandparent.Parent.Child)
assert.Equal(t, args.Grandparent.Parent, p.Subcommand())
assert.Equal(t, []string{"grandparent", "pa"}, p.SubcommandNames())
}
{
var args root
p, err := pparse("grandparent", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.Nil(t, args.Grandparent.Parent)
assert.Equal(t, args.Grandparent, p.Subcommand())
assert.Equal(t, []string{"grandparent"}, p.SubcommandNames())
}
{
var args root
p, err := pparse("", &args)
require.NoError(t, err)
require.Nil(t, args.Grandparent)
assert.Nil(t, p.Subcommand())
assert.Empty(t, p.SubcommandNames())
}
}
func TestSubcommandsWithPositionals(t *testing.T) {
type listCmd struct {
Pattern string `arg:"positional"`
@ -411,3 +495,14 @@ func TestValForNilStruct(t *testing.T) {
v := p.val(path{fields: []reflect.StructField{subField, subField}})
assert.False(t, v.IsValid())
}
func TestSubcommandInvalidInternal(t *testing.T) {
// this situation should never arise in practice but still good to test for it
var cmd struct{}
p, err := NewParser(Config{}, &cmd)
require.NoError(t, err)
p.subcommand = []string{"should", "never", "happen"}
sub := p.Subcommand()
assert.Nil(t, sub)
}

184
usage.go
View File

@ -3,23 +3,15 @@ package arg
import (
"fmt"
"io"
"os"
"strings"
)
// the width of the left column
const colWidth = 25
// to allow monkey patching in tests
var (
stdout io.Writer = os.Stdout
stderr io.Writer = os.Stderr
osExit = os.Exit
)
// Fail prints usage information to stderr and exits with non-zero status
func (p *Parser) Fail(msg string) {
p.failWithSubcommand(msg, p.cmd)
p.FailSubcommand(msg)
}
// FailSubcommand prints usage information for a specified subcommand to stderr,
@ -29,28 +21,19 @@ func (p *Parser) Fail(msg string) {
// a sequence of subcommand names starting with the top-level subcommand and so
// on down the tree.
func (p *Parser) FailSubcommand(msg string, subcommand ...string) error {
cmd, err := p.lookupCommand(subcommand...)
err := p.WriteUsageForSubcommand(p.config.Out, subcommand...)
if err != nil {
return err
}
p.failWithSubcommand(msg, cmd)
return nil
}
// failWithSubcommand prints usage information for the given subcommand to stderr and exits with non-zero status
func (p *Parser) failWithSubcommand(msg string, cmd *command) {
p.writeUsageForSubcommand(stderr, cmd)
fmt.Fprintln(stderr, "error:", msg)
osExit(-1)
fmt.Fprintln(p.config.Out, "error:", msg)
p.config.Exit(-1)
return nil
}
// WriteUsage writes usage information to the given writer
func (p *Parser) WriteUsage(w io.Writer) {
cmd := p.cmd
if p.lastCmd != nil {
cmd = p.lastCmd
}
p.writeUsageForSubcommand(w, cmd)
p.WriteUsageForSubcommand(w, p.subcommand...)
}
// WriteUsageForSubcommand writes the usage information for a specified
@ -63,12 +46,7 @@ func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) erro
if err != nil {
return err
}
p.writeUsageForSubcommand(w, cmd)
return nil
}
// writeUsageForSubcommand writes usage information for the given subcommand
func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *command) {
var positionals, longOptions, shortOptions []*spec
for _, spec := range cmd.specs {
switch {
@ -85,18 +63,10 @@ func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *command) {
fmt.Fprintln(w, p.version)
}
// make a list of ancestor commands so that we print with full context
var ancestors []string
ancestor := cmd
for ancestor != nil {
ancestors = append(ancestors, ancestor.name)
ancestor = ancestor.parent
}
// print the beginning of the usage string
fmt.Fprint(w, "Usage:")
for i := len(ancestors) - 1; i >= 0; i-- {
fmt.Fprint(w, " "+ancestors[i])
fmt.Fprintf(w, "Usage: %s", p.cmd.name)
for _, s := range subcommand {
fmt.Fprint(w, " "+s)
}
// write the option component of the usage message
@ -157,47 +127,66 @@ func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *command) {
}
fmt.Fprint(w, "\n")
return nil
}
func printTwoCols(w io.Writer, left, help string, defaultVal string, envVal string) {
lhs := " " + left
// print prints a line like this:
//
// --option FOO A description of the option [default: 123]
//
// If the text on the left is longer than a certain threshold, the description is moved to the next line:
//
// --verylongoptionoption VERY_LONG_VARIABLE
// A description of the option [default: 123]
//
// If multiple "extras" are provided then they are put inside a single set of square brackets:
//
// --option FOO A description of the option [default: 123, env: FOO]
func print(w io.Writer, item, description string, bracketed ...string) {
lhs := " " + item
fmt.Fprint(w, lhs)
if help != "" {
if description != "" {
if len(lhs)+2 < colWidth {
fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs)))
} else {
fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
}
fmt.Fprint(w, help)
fmt.Fprint(w, description)
}
bracketsContent := []string{}
if defaultVal != "" {
bracketsContent = append(bracketsContent,
fmt.Sprintf("default: %s", defaultVal),
)
var brack string
for _, s := range bracketed {
if s != "" {
if brack != "" {
brack += ", "
}
brack += s
}
}
if envVal != "" {
bracketsContent = append(bracketsContent,
fmt.Sprintf("env: %s", envVal),
)
}
if len(bracketsContent) > 0 {
fmt.Fprintf(w, " [%s]", strings.Join(bracketsContent, ", "))
if brack != "" {
fmt.Fprintf(w, " [%s]", brack)
}
fmt.Fprint(w, "\n")
}
func withDefault(s string) string {
if s == "" {
return ""
}
return "default: " + s
}
func withEnv(env string) string {
if env == "" {
return ""
}
return "env: " + env
}
// WriteHelp writes the usage string followed by the full help string for each option
func (p *Parser) WriteHelp(w io.Writer) {
cmd := p.cmd
if p.lastCmd != nil {
cmd = p.lastCmd
}
p.writeHelpForSubcommand(w, cmd)
p.WriteHelpForSubcommand(w, p.subcommand...)
}
// WriteHelpForSubcommand writes the usage string followed by the full help
@ -210,13 +199,9 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
if err != nil {
return err
}
p.writeHelpForSubcommand(w, cmd)
return nil
}
// writeHelp writes the usage string for the given subcommand
func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
var positionals, longOptions, shortOptions []*spec
var positionals, longOptions, shortOptions, envOnlyOptions []*spec
var hasVersionOption bool
for _, spec := range cmd.specs {
switch {
case spec.positional:
@ -225,19 +210,21 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
longOptions = append(longOptions, spec)
case spec.short != "":
shortOptions = append(shortOptions, spec)
case spec.short == "" && spec.long == "":
envOnlyOptions = append(envOnlyOptions, spec)
}
}
if p.description != "" {
fmt.Fprintln(w, p.description)
}
p.writeUsageForSubcommand(w, cmd)
p.WriteUsageForSubcommand(w, subcommand...)
// write the list of positionals
if len(positionals) > 0 {
fmt.Fprint(w, "\nPositional arguments:\n")
for _, spec := range positionals {
printTwoCols(w, spec.placeholder, spec.help, "", "")
print(w, spec.placeholder, spec.help)
}
}
@ -249,6 +236,9 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
}
for _, spec := range longOptions {
p.printOption(w, spec)
if spec.long == "version" {
hasVersionOption = true
}
}
}
@ -265,6 +255,9 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
fmt.Fprint(w, "\nGlobal options:\n")
for _, spec := range globals {
p.printOption(w, spec)
if spec.long == "version" {
hasVersionOption = true
}
}
}
@ -275,7 +268,7 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
short: "h",
help: "display this help and exit",
})
if p.version != "" {
if !hasVersionOption && p.version != "" {
p.printOption(w, &spec{
cardinality: zero,
long: "version",
@ -283,13 +276,27 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
})
}
// write the list of environment only variables
if len(envOnlyOptions) > 0 {
fmt.Fprint(w, "\nEnvironment variables:\n")
for _, spec := range envOnlyOptions {
p.printEnvOnlyVar(w, spec)
}
}
// write the list of subcommands
if len(cmd.subcommands) > 0 {
fmt.Fprint(w, "\nCommands:\n")
for _, subcmd := range cmd.subcommands {
printTwoCols(w, subcmd.name, subcmd.help, "", "")
names := append([]string{subcmd.name}, subcmd.aliases...)
print(w, strings.Join(names, ", "), subcmd.help)
}
}
if p.epilogue != "" {
fmt.Fprintln(w, "\n"+p.epilogue)
}
return nil
}
func (p *Parser) printOption(w io.Writer, spec *spec) {
@ -301,30 +308,23 @@ func (p *Parser) printOption(w io.Writer, spec *spec) {
ways = append(ways, synopsis(spec, "-"+spec.short))
}
if len(ways) > 0 {
printTwoCols(w, strings.Join(ways, ", "), spec.help, spec.defaultVal, spec.env)
print(w, strings.Join(ways, ", "), spec.help, withDefault(spec.defaultString), withEnv(spec.env))
}
}
// lookupCommand finds a subcommand based on a sequence of subcommand names. The
// first string should be a top-level subcommand, the next should be a child
// subcommand of that subcommand, and so on. If no strings are given then the
// root command is returned. If no such subcommand exists then an error is
// returned.
func (p *Parser) lookupCommand(path ...string) (*command, error) {
cmd := p.cmd
for _, name := range path {
var found *command
for _, child := range cmd.subcommands {
if child.name == name {
found = child
}
}
if found == nil {
return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
}
cmd = found
func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) {
ways := make([]string, 0, 2)
if spec.required {
ways = append(ways, "Required.")
} else {
ways = append(ways, "Optional.")
}
return cmd, nil
if spec.help != "" {
ways = append(ways, spec.help)
}
print(w, spec.env, strings.Join(ways, " "), withDefault(spec.defaultString))
}
func synopsis(spec *spec, form string) string {

View File

@ -50,12 +50,16 @@ Options:
--optimize OPTIMIZE, -O OPTIMIZE
optimization level
--ids IDS Ids
--values VALUES Values [default: [3.14 42 256]]
--values VALUES Values
--workers WORKERS, -w WORKERS
number of workers to start [default: 10, env: WORKERS]
--testenv TESTENV, -a TESTENV [env: TEST_ENV]
--file FILE, -f FILE File with mandatory extension [default: scratch.txt]
--help, -h display this help and exit
Environment variables:
API_KEY Required. Only via env-var for security reasons
TRACE Optional. Record low-level trace
`
var args struct {
@ -70,11 +74,12 @@ Options:
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"`
ApiKey string `arg:"required,-,--,env:API_KEY" help:"Only via env-var for security reasons"`
Trace bool `arg:"-,--,env" help:"Record low-level trace"`
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)
@ -285,6 +290,37 @@ Options:
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 {
@ -414,6 +450,8 @@ Global options:
_ = p.Parse([]string{"child", "nested", "value"})
assert.Equal(t, []string{"child", "nested"}, p.SubcommandNames())
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
@ -435,7 +473,7 @@ func TestNonexistentSubcommand(t *testing.T) {
var args struct {
sub *struct{} `arg:"subcommand"`
}
p, err := NewParser(Config{}, &args)
p, err := NewParser(Config{Exit: func(int) {}}, &args)
require.NoError(t, err)
var b bytes.Buffer
@ -475,7 +513,7 @@ Options:
ShortOnly2 string `arg:"-b,--,required" help:"some help2"`
}
p, err := NewParser(Config{Program: "example"}, &args)
assert.NoError(t, err)
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
@ -522,10 +560,16 @@ Usage: example [-s SHORT]
Options:
-s SHORT [env: SHORT]
--help, -h display this help and exit
Environment variables:
ENVONLY Optional.
ENVONLY2 Optional.
CUSTOM Optional.
`
var args struct {
Short string `arg:"--,-s,env"`
EnvOnly string `arg:"--,env"`
EnvOnly2 string `arg:"--,-,env"`
EnvOnlyOverriden string `arg:"--,env:CUSTOM"`
}
@ -541,19 +585,39 @@ Options:
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestEnvOnlyArgs(t *testing.T) {
expectedUsage := "Usage: example [--arg ARG]"
expectedHelp := `
Usage: example [--arg ARG]
Options:
--arg ARG, -a ARG [env: MY_ARG]
--help, -h display this help and exit
Environment variables:
AUTH_KEY Required.
`
var args struct {
ArgParam string `arg:"-a,--arg,env:MY_ARG"`
AuthKey string `arg:"required,--,env:AUTH_KEY"`
}
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) {
originalStderr := stderr
originalExit := osExit
defer func() {
stderr = originalStderr
osExit = originalExit
}()
var b bytes.Buffer
stderr = &b
var stdout bytes.Buffer
var exitCode int
osExit = func(code int) { exitCode = code }
exit := func(code int) { exitCode = code }
expectedStdout := `
Usage: example [--foo FOO]
@ -563,27 +627,18 @@ error: something went wrong
var args struct {
Foo int
}
p, err := NewParser(Config{Program: "example"}, &args)
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:], b.String())
assert.Equal(t, expectedStdout[1:], stdout.String())
assert.Equal(t, -1, exitCode)
}
func TestFailSubcommand(t *testing.T) {
originalStderr := stderr
originalExit := osExit
defer func() {
stderr = originalStderr
osExit = originalExit
}()
var b bytes.Buffer
stderr = &b
var stdout bytes.Buffer
var exitCode int
osExit = func(code int) { exitCode = code }
exit := func(code int) { exitCode = code }
expectedStdout := `
Usage: example sub
@ -593,12 +648,70 @@ error: something went wrong
var args struct {
Sub *struct{} `arg:"subcommand"`
}
p, err := NewParser(Config{Program: "example"}, &args)
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:], b.String())
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())
}
func TestHelpShowsSubcommandAliases(t *testing.T) {
expectedHelp := `
Usage: example <command> [<args>]
Options:
--help, -h display this help and exit
Commands:
remove, rm, r remove something from somewhere
simple do something simple
halt, stop stop now
`
var args struct {
Remove *struct{} `arg:"subcommand:remove|rm|r" help:"remove something from somewhere"`
Simple *struct{} `arg:"subcommand" help:"do something simple"`
Stop *struct{} `arg:"subcommand:halt|stop" help:"stop now"`
}
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())
}