Compare commits

..

86 Commits

Author SHA1 Message Date
Jeff Carr 5716f9af0d dump go.* 2024-12-03 18:01:02 -06:00
Jeff Carr 3d847431ab dump go.mod and go.sum 2024-11-07 16:55:05 -06:00
Jeff Carr f92d210ca7 Merge remote-tracking branch 'flint/master' into jcarr
Signed-off-by: Jeff Carr <jcarr@wit.com>
2024-11-07 12:48:14 -06:00
Alex Flint 438bbfff1e
Merge pull request from hhromic/implement-204
Add support for setting a global env var prefix
2024-11-04 12:58:37 -05:00
Alex Flint efb1be7122
Merge pull request from alexflint/pass-dash-dash-through
Passing the no-more-options string "--" twice or more
2024-11-04 12:56:12 -05:00
Alex Flint 51d9bef113 passing the no-more-options string "--" twice or more should pass the second and subsequent ones through as positionals 2024-10-21 17:08:37 -04:00
Hugo Hromic cb7e5c1905
Add global env prefix example to README
* Also made newline separations around sections consistent
* Also fixed usage of `p.Parse()` in env variable ignore example
2024-09-07 13:07:21 +01:00
Hugo Hromic 9b5c76b1c4
Add support for setting a global env var prefix 2024-09-07 12:19:10 +01:00
Alex Flint b218ad854d
Merge pull request from alexflint/update-funding
Update FUNDING.yml
2024-09-06 09:00:31 -04:00
Alex Flint dcb5577c2b
Update FUNDING.yml 2024-09-06 09:00:13 -04:00
Alex Flint d10a064207
Merge pull request from hhromic/fix-261
Fix help text for positional args with default and env var
2024-09-05 17:20:09 -04:00
Alex Flint a5045bbe85
Merge pull request from hhromic/fix-193
Move writing program version from usage to help writer
2024-09-05 17:19:12 -04:00
Alex Flint 3925edf11a
Merge pull request from alexflint/parse-docs
Update API docs for Parser.Parse
2024-09-05 17:17:34 -04:00
Alex Flint 12fffac1d8 field -> fields 2024-09-05 17:16:23 -04:00
Alex Flint b13a62172a update api docs for Parser.Parse 2024-09-05 17:15:02 -04:00
Alex Flint 7cf32414af
Merge pull request from alexflint/sponsorship
Living in a monastery; looking for funding
2024-09-05 17:06:35 -04:00
Alex Flint bdb7560b8d
Living in a monastery looking for funding
The first commit to this project was on October 31, 2015, almost 9 years ago. It was [268 lines of code](408290f7c2) and it worked pretty well! That was just about three and a half years after Go 1.0 was released. What fun!

At that time there was no Go module system, so there was no need for versioned releases. Later, I started releasing official versions from time to time. v1.0.0 was published in December 2018.

Over the years I've resisted adding a lot of features, and as a result the library is in pretty good shape. I use it in almost every Go program I write, personally, both servers/daemons and command line tools. It's nice!

I live in a Buddhist monastery in Vermont now, not as a monk but as a lay practitioner. I'm working on building a form of Buddhism fit for consumption by AI systems. I love maintaining this little piece of software and I'd love some financial support to do so. I don't have a day job, and I need money to buy firewood, pay for car insurance, and travel to see my folks back home in Australia from time to time.

If you use go-arg please consider sponsoring me. It would make a huge difference to me, and it will create a connection between us. I look forward to many long relationships.
2024-09-05 17:05:51 -04:00
Alex Flint 50166cae2c
Merge pull request from alexflint/readme-custom-error-handling
Add info to README about programmatically reproducing behavior of MustParse
2024-09-04 10:31:40 -04:00
Alex Flint 7fd624cf1c add info to README about programmatically reproducing behavior of MustParse 2024-09-04 10:27:34 -04:00
Hugo Hromic bf156d17a3
Fix help text for positional args with default and env var 2024-07-22 19:25:51 +01:00
Hugo Hromic 3673177bf9
Move writing program version from usage to help writer
* Writing the version on usage text is unexpected and confusing
2024-07-06 11:52:47 +01:00
Alex Flint 3de7278c4f
Merge pull request from hhromic/fix-testable-example
Fix testable example output comment formatting
2024-07-04 12:40:29 -04:00
Hugo Hromic b8282df4c4
Fix testable example output comment formatting 2024-06-30 23:46:34 +01:00
Alex Flint ec0ced7467
Merge pull request from alexflint/bump-go-versions
bump go versions used in CI
2024-06-30 16:42:12 -04:00
Alex Flint 0cc152dce5
Merge pull request from hhromic/better-version-v2
Fix usage writing when using custom version flag
2024-06-30 12:27:39 -04:00
Alex Flint 67353a8bcf
Update version of github actions 2024-06-30 10:35:08 -04:00
Alex Flint af368523db
Update go.yml 2024-06-30 10:33:03 -04:00
Alex Flint b6422dcbc3
Merge pull request from testwill/typo
fix: typo
2024-06-30 10:32:09 -04:00
Alex Flint 56ee7c97ac
Merge pull request from purpleidea/feat/env-docs
add an example for environment vars with arg names
2024-06-30 10:31:46 -04:00
Alex Flint 177b84441e
Merge pull request from hhromic/fix-246
Use standard exit status code for usage errors
2024-06-30 10:31:00 -04:00
Hugo Hromic c087d71802
Add note for version flag overriding to README 2024-06-30 00:13:28 +01:00
Hugo Hromic c992aa8627
Add more test cases for version help/usage writing 2024-06-30 00:12:34 +01:00
Hugo Hromic bed89eb683
Implement scanning of version flag in specs for usage generation 2024-06-29 23:42:22 +01:00
Hugo Hromic 4ed4ce751f
Better scanning of version flag in specs for help generation 2024-06-29 23:42:22 +01:00
Hugo Hromic a7c40c36a3
Use standard exit status code for usage errors
* The stdlib `flags` package and most command line utilities use status code `2`.
2024-06-29 15:44:50 +01:00
Alex Flint bee5cf5d7c
Merge pull request from hhromic/fix-254
Fix crash on errors in package-level `MustParse`
2024-06-28 11:03:08 -04:00
Hugo Hromic aa844c7de9
Fix crash on errors in package-level `MustParse` 2024-06-27 00:33:09 +01:00
Alex Flint dfca71d159
Merge pull request from alexflint/handle-empty-placeholder
Handle explicit empty placeholders
2024-04-02 12:16:06 -04:00
Alex Flint 188bd31bf6
Merge pull request from alexflint/restore-100pct-coverage
Restore 100% test coverage
2024-04-02 12:14:49 -04:00
Alex Flint 8a917260c3 add a test case with single-level subcommands 2024-04-02 12:10:52 -04:00
Alex Flint 3ddfffdcd3 add test for help and usage when a --version flag is present 2024-04-02 12:05:00 -04:00
Alex Flint 68948b2ac1 restore 100% code coverage 2024-03-31 12:05:26 -04:00
Alex Flint be792f1f8b ping 2024-03-31 11:52:16 -04:00
Alex Flint 8e35a4f0d4 handle explicit empty placeholders 2024-03-31 10:30:12 -04:00
James Shubin 84ddf1d244 add an example for environment vars with arg names
If you want to specify both of these, and if they should have different
names, then this shows you how it can be done.
2024-02-28 22:29:16 -05:00
Jeff Carr 6b16520795 go mod update against scalar
Signed-off-by: Jeff Carr <jcarr@wit.com>
2024-01-14 14:37:17 -06:00
Jeff Carr 0af6f25365 add register()
Signed-off-by: Jeff Carr <jcarr@wit.com>
2024-01-14 14:26:47 -06:00
Jeff Carr 530fcb84d4 isolate tests
Signed-off-by: Jeff Carr <jcarr@wit.com>
2024-01-14 14:25:54 -06:00
guoguangwu 582e6d537a fix: typo
Signed-off-by: guoguangwu <guoguangwu@magic-shield.com>
2023-11-15 17:58:55 +08:00
Alex Flint bf629a16cb
Merge pull request from alexflint/subcommand-aliases
add subcommand aliases
2023-10-10 18:36:46 -04:00
Alex Flint f02da4cd10 bump go versions used in CI 2023-10-08 20:39:23 -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 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 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 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 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 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 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 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 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: 
2022-10-10 17:25:14 +02:00
Alex Flint 23b2b67fe2 fix issue 2022-06-09 11:21:29 -04:00
41 changed files with 1800 additions and 5922 deletions

1
.github/FUNDING.yml vendored Normal file
View File

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

View File

@ -15,17 +15,17 @@ jobs:
strategy:
fail-fast: false
matrix:
go: ['1.17', '1.18', '1.19']
go: ['1.20', '1.21', '1.22']
steps:
- id: go
name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Build
run: go build -v .

3
.gitignore vendored
View File

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

11
Makefile Normal file
View File

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

545
README.md
View File

@ -16,12 +16,6 @@
Declare command line arguments for your program by defining a struct.
```go
import "github.com/go-arg/v2
```
TODO
```go
var args struct {
Foo string
@ -39,7 +33,7 @@ hello true
### Installation
```shell
go get github.com/alexflint/go-arg/v2
go get github.com/alexflint/go-arg
```
### Required arguments
@ -70,7 +64,7 @@ fmt.Println("Input:", args.Input)
fmt.Println("Output:", args.Output)
```
```
```shell
$ ./example src.txt x.out y.out z.out
Input: src.txt
Output: [x.out y.out z.out]
@ -86,17 +80,85 @@ arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```
```shell
$ WORKERS=4 ./example
Workers: 4
```
```
```shell
$ WORKERS=4 ./example --workers=6
Workers: 6
```
You can also override the name of the environment variable:
```go
var args struct {
Workers int `arg:"env:NUM_WORKERS"`
}
arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```shell
$ NUM_WORKERS=4 ./example
Workers: 4
```
You can provide multiple values in environment variables using commas:
```go
var args struct {
Workers []int `arg:"env"`
}
arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```shell
$ WORKERS='1,99' ./example
Workers: [1 99]
```
Command line arguments take precedence over environment variables:
```go
var args struct {
Workers int `arg:"--count,env:NUM_WORKERS"`
}
arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```shell
$ NUM_WORKERS=6 ./example
Workers: 6
$ NUM_WORKERS=6 ./example --count 4
Workers: 4
```
Configuring a global environment variable name prefix is also possible:
```go
var args struct {
Workers int `arg:"--count,env:NUM_WORKERS"`
}
p, err := arg.NewParser(arg.Config{
EnvPrefix: "MYAPP_",
}, &args)
p.MustParse(os.Args[1:])
fmt.Println("Workers:", args.Workers)
```
```shell
$ MYAPP_NUM_WORKERS=6 ./example
Workers: 6
```
### Usage strings
```go
var args struct {
Input string `arg:"positional"`
@ -134,19 +196,28 @@ var args struct {
arg.MustParse(&args)
```
### Overriding the name of an environment variable
Command line arguments take precedence over environment variables, which take precedence over default values. This means that we check whether a certain option was provided on the command line, then if not, we check for an environment variable (only if an `env` tag was provided), then if none is found, we check for a `default` tag containing a default value.
```go
var args struct {
Workers int `arg:"env:NUM_WORKERS"`
Test string `arg:"-t,env:TEST" default:"something"`
}
arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```
$ NUM_WORKERS=4 ./example
Workers: 4
#### Ignoring environment variables and/or default values
```go
var args struct {
Test string `arg:"-t,env:TEST" default:"something"`
}
p, err := arg.NewParser(arg.Config{
IgnoreEnv: true,
IgnoreDefault: true,
}, &args)
err = p.Parse(os.Args[1:])
```
### Arguments with multiple values
@ -165,7 +236,26 @@ fmt.Printf("Fetching the following IDs from %s: %q", args.Database, args.IDs)
Fetching the following IDs from foo: [1 2 3]
```
### Arguments that can be specified multiple times, mixed with positionals
```go
var args struct {
Commands []string `arg:"-c,separate"`
Files []string `arg:"-f,separate"`
Databases []string `arg:"positional"`
}
arg.MustParse(&args)
```
```shell
./example -c cmd1 db1 -f file1 db2 -c cmd2 -f file2 -f file3 db3 -c cmd3
Commands: [cmd1 cmd2 cmd3]
Files [file1 file2 file3]
Databases [db1 db2 db3]
```
### Arguments with keys and values
```go
var args struct {
UserIDs map[string]int
@ -179,29 +269,11 @@ fmt.Println(args.UserIDs)
map[john:123 mary:456]
```
### Custom validation
```go
var args struct {
Foo string
Bar string
}
p := arg.MustParse(&args)
if args.Foo == "" && args.Bar == "" {
p.Fail("you must provide either --foo or --bar")
}
```
```shell
./example
Usage: samples [--foo FOO] [--bar BAR]
error: you must provide either --foo or --bar
```
### Version strings
```go
type args struct {
// ...
...
}
func (args) Version() string {
@ -219,6 +291,28 @@ $ ./example --version
someprogram 4.3.0
```
> **Note**
> If a `--version` flag is defined in `args` or any subcommand, it overrides the built-in versioning.
### Custom validation
```go
var args struct {
Foo string
Bar string
}
p := arg.MustParse(&args)
if args.Foo == "" && args.Bar == "" {
p.Fail("you must provide either --foo or --bar")
}
```
```shell
./example
Usage: samples [--foo FOO] [--bar BAR]
error: you must provide either --foo or --bar
```
### Overriding option names
```go
@ -243,13 +337,11 @@ Options:
--help, -h display this help and exit
```
### Embedded structs
The fields of embedded structs are treated just like regular fields:
```go
type DatabaseOptions struct {
Host string
Username string
@ -288,7 +380,7 @@ The following types may be used as arguments:
- maps using any of the above as keys and values
- any type that implements `encoding.TextUnmarshaler`
### Custom parsing
### Custom parsing
Implement `encoding.TextUnmarshaler` to define your own parsing logic.
@ -317,6 +409,7 @@ func main() {
fmt.Printf("%#v\n", args.Name)
}
```
```shell
$ ./example --name=foo.bar
main.NameDotName{Head:"foo", Tail:"bar"}
@ -326,121 +419,73 @@ Usage: example [--name NAME]
error: error processing --name: missing period in "oops"
```
### Slice-valued environment variables
### Custom parsing with default values
You can provide multiple values using the CSV (RFC 4180) format:
Implement `encoding.TextMarshaler` to define your own default value strings:
```go
// Accepts command line arguments of the form "head.tail"
type NameDotName struct {
Head, Tail string
}
func (n *NameDotName) UnmarshalText(b []byte) error {
// same as previous example
}
// this is only needed if you want to display a default value in the usage string
func (n *NameDotName) MarshalText() ([]byte, error) {
return []byte(fmt.Sprintf("%s.%s", n.Head, n.Tail)), nil
}
func main() {
var args struct {
Name NameDotName `default:"file.txt"`
}
arg.MustParse(&args)
fmt.Printf("%#v\n", args.Name)
}
```
```shell
$ ./example --help
Usage: test [--name NAME]
Options:
--name NAME [default: file.txt]
--help, -h display this help and exit
$ ./example
main.NameDotName{Head:"file", Tail:"txt"}
```
### Custom placeholders
Use the `placeholder` tag to control which placeholder text is used in the usage text.
```go
var args struct {
Workers []int `arg:"env"`
}
arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```
$ WORKERS='1,99' ./example
Workers: [1 99]
```
### Parsing command line tokens and environment variables from a slice
You can override the command line tokens and environment variables processed by go-arg:
```go
var args struct {
Samsara int
Nirvana float64 `arg:"env:NIRVANA"`
}
p, err := arg.NewParser(&args)
if err != nil {
log.Fatal(err)
}
cmdline := []string{"./thisprogram", "--samsara=123"}
environ := []string{"NIRVANA=45.6"}
err = p.Parse(cmdline, environ)
if err != nil {
log.Fatal(err)
}
```
```
./example
SAMSARA: 123
NIRVANA: 45.6
```
### Configuration files
TODO
### Combining command line options, environment variables, and default values
By default, command line arguments take precedence over environment variables, which take precedence over default values. This means that we check whether a certain option was provided on the command line, then if not, we check for an environment variable (only if an `env` tag was provided), then, if none is found, we check for a `default` tag.
```go
var args struct {
Test string `arg:"-t,env:TEST" default:"something"`
}
arg.MustParse(&args)
```
### Changing precedence of command line options, environment variables, and default values
You can use the low-level functions `Process*` and `OverwriteWith*` to control which things override which other things. Here is an example in which environment variables take precedence over command line options, which is the opposite of the default behavior:
```go
var args struct {
Test string `arg:"env:TEST"`
}
p, err := arg.NewParser(&args)
if err != nil {
log.Fatal(err)
}
err = p.ParseCommandLine(os.Args)
if err != nil {
p.Fail(err.Error())
}
err = p.OverwriteWithEnvironment(os.Environ())
if err != nil {
p.Fail(err.Error())
}
err = p.Validate()
if err != nil {
p.Fail(err.Error())
}
fmt.Printf("test=%s\n", args.Test)
```
```
TEST=value_from_env ./example --test=value_from_option
test=value_from_env
```
### Ignoring environment variables
TODO
### Ignoring default values
TODO
### Arguments that can be specified multiple times
```go
var args struct {
Commands []string `arg:"-c,separate"`
Files []string `arg:"-f,separate"`
Input string `arg:"positional" placeholder:"SRC"`
Output []string `arg:"positional" placeholder:"DST"`
Optimize int `arg:"-O" help:"optimization level" placeholder:"LEVEL"`
MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" placeholder:"N"`
}
arg.MustParse(&args)
```
```shell
./example -c cmd1 -f file1 -c cmd2 -f file2 -f file3 -c cmd3
Commands: [cmd1 cmd2 cmd3]
Files [file1 file2 file3]
$ ./example -h
Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
Positional arguments:
SRC
DST
Options:
--optimize LEVEL, -O LEVEL
optimization level
--maxjobs N, -j N maximum number of simultaneous jobs
--help, -h display this help and exit
```
### Description strings
@ -504,14 +549,16 @@ For more information visit github.com/alexflint/go-arg
### Subcommands
Subcommands are commonly used in tools that group multiple functions into a single program. An example is the `git` tool:
Subcommands are commonly used in tools that wish to group multiple functions into a single program. An example is the `git` tool:
```shell
$ git checkout [arguments specific to checking out code]
$ git commit [arguments specific to committing code]
$ git push [arguments specific to pushing code]
$ git commit [arguments specific to committing]
$ git push [arguments specific to pushing]
```
This can be implemented with `go-arg` with the `arg:"subcommand"` tag:
The strings "checkout", "commit", and "push" are different from simple positional arguments because the options available to the user change depending on which subcommand they choose.
This can be implemented with `go-arg` as follows:
```go
type CheckoutCmd struct {
@ -546,9 +593,14 @@ case args.Push != nil:
}
```
Note that the `subcommand` tag can only be used with fields that are pointers to structs, and that any struct that contains subcommands cannot also contain positionals.
Some additional rules apply when working with subcommands:
* The `subcommand` tag can only be used with fields that are pointers to structs
* Any struct that contains a subcommand must not contain any positionals
### Terminating when no subcommands are specified
This package allows to have a program that accepts subcommands, but also does something else
when no subcommands are specified.
If on the other hand you want the program to terminate when no subcommands are specified,
the recommended way is:
```go
p := arg.MustParse(&args)
@ -557,39 +609,192 @@ if p.Subcommand() == nil {
}
```
### Customizing placeholder strings
### Custom handling of --help and --version
Use the `placeholder` tag to control which placeholder text is used in the usage text.
The following reproduces the internal logic of `MustParse` for the simple case where
you are not using subcommands or --version. This allows you to respond
programatically to --help, and to any errors that come up.
```go
var args struct {
Input string `arg:"positional" placeholder:"SRC"`
Output []string `arg:"positional" placeholder:"DST"`
Optimize int `arg:"-O" placeholder:"LEVEL"`
MaxJobs int `arg:"-j" placeholder:"N"`
Something string
}
arg.MustParse(&args)
```
```shell
$ ./example -h
Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
Positional arguments:
SRC
DST
p, err := arg.NewParser(arg.Config{}, &args)
if err != nil {
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
}
err = p.Parse(os.Args[1:])
switch {
case err == arg.ErrHelp: // indicates that user wrote "--help" on command line
p.WriteHelp(os.Stdout)
os.Exit(0)
case err != nil:
fmt.Printf("error: %v\n", err)
p.WriteUsage(os.Stdout)
os.Exit(1)
}
```
```shell
$ go run ./example --help
Usage: ./example --something SOMETHING
Options:
--optimize LEVEL, -O LEVEL
--maxjobs N, -j N
--something SOMETHING
--help, -h display this help and exit
$ ./example --wrong
error: unknown argument --wrong
Usage: ./example --something SOMETHING
$ ./example
error: --something is required
Usage: ./example --something SOMETHING
```
To also handle --version programatically, use the following:
```go
type args struct {
Something string
}
func (args) Version() string {
return "1.2.3"
}
func main() {
var args args
p, err := arg.NewParser(arg.Config{}, &args)
if err != nil {
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
}
err = p.Parse(os.Args[1:])
switch {
case err == arg.ErrHelp: // found "--help" on command line
p.WriteHelp(os.Stdout)
os.Exit(0)
case err == arg.ErrVersion: // found "--version" on command line
fmt.Println(args.Version())
os.Exit(0)
case err != nil:
fmt.Printf("error: %v\n", err)
p.WriteUsage(os.Stdout)
os.Exit(1)
}
fmt.Printf("got %q\n", args.Something)
}
```
```shell
$ ./example --version
1.2.3
$ go run ./example --help
1.2.3
Usage: example --something SOMETHING
Options:
--something SOMETHING
--help, -h display this help and exit
$ ./example --wrong
1.2.3
error: unknown argument --wrong
Usage: example --something SOMETHING
$ ./example
error: --something is required
Usage: example --something SOMETHING
```
To generate subcommand-specific help messages, use the following most general version
(this also works in absence of subcommands but is a bit more complex):
```go
type fetchCmd struct {
Count int
}
type args struct {
Something string
Fetch *fetchCmd `arg:"subcommand"`
}
func (args) Version() string {
return "1.2.3"
}
func main() {
var args args
p, err := arg.NewParser(arg.Config{}, &args)
if err != nil {
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
}
err = p.Parse(os.Args[1:])
switch {
case err == arg.ErrHelp: // found "--help" on command line
p.WriteHelpForSubcommand(os.Stdout, p.SubcommandNames()...)
os.Exit(0)
case err == arg.ErrVersion: // found "--version" on command line
fmt.Println(args.Version())
os.Exit(0)
case err != nil:
fmt.Printf("error: %v\n", err)
p.WriteUsageForSubcommand(os.Stdout, p.SubcommandNames()...)
os.Exit(1)
}
}
```
```shell
$ ./example --version
1.2.3
$ ./example --help
1.2.3
Usage: example [--something SOMETHING] <command> [<args>]
Options:
--something SOMETHING
--help, -h display this help and exit
--version display version and exit
Commands:
fetch
$ ./example fetch --help
1.2.3
Usage: example fetch [--count COUNT]
Options:
--count COUNT
Global options:
--something SOMETHING
--help, -h display this help and exit
--version display version and exit
```
### API Documentation
https://godoc.org/github.com/alexflint/go-arg
https://pkg.go.dev/github.com/alexflint/go-arg
### Migrating from v1.x
### Rationale
Migrating IgnoreEnv to passing a nil environ
There are many command line argument parsing libraries for Go, including one in the standard library, so why build another?
Migrating from IgnoreDefault to calling ProcessCommandLine
The `flag` library that ships in the standard library seems awkward to me. Positional arguments must precede options, so `./prog x --foo=1` does what you expect but `./prog --foo=1 x` does not. It also does not allow arguments to have both long (`--foo`) and short (`-f`) forms.
Many third-party argument parsing libraries are great for writing sophisticated command line interfaces, but feel to me like overkill for a simple script with a few flags.
The idea behind `go-arg` is that Go already has an excellent way to describe data structures using structs, so there is no need to develop additional levels of abstraction. Instead of one API to specify which arguments your program accepts, and then another API to get the values of those arguments, `go-arg` replaces both with a single struct.
### Backward compatibility notes
Earlier versions of this library required the help text to be part of the `arg` tag. This is still supported but is now deprecated. Instead, you should use a separate `help` tag, described above, which makes it possible to include commas inside help text.

8
go.mod
View File

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

15
go.sum
View File

@ -1,15 +0,0 @@
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=

View File

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

View File

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

View File

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

308
parse.go
View File

@ -5,12 +5,13 @@ import (
"encoding/csv"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strings"
scalar "github.com/alexflint/go-scalar"
"go.wit.com/dev/alexflint/scalar"
)
// path represents a sequence of steps to find the output location for an
@ -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 // placeholder string 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,49 @@ 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 and test code
var mustParseExit = os.Exit
var mustParseOut io.Writer = os.Stdout
// 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, Out: mustParseOut}, register...)
}
// mustParse is a helper that facilitates testing
func mustParse(config Config, dest ...interface{}) *Parser {
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(2)
return nil
}
p.MustParse(flags())
return p
}
@ -125,6 +144,19 @@ type Config struct {
// 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
// EnvPrefix instructs the library to use a name prefix when reading environment variables.
EnvPrefix string
// 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
@ -137,7 +169,7 @@ type Parser struct {
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
@ -187,6 +219,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 {
@ -216,23 +256,36 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", t))
}
cmd, err := cmdFromStruct(name, path{root: i}, t)
cmd, err := cmdFromStruct(name, path{root: i}, t, config.EnvPrefix)
if err != nil {
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)
}
}
@ -253,7 +306,7 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
return &p, nil
}
func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*command, error) {
// commands can only be created from pointers to structs
if t.Kind() != reflect.Ptr {
return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a %s",
@ -304,13 +357,8 @@ 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
// process each comma-separated part of the tag
var isSubcommand bool
for _, key := range strings.Split(tag, ",") {
if key == "" {
continue
@ -328,18 +376,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
@ -350,24 +393,30 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
case key == "env":
// Use override name if provided
if value != "" {
spec.env = value
spec.env = envPrefix + value
} else {
spec.env = strings.ToUpper(field.Name)
spec.env = envPrefix + strings.ToUpper(field.Name)
}
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, envPrefix)
if err != nil {
errs = append(errs, err.Error())
return false
}
subcmd.aliases = cmdnames[1:]
subcmd.parent = &cmd
subcmd.help = field.Tag.Get("help")
@ -379,6 +428,7 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
}
}
// placeholder is the string used in the help text like this: "--somearg PLACEHOLDER"
placeholder, hasPlaceholder := field.Tag.Lookup("placeholder")
if hasPlaceholder {
spec.placeholder = placeholder
@ -388,27 +438,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
})
@ -431,8 +514,15 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
return &cmd, nil
}
// Parse processes the given command line option, storing the results in the field
// of the structs from which NewParser was constructed
// Parse processes the given command line option, storing the results in the fields
// of the structs from which NewParser was constructed.
//
// It returns ErrHelp if "--help" is one of the command line args and ErrVersion if
// "--version" is one of the command line args (the latter only applies if the
// destination struct passed to NewParser implements Versioned.)
//
// To respond to --help and --version in the way that MustParse does, see examples
// in the README under "Custom handling of --help and --version".
func (p *Parser) Parse(args []string) error {
err := p.process(args)
if err != nil {
@ -449,6 +539,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 {
@ -502,7 +606,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))
@ -516,6 +620,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
@ -523,7 +636,7 @@ func (p *Parser) process(args []string) error {
// must use explicit for loop, not range, because we manipulate i inside the loop
for i := 0; i < len(args); i++ {
arg := args[i]
if arg == "--" {
if arg == "--" && !allpositional {
allpositional = true
continue
}
@ -548,7 +661,12 @@ func (p *Parser) process(args []string) error {
}
// 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 {
@ -559,7 +677,7 @@ func (p *Parser) process(args []string) error {
}
curCmd = subcmd
p.lastCmd = curCmd
p.subcommand = append(p.subcommand, arg)
continue
}
@ -568,7 +686,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"
@ -582,7 +702,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
@ -671,17 +791,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 !p.config.IgnoreDefault && 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)
}
}
@ -742,6 +871,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

@ -7,15 +7,15 @@ import (
"unicode"
"unicode/utf8"
scalar "github.com/alexflint/go-scalar"
"go.wit.com/dev/alexflint/scalar"
)
var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()
// cardinality tracks how many tokens are expected for a given spec
// - zero is a boolean, which does to expect any value
// - one is an ordinary option that will be parsed from a single token
// - multiple is a slice or map that can accept zero or more tokens
// - 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

@ -5,7 +5,7 @@ import (
"reflect"
"strings"
scalar "github.com/alexflint/go-scalar"
"go.wit.com/dev/alexflint/scalar"
)
// setSliceOrMap parses a sequence of strings into a slice or map. If clear is

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
}

10
test/Makefile Normal file
View File

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

View File

@ -162,8 +162,8 @@ func Example_helpText() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stdout = os.Stdout
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
@ -195,19 +195,18 @@ func Example_helpPlaceholder() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stdout = os.Stdout
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
// output:
// Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
//
// Positional arguments:
// SRC
// DST
//
// Options:
// --optimize LEVEL, -O LEVEL
// optimization level
@ -236,8 +235,8 @@ func Example_helpTextWithSubcommand() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stdout = os.Stdout
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
@ -274,8 +273,8 @@ func Example_helpTextWhenUsingSubcommand() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stdout = os.Stdout
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
@ -311,10 +310,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 +358,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 +394,8 @@ func Example_errorText() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stderr = os.Stdout
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
@ -421,8 +418,8 @@ func Example_errorTextForSubcommand() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stderr = os.Stdout
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
@ -457,8 +454,8 @@ func Example_subcommand() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stderr = os.Stdout
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
@ -505,3 +502,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
}

11
test/go.mod Normal file
View File

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

10
test/go.sum Normal file
View File

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

View File

@ -2,6 +2,7 @@ package arg
import (
"bytes"
"encoding/json"
"fmt"
"net"
"net/mail"
@ -27,11 +28,11 @@ func parse(cmdline string, dest interface{}) error {
}
func pparse(cmdline string, dest interface{}) (*Parser, error) {
return parseWithEnv(cmdline, nil, dest)
return parseWithEnv(Config{}, cmdline, nil, dest)
}
func parseWithEnv(cmdline string, env []string, dest interface{}) (*Parser, error) {
p, err := NewParser(Config{}, dest)
func parseWithEnv(config Config, cmdline string, env []string, dest interface{}) (*Parser, error) {
p, err := NewParser(config, dest)
if err != nil {
return nil, err
}
@ -97,9 +98,9 @@ func TestInt(t *testing.T) {
func TestHexOctBin(t *testing.T) {
var args struct {
Hex int
Oct int
Bin int
Hex int
Oct int
Bin int
Underscored int
}
err := parse("--hex 0xA --oct 0o10 --bin 0b101 --underscored 123_456", &args)
@ -226,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(Config{}, "", []string{}, &args)
require.Error(t, err, "environment variable FOO is required")
}
func TestShortFlag(t *testing.T) {
var args struct {
Foo string `arg:"-f"`
@ -600,6 +609,15 @@ func TestNoMoreOptionsBeforeHelp(t *testing.T) {
assert.NotEqual(t, ErrHelp, err)
}
func TestNoMoreOptionsTwice(t *testing.T) {
var args struct {
X []string `arg:"positional"`
}
err := parse("-- --", &args)
require.NoError(t, err)
assert.Equal(t, []string{"--"}, args.X)
}
func TestHelpFlag(t *testing.T) {
var args struct {
Foo string
@ -683,11 +701,26 @@ func TestMustParse(t *testing.T) {
assert.NotNil(t, parser)
}
func TestMustParseError(t *testing.T) {
var args struct {
Foo []string `default:""`
}
var exitCode int
var stdout bytes.Buffer
mustParseExit = func(code int) { exitCode = code }
mustParseOut = &stdout
os.Args = []string{"example"}
parser := MustParse(&args)
assert.Nil(t, parser)
assert.Equal(t, 2, exitCode)
assert.Contains(t, stdout.String(), "default values are not supported for slice or map fields")
}
func TestEnvironmentVariable(t *testing.T) {
var args struct {
Foo string `arg:"env"`
}
_, err := parseWithEnv("", []string{"FOO=bar"}, &args)
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
require.NoError(t, err)
assert.Equal(t, "bar", args.Foo)
}
@ -696,7 +729,7 @@ func TestEnvironmentVariableNotPresent(t *testing.T) {
var args struct {
NotPresent string `arg:"env"`
}
_, err := parseWithEnv("", nil, &args)
_, err := parseWithEnv(Config{}, "", nil, &args)
require.NoError(t, err)
assert.Equal(t, "", args.NotPresent)
}
@ -705,7 +738,7 @@ func TestEnvironmentVariableOverrideName(t *testing.T) {
var args struct {
Foo string `arg:"env:BAZ"`
}
_, err := parseWithEnv("", []string{"BAZ=bar"}, &args)
_, err := parseWithEnv(Config{}, "", []string{"BAZ=bar"}, &args)
require.NoError(t, err)
assert.Equal(t, "bar", args.Foo)
}
@ -714,7 +747,7 @@ func TestEnvironmentVariableOverrideArgument(t *testing.T) {
var args struct {
Foo string `arg:"env"`
}
_, err := parseWithEnv("--foo zzz", []string{"FOO=bar"}, &args)
_, err := parseWithEnv(Config{}, "--foo zzz", []string{"FOO=bar"}, &args)
require.NoError(t, err)
assert.Equal(t, "zzz", args.Foo)
}
@ -723,7 +756,7 @@ func TestEnvironmentVariableError(t *testing.T) {
var args struct {
Foo int `arg:"env"`
}
_, err := parseWithEnv("", []string{"FOO=bar"}, &args)
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
assert.Error(t, err)
}
@ -731,7 +764,7 @@ func TestEnvironmentVariableRequired(t *testing.T) {
var args struct {
Foo string `arg:"env,required"`
}
_, err := parseWithEnv("", []string{"FOO=bar"}, &args)
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
require.NoError(t, err)
assert.Equal(t, "bar", args.Foo)
}
@ -740,7 +773,7 @@ func TestEnvironmentVariableSliceArgumentString(t *testing.T) {
var args struct {
Foo []string `arg:"env"`
}
_, err := parseWithEnv("", []string{`FOO=bar,"baz, qux"`}, &args)
_, err := parseWithEnv(Config{}, "", []string{`FOO=bar,"baz, qux"`}, &args)
require.NoError(t, err)
assert.Equal(t, []string{"bar", "baz, qux"}, args.Foo)
}
@ -749,7 +782,7 @@ func TestEnvironmentVariableSliceEmpty(t *testing.T) {
var args struct {
Foo []string `arg:"env"`
}
_, err := parseWithEnv("", []string{`FOO=`}, &args)
_, err := parseWithEnv(Config{}, "", []string{`FOO=`}, &args)
require.NoError(t, err)
assert.Len(t, args.Foo, 0)
}
@ -758,7 +791,7 @@ func TestEnvironmentVariableSliceArgumentInteger(t *testing.T) {
var args struct {
Foo []int `arg:"env"`
}
_, err := parseWithEnv("", []string{`FOO=1,99`}, &args)
_, err := parseWithEnv(Config{}, "", []string{`FOO=1,99`}, &args)
require.NoError(t, err)
assert.Equal(t, []int{1, 99}, args.Foo)
}
@ -767,7 +800,7 @@ func TestEnvironmentVariableSliceArgumentFloat(t *testing.T) {
var args struct {
Foo []float32 `arg:"env"`
}
_, err := parseWithEnv("", []string{`FOO=1.1,99.9`}, &args)
_, err := parseWithEnv(Config{}, "", []string{`FOO=1.1,99.9`}, &args)
require.NoError(t, err)
assert.Equal(t, []float32{1.1, 99.9}, args.Foo)
}
@ -776,7 +809,7 @@ func TestEnvironmentVariableSliceArgumentBool(t *testing.T) {
var args struct {
Foo []bool `arg:"env"`
}
_, err := parseWithEnv("", []string{`FOO=true,false,0,1`}, &args)
_, err := parseWithEnv(Config{}, "", []string{`FOO=true,false,0,1`}, &args)
require.NoError(t, err)
assert.Equal(t, []bool{true, false, false, true}, args.Foo)
}
@ -785,7 +818,7 @@ func TestEnvironmentVariableSliceArgumentWrongCsv(t *testing.T) {
var args struct {
Foo []int `arg:"env"`
}
_, err := parseWithEnv("", []string{`FOO=1,99\"`}, &args)
_, err := parseWithEnv(Config{}, "", []string{`FOO=1,99\"`}, &args)
assert.Error(t, err)
}
@ -793,7 +826,7 @@ func TestEnvironmentVariableSliceArgumentWrongType(t *testing.T) {
var args struct {
Foo []bool `arg:"env"`
}
_, err := parseWithEnv("", []string{`FOO=one,two`}, &args)
_, err := parseWithEnv(Config{}, "", []string{`FOO=one,two`}, &args)
assert.Error(t, err)
}
@ -801,7 +834,7 @@ func TestEnvironmentVariableMap(t *testing.T) {
var args struct {
Foo map[int]string `arg:"env"`
}
_, err := parseWithEnv("", []string{`FOO=1=one,99=ninetynine`}, &args)
_, err := parseWithEnv(Config{}, "", []string{`FOO=1=one,99=ninetynine`}, &args)
require.NoError(t, err)
assert.Len(t, args.Foo, 2)
assert.Equal(t, "one", args.Foo[1])
@ -812,11 +845,21 @@ func TestEnvironmentVariableEmptyMap(t *testing.T) {
var args struct {
Foo map[int]string `arg:"env"`
}
_, err := parseWithEnv("", []string{`FOO=`}, &args)
_, err := parseWithEnv(Config{}, "", []string{`FOO=`}, &args)
require.NoError(t, err)
assert.Len(t, args.Foo, 0)
}
func TestEnvironmentVariableWithPrefix(t *testing.T) {
var args struct {
Foo string `arg:"env"`
}
_, err := parseWithEnv(Config{EnvPrefix: "MYAPP_"}, "", []string{"MYAPP_FOO=bar"}, &args)
require.NoError(t, err)
assert.Equal(t, "bar", args.Foo)
}
func TestEnvironmentVariableIgnored(t *testing.T) {
var args struct {
Foo string `arg:"env"`
@ -844,6 +887,24 @@ func TestDefaultValuesIgnored(t *testing.T) {
assert.Equal(t, "", args.Foo)
}
func TestRequiredEnvironmentOnlyVariableIsMissing(t *testing.T) {
var args struct {
Foo string `arg:"required,--,env:FOO"`
}
_, err := parseWithEnv(Config{}, "", []string{""}, &args)
assert.Error(t, err)
}
func TestOptionalEnvironmentOnlyVariable(t *testing.T) {
var args struct {
Foo string `arg:"env:FOO"`
}
_, err := parseWithEnv(Config{}, "", []string{}, &args)
assert.NoError(t, err)
}
func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
var args struct {
Sub *struct {
@ -856,10 +917,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: 2, 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
}
@ -1313,11 +1415,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) {
@ -1348,13 +1494,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) {
@ -1363,7 +1517,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) {
@ -1382,13 +1536,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) {
@ -1402,7 +1564,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)
@ -1419,68 +1581,201 @@ 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)
assert.Equal(t, 2, 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)
}
func TestExitFunctionAndOutStreamGetFilledIn(t *testing.T) {
var args struct{}
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
assert.NotNil(t, p.config.Exit) // go prohibits function pointer comparison
assert.Equal(t, p.config.Out, os.Stdout)
}

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)
}

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)
@ -232,7 +237,7 @@ func (versioned) Version() string {
}
func TestUsageWithVersion(t *testing.T) {
expectedUsage := "example 3.2.1\nUsage: example"
expectedUsage := "Usage: example"
expectedHelp := `
example 3.2.1
@ -255,6 +260,233 @@ Options:
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestUsageWithUserDefinedVersionFlag(t *testing.T) {
expectedUsage := "Usage: example [--version]"
expectedHelp := `
Usage: example [--version]
Options:
--version this is a user-defined version flag
--help, -h display this help and exit
`
var args struct {
ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
}
os.Args[0] = "example"
p, err := NewParser(Config{}, &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 TestUsageWithVersionAndUserDefinedVersionFlag(t *testing.T) {
expectedUsage := "Usage: example [--version]"
expectedHelp := `
Usage: example [--version]
Options:
--version this is a user-defined version flag
--help, -h display this help and exit
`
var args struct {
versioned
ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
}
os.Args[0] = "example"
p, err := NewParser(Config{}, &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()))
}
type subcommand struct {
Number int `arg:"-n,--number" help:"compute something on the given number"`
}
func TestUsageWithVersionAndSubcommand(t *testing.T) {
expectedUsage := "Usage: example <command> [<args>]"
expectedHelp := `
example 3.2.1
Usage: example <command> [<args>]
Options:
--help, -h display this help and exit
--version display version and exit
Commands:
cmd
`
var args struct {
versioned
Cmd *subcommand `arg:"subcommand"`
}
os.Args[0] = "example"
p, err := NewParser(Config{}, &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()))
expectedUsage = "Usage: example cmd [--number NUMBER]"
expectedHelp = `
example 3.2.1
Usage: example cmd [--number NUMBER]
Options:
--number NUMBER, -n NUMBER
compute something on the given number
--help, -h display this help and exit
--version display version and exit
`
_ = p.Parse([]string{"cmd"})
help = bytes.Buffer{}
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
usage = bytes.Buffer{}
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestUsageWithUserDefinedVersionFlagAndSubcommand(t *testing.T) {
expectedUsage := "Usage: example [--version] <command> [<args>]"
expectedHelp := `
Usage: example [--version] <command> [<args>]
Options:
--version this is a user-defined version flag
--help, -h display this help and exit
Commands:
cmd
`
var args struct {
Cmd *subcommand `arg:"subcommand"`
ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
}
os.Args[0] = "example"
p, err := NewParser(Config{}, &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()))
expectedUsage = "Usage: example cmd [--number NUMBER]"
expectedHelp = `
Usage: example cmd [--number NUMBER]
Options:
--number NUMBER, -n NUMBER
compute something on the given number
Global options:
--version this is a user-defined version flag
--help, -h display this help and exit
`
_ = p.Parse([]string{"cmd"})
help = bytes.Buffer{}
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
usage = bytes.Buffer{}
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestUsageWithVersionAndUserDefinedVersionFlagAndSubcommand(t *testing.T) {
expectedUsage := "Usage: example [--version] <command> [<args>]"
expectedHelp := `
Usage: example [--version] <command> [<args>]
Options:
--version this is a user-defined version flag
--help, -h display this help and exit
Commands:
cmd
`
var args struct {
versioned
Cmd *subcommand `arg:"subcommand"`
ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
}
os.Args[0] = "example"
p, err := NewParser(Config{}, &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()))
expectedUsage = "Usage: example cmd [--number NUMBER]"
expectedHelp = `
Usage: example cmd [--number NUMBER]
Options:
--number NUMBER, -n NUMBER
compute something on the given number
Global options:
--version this is a user-defined version flag
--help, -h display this help and exit
`
_ = p.Parse([]string{"cmd"})
help = bytes.Buffer{}
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
usage = bytes.Buffer{}
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
type described struct{}
// Described returns the description for this program
@ -410,6 +642,50 @@ Options:
assert.Equal(t, expectedUsage, usage.String())
}
func TestUsageWithSubcommands(t *testing.T) {
expectedUsage := "Usage: example child [--values VALUES]"
expectedHelp := `
Usage: example child [--values VALUES]
Options:
--values VALUES Values
Global options:
--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"`
} `arg:"subcommand:child"`
}
os.Args[0] = "example"
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
_ = p.Parse([]string{"child"})
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
var help2 bytes.Buffer
p.WriteHelpForSubcommand(&help2, "child")
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")
assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String()))
}
func TestUsageWithNestedSubcommands(t *testing.T) {
expectedUsage := "Usage: example child nested [--enable] OUTPUT"
@ -445,6 +721,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())
@ -466,7 +744,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
@ -506,7 +784,36 @@ 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)
assert.Equal(t, expectedHelp[1:], help.String())
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestUsageWithEmptyPlaceholder(t *testing.T) {
expectedUsage := "Usage: example [-a] [--b] [--c]"
expectedHelp := `
Usage: example [-a] [--b] [--c]
Options:
-a some help for a
--b some help for b
--c, -c some help for c
--help, -h display this help and exit
`
var args struct {
ShortOnly string `arg:"-a,--" placeholder:"" help:"some help for a"`
LongOnly string `arg:"--b" placeholder:"" help:"some help for b"`
Both string `arg:"-c,--c" placeholder:"" help:"some help for c"`
}
p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
@ -553,10 +860,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"`
}
@ -572,19 +885,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]
@ -594,27 +927,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, -1, exitCode)
assert.Equal(t, expectedStdout[1:], stdout.String())
assert.Equal(t, 2, 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
@ -624,12 +948,139 @@ 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, -1, exitCode)
assert.Equal(t, expectedStdout[1:], stdout.String())
assert.Equal(t, 2, 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())
}
func TestHelpShowsPositionalWithDefault(t *testing.T) {
expectedHelp := `
Usage: example [FOO]
Positional arguments:
FOO this is a positional with a default [default: bar]
Options:
--help, -h display this help and exit
`
var args struct {
Foo string `arg:"positional" default:"bar" help:"this is a positional with a default"`
}
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 TestHelpShowsPositionalWithEnv(t *testing.T) {
expectedHelp := `
Usage: example [FOO]
Positional arguments:
FOO this is a positional with an env variable [env: FOO]
Options:
--help, -h display this help and exit
`
var args struct {
Foo string `arg:"positional,env:FOO" help:"this is a positional with an env variable"`
}
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 TestHelpShowsPositionalWithDefaultAndEnv(t *testing.T) {
expectedHelp := `
Usage: example [FOO]
Positional arguments:
FOO this is a positional with a default and an env variable [default: bar, env: FOO]
Options:
--help, -h display this help and exit
`
var args struct {
Foo string `arg:"positional,env:FOO" default:"bar" help:"this is a positional with a default and an env variable"`
}
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())
}

220
usage.go
View File

@ -3,54 +3,37 @@ 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
// Fail prints usage information to p.Config.Out and exits with status code 2.
func (p *Parser) Fail(msg string) {
p.failWithSubcommand(msg, p.cmd)
p.FailSubcommand(msg)
}
// FailSubcommand prints usage information for a specified subcommand to stderr,
// then exits with non-zero status. To write usage information for a top-level
// FailSubcommand prints usage information for a specified subcommand to p.Config.Out,
// then exits with status code 2. To write usage information for a top-level
// subcommand, provide just the name of that subcommand. To write usage
// information for a subcommand that is nested under another subcommand, provide
// a sequence of subcommand names starting with the top-level subcommand and so
// on down the tree.
func (p *Parser) FailSubcommand(msg string, subcommand ...string) error {
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(2)
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 {
@ -81,22 +59,10 @@ func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *command) {
}
}
if p.version != "" {
fmt.Fprintln(w, p.version)
}
// make a list of ancestor commands so that we print with full context
var ancestors []string
ancestor := cmd
for ancestor != nil {
ancestors = append(ancestors, ancestor.name)
ancestor = ancestor.parent
}
// print the beginning of the usage string
fmt.Fprint(w, "Usage:")
for i := len(ancestors) - 1; i >= 0; i-- {
fmt.Fprint(w, " "+ancestors[i])
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 +123,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,34 +195,55 @@ 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:
positionals = append(positionals, spec)
case spec.long != "":
longOptions = append(longOptions, spec)
if spec.long == "version" {
hasVersionOption = true
}
case spec.short != "":
shortOptions = append(shortOptions, spec)
case spec.short == "" && spec.long == "":
envOnlyOptions = append(envOnlyOptions, spec)
}
}
// obtain a flattened list of options from all ancestors
// also determine if any ancestor has a version option spec
var globals []*spec
ancestor := cmd.parent
for ancestor != nil {
for _, spec := range ancestor.specs {
if spec.long == "version" {
hasVersionOption = true
break
}
}
globals = append(globals, ancestor.specs...)
ancestor = ancestor.parent
}
if p.description != "" {
fmt.Fprintln(w, p.description)
}
p.writeUsageForSubcommand(w, cmd)
if !hasVersionOption && p.version != "" {
fmt.Fprintln(w, p.version)
}
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, withDefault(spec.defaultString), withEnv(spec.env))
}
}
@ -252,14 +258,6 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
}
}
// obtain a flattened list of options from all ancestors
var globals []*spec
ancestor := cmd.parent
for ancestor != nil {
globals = append(globals, ancestor.specs...)
ancestor = ancestor.parent
}
// write the list of global options
if len(globals) > 0 {
fmt.Fprint(w, "\nGlobal options:\n")
@ -275,7 +273,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,17 +281,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) {
@ -305,34 +313,30 @@ 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 {
if spec.cardinality == zero {
// if the user omits the placeholder tag then we pick one automatically,
// but if the user explicitly specifies an empty placeholder then we
// leave out the placeholder in the help message
if spec.cardinality == zero || spec.placeholder == "" {
return form
}
return form + " " + spec.placeholder

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +0,0 @@
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=

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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