Compare commits
355 Commits
brettlangd
...
main
Author | SHA1 | Date |
---|---|---|
|
5716f9af0d | |
|
3d847431ab | |
|
f92d210ca7 | |
|
438bbfff1e | |
|
efb1be7122 | |
|
51d9bef113 | |
|
cb7e5c1905 | |
|
9b5c76b1c4 | |
|
b218ad854d | |
|
dcb5577c2b | |
|
d10a064207 | |
|
a5045bbe85 | |
|
3925edf11a | |
|
12fffac1d8 | |
|
b13a62172a | |
|
7cf32414af | |
|
bdb7560b8d | |
|
50166cae2c | |
|
7fd624cf1c | |
|
bf156d17a3 | |
|
3673177bf9 | |
|
3de7278c4f | |
|
b8282df4c4 | |
|
ec0ced7467 | |
|
0cc152dce5 | |
|
67353a8bcf | |
|
af368523db | |
|
b6422dcbc3 | |
|
56ee7c97ac | |
|
177b84441e | |
|
c087d71802 | |
|
c992aa8627 | |
|
bed89eb683 | |
|
4ed4ce751f | |
|
a7c40c36a3 | |
|
bee5cf5d7c | |
|
aa844c7de9 | |
|
dfca71d159 | |
|
188bd31bf6 | |
|
8a917260c3 | |
|
3ddfffdcd3 | |
|
68948b2ac1 | |
|
be792f1f8b | |
|
8e35a4f0d4 | |
|
84ddf1d244 | |
|
6b16520795 | |
|
0af6f25365 | |
|
530fcb84d4 | |
|
582e6d537a | |
|
bf629a16cb | |
|
f02da4cd10 | |
|
e7a4f77ed0 | |
|
960d38c3ce | |
|
0142b0b842 | |
|
5ec29ce755 | |
|
8e9f60aafc | |
|
660b9045e1 | |
|
c73f38cd54 | |
|
463902ef7d | |
|
259c83fd5a | |
|
18623d869b | |
|
b928a1839a | |
|
ccf62e0ffc | |
|
5f10667949 | |
|
c3cac76438 | |
|
0280e6e591 | |
|
e25b4707a7 | |
|
df28e7154b | |
|
5dbdd5d0c5 | |
|
efae1938fd | |
|
c0a8e20a0a | |
|
5036dce2d6 | |
|
cef66fd2f6 | |
|
727f8533ac | |
|
3489ea5b2e | |
|
763072452f | |
|
3d95a706a6 | |
|
d949871b67 | |
|
9d5e97ac8a | |
|
67f7183b85 | |
|
522dbbcea8 | |
|
27c832b934 | |
|
197e226c77 | |
|
dbc2ba5d0c | |
|
4fc9666f79 | |
|
11f9b624a9 | |
|
7f4979a06e | |
|
0c21f821f8 | |
|
ea0f540c40 | |
|
74af96c6cc | |
|
c8b9567d1b | |
|
ebd7a68a06 | |
|
23b2b67fe2 | |
|
b48371a62f | |
|
f0f44b65d1 | |
|
5fb236a65d | |
|
d3706100bf | |
|
25d4d1c864 | |
|
3bf2a5e78a | |
|
a87d80089a | |
|
bf32f08247 | |
|
b47d6e3da6 | |
|
a4afd6a849 | |
|
f2f876420c | |
|
66cb696e79 | |
|
0f0b4b5c3f | |
|
b157e8d10a | |
|
ff38a63b36 | |
|
3d59e5e89e | |
|
eb0393e9bc | |
|
fa12c02e81 | |
|
7cc8da61cf | |
|
c9b504edc1 | |
|
679be43af3 | |
|
2e81334206 | |
|
9d937ba6c9 | |
|
1e81bb6866 | |
|
4354574615 | |
|
473453d8c8 | |
|
a84487a43a | |
|
a0937d1b58 | |
|
01a9fab8d7 | |
|
fe4a138ac8 | |
|
6a01a15f75 | |
|
d4b9b2a008 | |
|
a80336128c | |
|
57f610284f | |
|
91214e01ea | |
|
0100c0a411 | |
|
bb4e7fd4b0 | |
|
ccf882dca7 | |
|
9949860eb3 | |
|
23b96d7aac | |
|
1dfefdc43e | |
|
f4eb7f3a58 | |
|
113aef7114 | |
|
cec6b0d378 | |
|
172800ff9a | |
|
cf2205c84d | |
|
4f2ab5c009 | |
|
ec285c8ec4 | |
|
bd6844a20d | |
|
efe5cdf4da | |
|
b099bc916b | |
|
bfa189218e | |
|
2a91531140 | |
|
788c166025 | |
|
2a23168641 | |
|
aa6cb95149 | |
|
438a91dba1 | |
|
04c3fdbd80 | |
|
faebd3e0f2 | |
|
b91c03d2c6 | |
|
7b1d8470a9 | |
|
96c756c382 | |
|
c47edd0324 | |
|
a68c3d0653 | |
|
6be0398e80 | |
|
2a3b5ea3cb | |
|
d4bb56e096 | |
|
912ef0c3e4 | |
|
2bc58b597b | |
|
65205f111c | |
|
68f1691080 | |
|
f78dec8769 | |
|
7a71aa1b20 | |
|
388f831e68 | |
|
40ed78b0fa | |
|
efbf84df4c | |
|
499991fce1 | |
|
854aa644a6 | |
|
1fc1a6f6df | |
|
9fccfd3d6a | |
|
65ef631f5f | |
|
af757bea98 | |
|
ce896f3df9 | |
|
7ac7956369 | |
|
b43f8e2e65 | |
|
063a48797d | |
|
cfdee944f9 | |
|
6f3675fdf1 | |
|
17bbf2e7ef | |
|
ce4cd0ce03 | |
|
82c2a36dd7 | |
|
db9f869c99 | |
|
c24567c12e | |
|
5df19ebe00 | |
|
338e831a84 | |
|
e9c71eb4fa | |
|
cb4e079d13 | |
|
2cc1f136b1 | |
|
711618869d | |
|
7e2466d707 | |
|
f5d3733c0a | |
|
5943b1ad42 | |
|
9f5522668a | |
|
cfd894f446 | |
|
33db14a48b | |
|
ced05bfe8a | |
|
9d4521ce8b | |
|
c49d847704 | |
|
c3a019cdb8 | |
|
904e039267 | |
|
c0c7a3ba8a | |
|
e0fc08f7ad | |
|
7ac060af18 | |
|
809e9060d0 | |
|
45d0915afc | |
|
84e7a764db | |
|
cc768447a7 | |
|
5d3ebcceee | |
|
0c95297990 | |
|
873f3c2cf4 | |
|
233d378a50 | |
|
8baf7040d7 | |
|
11a27074fc | |
|
e6003d3b6a | |
|
9f37d5f600 | |
|
fcdfbc090b | |
|
990e87d80d | |
|
bd97edec87 | |
|
3c5e61a292 | |
|
b83047068d | |
|
15bf383f1d | |
|
edd1af4667 | |
|
6de9e789a9 | |
|
3392c173d7 | |
|
e2ce620ee4 | |
|
c6473c4586 | |
|
e55b361498 | |
|
a68d6000b6 | |
|
93fcb0e87d | |
|
c8c61cf8bb | |
|
f2f7bdbbd7 | |
|
a15b6ad670 | |
|
87be2d9790 | |
|
5b649de043 | |
|
237c5e2b23 | |
|
fb1ae1c3e0 | |
|
15b9bcfbb4 | |
|
39decf197f | |
|
a78c6ded26 | |
|
af12b7cfc2 | |
|
6a796e2c41 | |
|
4e977796af | |
|
ddec9e9e4f | |
|
2267a58718 | |
|
7df132abe8 | |
|
f282f71f26 | |
|
e2dda40825 | |
|
e86673b20a | |
|
9edf2ebc95 | |
|
b8678d4045 | |
|
78d30a555c | |
|
7b1d9ef23f | |
|
42c2ab5ac6 | |
|
f519755eae | |
|
2952bf0265 | |
|
891a07ec29 | |
|
e56211335f | |
|
6266d3e5b7 | |
|
1ec799ffcf | |
|
6b4ab7355c | |
|
57836b82be | |
|
f8987d1105 | |
|
e5a1f3c999 | |
|
25cd36cac6 | |
|
1c224f495b | |
|
f40417ef11 | |
|
e85cc7a2e2 | |
|
d7246f2485 | |
|
27a7b2fb3d | |
|
d615e5c1d8 | |
|
fb7d95b61b | |
|
a6af419fff | |
|
f1aabd5026 | |
|
96b097bef3 | |
|
5225bc2f3c | |
|
f7c0423bd1 | |
|
89714b6f48 | |
|
fa5fe315f8 | |
|
488fd7e82a | |
|
75bf1a1525 | |
|
074ee5f759 | |
|
4d71204936 | |
|
b9375a2e66 | |
|
74dd5a2c5a | |
|
6f2f3b4bf6 | |
|
b1eda2c7b6 | |
|
51337ded77 | |
|
d4cc703210 | |
|
0cc8e30fd6 | |
|
a0df5f3391 | |
|
59fccacb26 | |
|
ba9514f0be | |
|
d7961941f0 | |
|
398a01ebab | |
|
fb97335a13 | |
|
cef6506c97 | |
|
992acaf408 | |
|
58e62faa3d | |
|
8111804d17 | |
|
b413f8dfb0 | |
|
e6e0f59a17 | |
|
9e6f80aa90 | |
|
9173d259ef | |
|
d4c2b35b2e | |
|
8488cf10ce | |
|
c4c162448c | |
|
2c249ee1fc | |
|
44a8b85d82 | |
|
38c51f4cab | |
|
6859799559 | |
|
765ccf7459 | |
|
ec576f9765 | |
|
b658405f70 | |
|
9030aa1348 | |
|
bf73829f30 | |
|
db27431153 | |
|
7c77c70f85 | |
|
03900620e2 | |
|
12fa37d10d | |
|
e6fdb157e9 | |
|
f882700b72 | |
|
c453aa1a28 | |
|
34954f45ce | |
|
6e9648cac6 | |
|
a5617823b0 | |
|
5800b89ce9 | |
|
34b52501bd | |
|
e71d6514f4 | |
|
45474a9b25 | |
|
1488562b1e | |
|
aaae1550b7 | |
|
77dd0df006 | |
|
a1c72f6aa9 | |
|
c0809e537f | |
|
c9584269b9 | |
|
9a30acda05 | |
|
e389d7f782 | |
|
8fee8f7bbe | |
|
95761fa14a | |
|
b5933a0ea8 | |
|
93247e2f3b | |
|
865cc5a973 | |
|
64a4bab550 | |
|
ed2b19f2bb | |
|
e560d079ba | |
|
8dd29d34bf | |
|
b1ec8c9093 | |
|
c9155bb0c3 | |
|
f8ea16beee | |
|
9aad09fe14 | |
|
f89698667c | |
|
0c0f9a53ac |
|
@ -0,0 +1 @@
|
|||
github: [alexflint]
|
Binary file not shown.
After Width: | Height: | Size: 9.8 KiB |
|
@ -0,0 +1,37 @@
|
|||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
|
||||
build_and_test:
|
||||
name: Build and test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go: ['1.20', '1.21', '1.22']
|
||||
|
||||
steps:
|
||||
- id: go
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build
|
||||
run: go build -v .
|
||||
|
||||
- name: Test
|
||||
run: go test -v -coverprofile=profile.cov .
|
||||
|
||||
- name: Send coverage
|
||||
run: bash <(curl -s https://codecov.io/bash) -f profile.cov
|
|
@ -22,3 +22,5 @@ _testmain.go
|
|||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
go.*
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
language: go
|
||||
go:
|
||||
- tip
|
||||
before_install:
|
||||
- go get github.com/axw/gocov/gocov
|
||||
- go get github.com/mattn/goveralls
|
||||
- if ! go get github.com/golang/tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi
|
||||
script:
|
||||
- $HOME/gopath/bin/goveralls -service=travis-ci
|
|
@ -0,0 +1,11 @@
|
|||
all:
|
||||
@echo
|
||||
@echo
|
||||
|
||||
clean:
|
||||
rm -f go.*
|
||||
|
||||
redomod:
|
||||
rm -f go.*
|
||||
GO111MODULE= go mod init
|
||||
GO111MODULE= go mod tidy
|
729
README.md
729
README.md
|
@ -1,10 +1,20 @@
|
|||
[](https://godoc.org/github.com/alexflint/go-arg)
|
||||
[](https://travis-ci.org/alexflint/go-arg)
|
||||
[](https://coveralls.io/github/alexflint/go-arg?branch=master)
|
||||
<h1 align="center">
|
||||
<img src="./.github/banner.jpg" alt="go-arg" height="250px">
|
||||
<br>
|
||||
go-arg
|
||||
</br>
|
||||
</h1>
|
||||
<h4 align="center">Struct-based argument parsing for Go</h4>
|
||||
<p align="center">
|
||||
<a href="https://sourcegraph.com/github.com/alexflint/go-arg?badge"><img src="https://sourcegraph.com/github.com/alexflint/go-arg/-/badge.svg" alt="Sourcegraph"></a>
|
||||
<a href="https://pkg.go.dev/github.com/alexflint/go-arg"><img src="https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square" alt="Documentation"></a>
|
||||
<a href="https://github.com/alexflint/go-arg/actions"><img src="https://github.com/alexflint/go-arg/workflows/Go/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://codecov.io/gh/alexflint/go-arg"><img src="https://codecov.io/gh/alexflint/go-arg/branch/master/graph/badge.svg" alt="Coverage Status"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/alexflint/go-arg"><img src="https://goreportcard.com/badge/github.com/alexflint/go-arg" alt="Go Report Card"></a>
|
||||
</p>
|
||||
<br>
|
||||
|
||||
## Structured argument parsing for Go
|
||||
|
||||
Declare the command line arguments your program accepts by defining a struct.
|
||||
Declare command line arguments for your program by defining a struct.
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
|
@ -20,20 +30,26 @@ $ ./example --foo=hello --bar
|
|||
hello true
|
||||
```
|
||||
|
||||
### Installation
|
||||
|
||||
```shell
|
||||
go get github.com/alexflint/go-arg
|
||||
```
|
||||
|
||||
### Required arguments
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Foo string `arg:"required"`
|
||||
Bar bool
|
||||
ID int `arg:"required"`
|
||||
Timeout time.Duration
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example
|
||||
usage: example --foo FOO [--bar]
|
||||
error: --foo is required
|
||||
Usage: example --id ID [--timeout TIMEOUT]
|
||||
error: --id is required
|
||||
```
|
||||
|
||||
### Positional arguments
|
||||
|
@ -48,33 +64,121 @@ fmt.Println("Input:", args.Input)
|
|||
fmt.Println("Output:", args.Output)
|
||||
```
|
||||
|
||||
```
|
||||
```shell
|
||||
$ ./example src.txt x.out y.out z.out
|
||||
Input: src.txt
|
||||
Output: [x.out y.out z.out]
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Workers int `arg:"env"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
fmt.Println("Workers:", args.Workers)
|
||||
```
|
||||
|
||||
```shell
|
||||
$ WORKERS=4 ./example
|
||||
Workers: 4
|
||||
```
|
||||
|
||||
```shell
|
||||
$ WORKERS=4 ./example --workers=6
|
||||
Workers: 6
|
||||
```
|
||||
|
||||
You can also override the name of the environment variable:
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Workers int `arg:"env:NUM_WORKERS"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
fmt.Println("Workers:", args.Workers)
|
||||
```
|
||||
|
||||
```shell
|
||||
$ NUM_WORKERS=4 ./example
|
||||
Workers: 4
|
||||
```
|
||||
|
||||
You can provide multiple values in environment variables using commas:
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Workers []int `arg:"env"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
fmt.Println("Workers:", args.Workers)
|
||||
```
|
||||
|
||||
```shell
|
||||
$ WORKERS='1,99' ./example
|
||||
Workers: [1 99]
|
||||
```
|
||||
|
||||
Command line arguments take precedence over environment variables:
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Workers int `arg:"--count,env:NUM_WORKERS"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
fmt.Println("Workers:", args.Workers)
|
||||
```
|
||||
|
||||
```shell
|
||||
$ NUM_WORKERS=6 ./example
|
||||
Workers: 6
|
||||
$ NUM_WORKERS=6 ./example --count 4
|
||||
Workers: 4
|
||||
```
|
||||
|
||||
Configuring a global environment variable name prefix is also possible:
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Workers int `arg:"--count,env:NUM_WORKERS"`
|
||||
}
|
||||
|
||||
p, err := arg.NewParser(arg.Config{
|
||||
EnvPrefix: "MYAPP_",
|
||||
}, &args)
|
||||
|
||||
p.MustParse(os.Args[1:])
|
||||
fmt.Println("Workers:", args.Workers)
|
||||
```
|
||||
|
||||
```shell
|
||||
$ MYAPP_NUM_WORKERS=6 ./example
|
||||
Workers: 6
|
||||
```
|
||||
|
||||
### Usage strings
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Input string `arg:"positional"`
|
||||
Output []string `arg:"positional"`
|
||||
Verbose bool `arg:"-v,help:verbosity level"`
|
||||
Dataset string `arg:"help:dataset to use"`
|
||||
Optimize int `arg:"-O,help:optimization level"`
|
||||
Verbose bool `arg:"-v,--verbose" help:"verbosity level"`
|
||||
Dataset string `help:"dataset to use"`
|
||||
Optimize int `arg:"-O" help:"optimization level"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example -h
|
||||
usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]]
|
||||
Usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]]
|
||||
|
||||
positional arguments:
|
||||
input
|
||||
output
|
||||
Positional arguments:
|
||||
INPUT
|
||||
OUTPUT
|
||||
|
||||
options:
|
||||
Options:
|
||||
--verbose, -v verbosity level
|
||||
--dataset DATASET dataset to use
|
||||
--optimize OPTIMIZE, -O OPTIMIZE
|
||||
|
@ -86,14 +190,38 @@ options:
|
|||
|
||||
```go
|
||||
var args struct {
|
||||
Foo string
|
||||
Foo string `default:"abc"`
|
||||
Bar bool
|
||||
}
|
||||
args.Foo = "default value"
|
||||
arg.MustParse(&args)
|
||||
```
|
||||
|
||||
Command line arguments take precedence over environment variables, which take precedence over default values. This means that we check whether a certain option was provided on the command line, then if not, we check for an environment variable (only if an `env` tag was provided), then if none is found, we check for a `default` tag containing a default value.
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Test string `arg:"-t,env:TEST" default:"something"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
```
|
||||
|
||||
#### Ignoring environment variables and/or default values
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Test string `arg:"-t,env:TEST" default:"something"`
|
||||
}
|
||||
|
||||
p, err := arg.NewParser(arg.Config{
|
||||
IgnoreEnv: true,
|
||||
IgnoreDefault: true,
|
||||
}, &args)
|
||||
|
||||
err = p.Parse(os.Args[1:])
|
||||
```
|
||||
|
||||
### Arguments with multiple values
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Database string
|
||||
|
@ -108,22 +236,565 @@ fmt.Printf("Fetching the following IDs from %s: %q", args.Database, args.IDs)
|
|||
Fetching the following IDs from foo: [1 2 3]
|
||||
```
|
||||
|
||||
### Installation
|
||||
### Arguments that can be specified multiple times, mixed with positionals
|
||||
|
||||
```shell
|
||||
go get github.com/alexflint/go-arg
|
||||
```go
|
||||
var args struct {
|
||||
Commands []string `arg:"-c,separate"`
|
||||
Files []string `arg:"-f,separate"`
|
||||
Databases []string `arg:"positional"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
```
|
||||
|
||||
### Documentation
|
||||
```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]
|
||||
```
|
||||
|
||||
https://godoc.org/github.com/alexflint/go-arg
|
||||
### Arguments with keys and values
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
UserIDs map[string]int
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
fmt.Println(args.UserIDs)
|
||||
```
|
||||
|
||||
```shell
|
||||
./example --userids john=123 mary=456
|
||||
map[john:123 mary:456]
|
||||
```
|
||||
|
||||
### Version strings
|
||||
|
||||
```go
|
||||
type args struct {
|
||||
...
|
||||
}
|
||||
|
||||
func (args) Version() string {
|
||||
return "someprogram 4.3.0"
|
||||
}
|
||||
|
||||
func main() {
|
||||
var args args
|
||||
arg.MustParse(&args)
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example --version
|
||||
someprogram 4.3.0
|
||||
```
|
||||
|
||||
> **Note**
|
||||
> If a `--version` flag is defined in `args` or any subcommand, it overrides the built-in versioning.
|
||||
|
||||
### Custom validation
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Foo string
|
||||
Bar string
|
||||
}
|
||||
p := arg.MustParse(&args)
|
||||
if args.Foo == "" && args.Bar == "" {
|
||||
p.Fail("you must provide either --foo or --bar")
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
./example
|
||||
Usage: samples [--foo FOO] [--bar BAR]
|
||||
error: you must provide either --foo or --bar
|
||||
```
|
||||
|
||||
### Overriding option names
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Short string `arg:"-s"`
|
||||
Long string `arg:"--custom-long-option"`
|
||||
ShortAndLong string `arg:"-x,--my-option"`
|
||||
OnlyShort string `arg:"-o,--"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example --help
|
||||
Usage: example [-o ONLYSHORT] [--short SHORT] [--custom-long-option CUSTOM-LONG-OPTION] [--my-option MY-OPTION]
|
||||
|
||||
Options:
|
||||
--short SHORT, -s SHORT
|
||||
--custom-long-option CUSTOM-LONG-OPTION
|
||||
--my-option MY-OPTION, -x MY-OPTION
|
||||
-o ONLYSHORT
|
||||
--help, -h display this help and exit
|
||||
```
|
||||
|
||||
### Embedded structs
|
||||
|
||||
The fields of embedded structs are treated just like regular fields:
|
||||
|
||||
```go
|
||||
type DatabaseOptions struct {
|
||||
Host string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
type LogOptions struct {
|
||||
LogFile string
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
func main() {
|
||||
var args struct {
|
||||
DatabaseOptions
|
||||
LogOptions
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
}
|
||||
```
|
||||
|
||||
As usual, any field tagged with `arg:"-"` is ignored.
|
||||
|
||||
### Supported types
|
||||
|
||||
The following types may be used as arguments:
|
||||
- built-in integer types: `int, int8, int16, int32, int64, byte, rune`
|
||||
- built-in floating point types: `float32, float64`
|
||||
- strings
|
||||
- booleans
|
||||
- URLs represented as `url.URL`
|
||||
- time durations represented as `time.Duration`
|
||||
- email addresses represented as `mail.Address`
|
||||
- MAC addresses represented as `net.HardwareAddr`
|
||||
- pointers to any of the above
|
||||
- slices of any of the above
|
||||
- maps using any of the above as keys and values
|
||||
- any type that implements `encoding.TextUnmarshaler`
|
||||
|
||||
### Custom parsing
|
||||
|
||||
Implement `encoding.TextUnmarshaler` to define your own parsing logic.
|
||||
|
||||
```go
|
||||
// Accepts command line arguments of the form "head.tail"
|
||||
type NameDotName struct {
|
||||
Head, Tail string
|
||||
}
|
||||
|
||||
func (n *NameDotName) UnmarshalText(b []byte) error {
|
||||
s := string(b)
|
||||
pos := strings.Index(s, ".")
|
||||
if pos == -1 {
|
||||
return fmt.Errorf("missing period in %s", s)
|
||||
}
|
||||
n.Head = s[:pos]
|
||||
n.Tail = s[pos+1:]
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
var args struct {
|
||||
Name NameDotName
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
fmt.Printf("%#v\n", args.Name)
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example --name=foo.bar
|
||||
main.NameDotName{Head:"foo", Tail:"bar"}
|
||||
|
||||
$ ./example --name=oops
|
||||
Usage: example [--name NAME]
|
||||
error: error processing --name: missing period in "oops"
|
||||
```
|
||||
|
||||
### Custom parsing with default values
|
||||
|
||||
Implement `encoding.TextMarshaler` to define your own default value strings:
|
||||
|
||||
```go
|
||||
// Accepts command line arguments of the form "head.tail"
|
||||
type NameDotName struct {
|
||||
Head, Tail string
|
||||
}
|
||||
|
||||
func (n *NameDotName) UnmarshalText(b []byte) error {
|
||||
// same as previous example
|
||||
}
|
||||
|
||||
// this is only needed if you want to display a default value in the usage string
|
||||
func (n *NameDotName) MarshalText() ([]byte, error) {
|
||||
return []byte(fmt.Sprintf("%s.%s", n.Head, n.Tail)), nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
var args struct {
|
||||
Name NameDotName `default:"file.txt"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
fmt.Printf("%#v\n", args.Name)
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example --help
|
||||
Usage: test [--name NAME]
|
||||
|
||||
Options:
|
||||
--name NAME [default: file.txt]
|
||||
--help, -h display this help and exit
|
||||
|
||||
$ ./example
|
||||
main.NameDotName{Head:"file", Tail:"txt"}
|
||||
```
|
||||
|
||||
### Custom placeholders
|
||||
|
||||
Use the `placeholder` tag to control which placeholder text is used in the usage text.
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Input string `arg:"positional" placeholder:"SRC"`
|
||||
Output []string `arg:"positional" placeholder:"DST"`
|
||||
Optimize int `arg:"-O" help:"optimization level" placeholder:"LEVEL"`
|
||||
MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" placeholder:"N"`
|
||||
}
|
||||
arg.MustParse(&args)
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example -h
|
||||
Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
|
||||
|
||||
Positional arguments:
|
||||
SRC
|
||||
DST
|
||||
|
||||
Options:
|
||||
--optimize LEVEL, -O LEVEL
|
||||
optimization level
|
||||
--maxjobs N, -j N maximum number of simultaneous jobs
|
||||
--help, -h display this help and exit
|
||||
```
|
||||
|
||||
### Description strings
|
||||
|
||||
A descriptive message can be added at the top of the help text by implementing
|
||||
a `Description` function that returns a string.
|
||||
|
||||
```go
|
||||
type args struct {
|
||||
Foo string
|
||||
}
|
||||
|
||||
func (args) Description() string {
|
||||
return "this program does this and that"
|
||||
}
|
||||
|
||||
func main() {
|
||||
var args args
|
||||
arg.MustParse(&args)
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example -h
|
||||
this program does this and that
|
||||
Usage: example [--foo FOO]
|
||||
|
||||
Options:
|
||||
--foo FOO
|
||||
--help, -h display this help and exit
|
||||
```
|
||||
|
||||
Similarly an epilogue can be added at the end of the help text by implementing
|
||||
the `Epilogue` function.
|
||||
|
||||
```go
|
||||
type args struct {
|
||||
Foo string
|
||||
}
|
||||
|
||||
func (args) Epilogue() string {
|
||||
return "For more information visit github.com/alexflint/go-arg"
|
||||
}
|
||||
|
||||
func main() {
|
||||
var args args
|
||||
arg.MustParse(&args)
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example -h
|
||||
Usage: example [--foo FOO]
|
||||
|
||||
Options:
|
||||
--foo FOO
|
||||
--help, -h display this help and exit
|
||||
|
||||
For more information visit github.com/alexflint/go-arg
|
||||
```
|
||||
|
||||
### Subcommands
|
||||
|
||||
Subcommands are commonly used in tools that wish to group multiple functions into a single program. An example is the `git` tool:
|
||||
```shell
|
||||
$ git checkout [arguments specific to checking out code]
|
||||
$ git commit [arguments specific to committing]
|
||||
$ git push [arguments specific to pushing]
|
||||
```
|
||||
|
||||
The strings "checkout", "commit", and "push" are different from simple positional arguments because the options available to the user change depending on which subcommand they choose.
|
||||
|
||||
This can be implemented with `go-arg` as follows:
|
||||
|
||||
```go
|
||||
type CheckoutCmd struct {
|
||||
Branch string `arg:"positional"`
|
||||
Track bool `arg:"-t"`
|
||||
}
|
||||
type CommitCmd struct {
|
||||
All bool `arg:"-a"`
|
||||
Message string `arg:"-m"`
|
||||
}
|
||||
type PushCmd struct {
|
||||
Remote string `arg:"positional"`
|
||||
Branch string `arg:"positional"`
|
||||
SetUpstream bool `arg:"-u"`
|
||||
}
|
||||
var args struct {
|
||||
Checkout *CheckoutCmd `arg:"subcommand:checkout"`
|
||||
Commit *CommitCmd `arg:"subcommand:commit"`
|
||||
Push *PushCmd `arg:"subcommand:push"`
|
||||
Quiet bool `arg:"-q"` // this flag is global to all subcommands
|
||||
}
|
||||
|
||||
arg.MustParse(&args)
|
||||
|
||||
switch {
|
||||
case args.Checkout != nil:
|
||||
fmt.Printf("checkout requested for branch %s\n", args.Checkout.Branch)
|
||||
case args.Commit != nil:
|
||||
fmt.Printf("commit requested with message \"%s\"\n", args.Commit.Message)
|
||||
case args.Push != nil:
|
||||
fmt.Printf("push requested from %s to %s\n", args.Push.Branch, args.Push.Remote)
|
||||
}
|
||||
```
|
||||
|
||||
Some additional rules apply when working with subcommands:
|
||||
* The `subcommand` tag can only be used with fields that are pointers to structs
|
||||
* Any struct that contains a subcommand must not contain any positionals
|
||||
|
||||
This package allows to have a program that accepts subcommands, but also does something else
|
||||
when no subcommands are specified.
|
||||
If on the other hand you want the program to terminate when no subcommands are specified,
|
||||
the recommended way is:
|
||||
|
||||
```go
|
||||
p := arg.MustParse(&args)
|
||||
if p.Subcommand() == nil {
|
||||
p.Fail("missing subcommand")
|
||||
}
|
||||
```
|
||||
|
||||
### Custom handling of --help and --version
|
||||
|
||||
The following reproduces the internal logic of `MustParse` for the simple case where
|
||||
you are not using subcommands or --version. This allows you to respond
|
||||
programatically to --help, and to any errors that come up.
|
||||
|
||||
```go
|
||||
var args struct {
|
||||
Something string
|
||||
}
|
||||
|
||||
p, err := arg.NewParser(arg.Config{}, &args)
|
||||
if err != nil {
|
||||
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
|
||||
}
|
||||
|
||||
err = p.Parse(os.Args[1:])
|
||||
switch {
|
||||
case err == arg.ErrHelp: // indicates that user wrote "--help" on command line
|
||||
p.WriteHelp(os.Stdout)
|
||||
os.Exit(0)
|
||||
case err != nil:
|
||||
fmt.Printf("error: %v\n", err)
|
||||
p.WriteUsage(os.Stdout)
|
||||
os.Exit(1)
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
$ go run ./example --help
|
||||
Usage: ./example --something SOMETHING
|
||||
|
||||
Options:
|
||||
--something SOMETHING
|
||||
--help, -h display this help and exit
|
||||
|
||||
$ ./example --wrong
|
||||
error: unknown argument --wrong
|
||||
Usage: ./example --something SOMETHING
|
||||
|
||||
$ ./example
|
||||
error: --something is required
|
||||
Usage: ./example --something SOMETHING
|
||||
```
|
||||
|
||||
To also handle --version programatically, use the following:
|
||||
|
||||
```go
|
||||
type args struct {
|
||||
Something string
|
||||
}
|
||||
|
||||
func (args) Version() string {
|
||||
return "1.2.3"
|
||||
}
|
||||
|
||||
func main() {
|
||||
var args args
|
||||
p, err := arg.NewParser(arg.Config{}, &args)
|
||||
if err != nil {
|
||||
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
|
||||
}
|
||||
|
||||
err = p.Parse(os.Args[1:])
|
||||
switch {
|
||||
case err == arg.ErrHelp: // found "--help" on command line
|
||||
p.WriteHelp(os.Stdout)
|
||||
os.Exit(0)
|
||||
case err == arg.ErrVersion: // found "--version" on command line
|
||||
fmt.Println(args.Version())
|
||||
os.Exit(0)
|
||||
case err != nil:
|
||||
fmt.Printf("error: %v\n", err)
|
||||
p.WriteUsage(os.Stdout)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("got %q\n", args.Something)
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example --version
|
||||
1.2.3
|
||||
|
||||
$ go run ./example --help
|
||||
1.2.3
|
||||
Usage: example --something SOMETHING
|
||||
|
||||
Options:
|
||||
--something SOMETHING
|
||||
--help, -h display this help and exit
|
||||
|
||||
$ ./example --wrong
|
||||
1.2.3
|
||||
error: unknown argument --wrong
|
||||
Usage: example --something SOMETHING
|
||||
|
||||
$ ./example
|
||||
error: --something is required
|
||||
Usage: example --something SOMETHING
|
||||
```
|
||||
|
||||
To generate subcommand-specific help messages, use the following most general version
|
||||
(this also works in absence of subcommands but is a bit more complex):
|
||||
|
||||
```go
|
||||
type fetchCmd struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
type args struct {
|
||||
Something string
|
||||
Fetch *fetchCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
func (args) Version() string {
|
||||
return "1.2.3"
|
||||
}
|
||||
|
||||
func main() {
|
||||
var args args
|
||||
p, err := arg.NewParser(arg.Config{}, &args)
|
||||
if err != nil {
|
||||
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
|
||||
}
|
||||
|
||||
err = p.Parse(os.Args[1:])
|
||||
switch {
|
||||
case err == arg.ErrHelp: // found "--help" on command line
|
||||
p.WriteHelpForSubcommand(os.Stdout, p.SubcommandNames()...)
|
||||
os.Exit(0)
|
||||
case err == arg.ErrVersion: // found "--version" on command line
|
||||
fmt.Println(args.Version())
|
||||
os.Exit(0)
|
||||
case err != nil:
|
||||
fmt.Printf("error: %v\n", err)
|
||||
p.WriteUsageForSubcommand(os.Stdout, p.SubcommandNames()...)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```shell
|
||||
$ ./example --version
|
||||
1.2.3
|
||||
|
||||
$ ./example --help
|
||||
1.2.3
|
||||
Usage: example [--something SOMETHING] <command> [<args>]
|
||||
|
||||
Options:
|
||||
--something SOMETHING
|
||||
--help, -h display this help and exit
|
||||
--version display version and exit
|
||||
|
||||
Commands:
|
||||
fetch
|
||||
|
||||
$ ./example fetch --help
|
||||
1.2.3
|
||||
Usage: example fetch [--count COUNT]
|
||||
|
||||
Options:
|
||||
--count COUNT
|
||||
|
||||
Global options:
|
||||
--something SOMETHING
|
||||
--help, -h display this help and exit
|
||||
--version display version and exit
|
||||
```
|
||||
|
||||
### API Documentation
|
||||
|
||||
https://pkg.go.dev/github.com/alexflint/go-arg
|
||||
|
||||
### Rationale
|
||||
|
||||
There are many command line argument parsing libraries for Go, including one in the standard library, so why build another?
|
||||
|
||||
The shortcomings of the `flag` library that ships in the standard library are well known. Positional arguments must preceed options, so `./prog x --foo=1` does what you expect but `./prog --foo=1 x` does not. Arguments cannot have both long (`--foo`) and short (`-f`) forms.
|
||||
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 geared for writing sophisticated command line interfaces. The excellent `codegangsta/cli` is perfect for working with multiple sub-commands and nested flags, but is probably overkill for a simple script with a handful of flags.
|
||||
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 main idea behind `go-arg` is that Go already has an excellent way to describe data structures using Go structs, so there is no need to develop more levels of abstraction on top of this. Instead of one API to specify which arguments your program accepts, and then another API to get the values of those arguments, why not replace both with a single struct?
|
||||
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.
|
||||
|
|
15
doc.go
15
doc.go
|
@ -19,18 +19,21 @@
|
|||
// 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` package name:
|
||||
// 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"`
|
||||
// Debug bool `arg:"-d" help:"turn on debug mode"`
|
||||
// RealMode bool `arg:"--real"
|
||||
// Wr io.Writer `arg:"-"`
|
||||
// }
|
||||
//
|
||||
// The valid tag strings are `positional`, `required`, and `help`. Further, 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). Fields can be excluded from processing with `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
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// This example demonstrates basic usage
|
||||
func Example_Basic() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = []string{"./example", "--foo=hello", "--bar"}
|
||||
|
||||
var args struct {
|
||||
Foo string
|
||||
Bar bool
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println(args.Foo, args.Bar)
|
||||
}
|
||||
|
||||
// This example demonstrates arguments that have default values
|
||||
func Example_DefaultValues() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = []string{"--help"}
|
||||
|
||||
var args struct {
|
||||
Foo string
|
||||
Bar bool
|
||||
}
|
||||
args.Foo = "default value"
|
||||
MustParse(&args)
|
||||
fmt.Println(args.Foo, args.Bar)
|
||||
}
|
||||
|
||||
// This example demonstrates arguments that are required
|
||||
func Example_RequiredArguments() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = []string{"--foo=1", "--bar"}
|
||||
|
||||
var args struct {
|
||||
Foo string `arg:"required"`
|
||||
Bar bool
|
||||
}
|
||||
MustParse(&args)
|
||||
}
|
||||
|
||||
// This example demonstrates positional arguments
|
||||
func Example_PositionalArguments() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = []string{"./example", "in", "out1", "out2", "out3"}
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional"`
|
||||
Output []string `arg:"positional"`
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println("Input:", args.Input)
|
||||
fmt.Println("Output:", args.Output)
|
||||
}
|
||||
|
||||
// This example demonstrates arguments that have multiple values
|
||||
func Example_MultipleValues() {
|
||||
// The args you would pass in on the command line
|
||||
os.Args = []string{"--help"}
|
||||
|
||||
var args struct {
|
||||
Database string
|
||||
IDs []int64
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Printf("Fetching the following IDs from %s: %q", args.Database, args.IDs)
|
||||
}
|
||||
|
||||
// This example shows the usage string generated by go-arg
|
||||
func Example_UsageString() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = []string{"--help"}
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional"`
|
||||
Output []string `arg:"positional"`
|
||||
Verbose bool `arg:"-v,help:verbosity level"`
|
||||
Dataset string `arg:"help:dataset to use"`
|
||||
Optimize int `arg:"-O,help:optimization level"`
|
||||
}
|
||||
MustParse(&args)
|
||||
}
|
358
parse_test.go
358
parse_test.go
|
@ -1,358 +0,0 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func parse(cmdline string, dest interface{}) error {
|
||||
p, err := NewParser(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.Parse(strings.Split(cmdline, " "))
|
||||
}
|
||||
|
||||
func TestStringSingle(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string
|
||||
}
|
||||
err := parse("--foo bar", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "bar", args.Foo)
|
||||
}
|
||||
|
||||
func TestMixed(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string `arg:"-f"`
|
||||
Bar int
|
||||
Baz uint `arg:"positional"`
|
||||
Ham bool
|
||||
Spam float32
|
||||
}
|
||||
args.Bar = 3
|
||||
err := parse("123 -spam=1.2 -ham -f xyz", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "xyz", args.Foo)
|
||||
assert.Equal(t, 3, args.Bar)
|
||||
assert.Equal(t, uint(123), args.Baz)
|
||||
assert.Equal(t, true, args.Ham)
|
||||
assert.EqualValues(t, 1.2, args.Spam)
|
||||
}
|
||||
|
||||
func TestRequired(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string `arg:"required"`
|
||||
}
|
||||
err := parse("", &args)
|
||||
require.Error(t, err, "--foo is required")
|
||||
}
|
||||
|
||||
func TestShortFlag(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string `arg:"-f"`
|
||||
}
|
||||
|
||||
err := parse("-f xyz", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "xyz", args.Foo)
|
||||
|
||||
err = parse("-foo xyz", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "xyz", args.Foo)
|
||||
|
||||
err = parse("--foo xyz", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "xyz", args.Foo)
|
||||
}
|
||||
|
||||
func TestInvalidShortFlag(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string `arg:"-foo"`
|
||||
}
|
||||
err := parse("", &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestLongFlag(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string `arg:"--abc"`
|
||||
}
|
||||
|
||||
err := parse("-abc xyz", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "xyz", args.Foo)
|
||||
|
||||
err = parse("--abc xyz", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "xyz", args.Foo)
|
||||
}
|
||||
|
||||
func TestCaseSensitive(t *testing.T) {
|
||||
var args struct {
|
||||
Lower bool `arg:"-v"`
|
||||
Upper bool `arg:"-V"`
|
||||
}
|
||||
|
||||
err := parse("-v", &args)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, args.Lower)
|
||||
assert.False(t, args.Upper)
|
||||
}
|
||||
|
||||
func TestCaseSensitive2(t *testing.T) {
|
||||
var args struct {
|
||||
Lower bool `arg:"-v"`
|
||||
Upper bool `arg:"-V"`
|
||||
}
|
||||
|
||||
err := parse("-V", &args)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, args.Lower)
|
||||
assert.True(t, args.Upper)
|
||||
}
|
||||
|
||||
func TestPositional(t *testing.T) {
|
||||
var args struct {
|
||||
Input string `arg:"positional"`
|
||||
Output string `arg:"positional"`
|
||||
}
|
||||
err := parse("foo", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "foo", args.Input)
|
||||
assert.Equal(t, "", args.Output)
|
||||
}
|
||||
|
||||
func TestPositionalPointer(t *testing.T) {
|
||||
var args struct {
|
||||
Input string `arg:"positional"`
|
||||
Output []*string `arg:"positional"`
|
||||
}
|
||||
err := parse("foo bar baz", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "foo", args.Input)
|
||||
bar := "bar"
|
||||
baz := "baz"
|
||||
assert.Equal(t, []*string{&bar, &baz}, args.Output)
|
||||
}
|
||||
|
||||
func TestRequiredPositional(t *testing.T) {
|
||||
var args struct {
|
||||
Input string `arg:"positional"`
|
||||
Output string `arg:"positional,required"`
|
||||
}
|
||||
err := parse("foo", &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestTooManyPositional(t *testing.T) {
|
||||
var args struct {
|
||||
Input string `arg:"positional"`
|
||||
Output string `arg:"positional"`
|
||||
}
|
||||
err := parse("foo bar baz", &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMultiple(t *testing.T) {
|
||||
var args struct {
|
||||
Foo []int
|
||||
Bar []string
|
||||
}
|
||||
err := parse("--foo 1 2 3 --bar x y z", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 2, 3}, args.Foo)
|
||||
assert.Equal(t, []string{"x", "y", "z"}, args.Bar)
|
||||
}
|
||||
|
||||
func TestMultipleWithEq(t *testing.T) {
|
||||
var args struct {
|
||||
Foo []int
|
||||
Bar []string
|
||||
}
|
||||
err := parse("--foo 1 2 3 --bar=x", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 2, 3}, args.Foo)
|
||||
assert.Equal(t, []string{"x"}, args.Bar)
|
||||
}
|
||||
|
||||
func TestExemptField(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string
|
||||
Bar interface{} `arg:"-"`
|
||||
}
|
||||
err := parse("--foo xyz", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "xyz", args.Foo)
|
||||
}
|
||||
|
||||
func TestUnknownField(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string
|
||||
}
|
||||
err := parse("--bar xyz", &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMissingRequired(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string `arg:"required"`
|
||||
X []string `arg:"positional"`
|
||||
}
|
||||
err := parse("x", &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMissingValue(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string
|
||||
}
|
||||
err := parse("--foo", &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestInvalidInt(t *testing.T) {
|
||||
var args struct {
|
||||
Foo int
|
||||
}
|
||||
err := parse("--foo=xyz", &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestInvalidUint(t *testing.T) {
|
||||
var args struct {
|
||||
Foo uint
|
||||
}
|
||||
err := parse("--foo=xyz", &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestInvalidFloat(t *testing.T) {
|
||||
var args struct {
|
||||
Foo float64
|
||||
}
|
||||
err := parse("--foo xyz", &args)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestInvalidBool(t *testing.T) {
|
||||
var args struct {
|
||||
Foo bool
|
||||
}
|
||||
err := parse("--foo=xyz", &args)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestInvalidIntSlice(t *testing.T) {
|
||||
var args struct {
|
||||
Foo []int
|
||||
}
|
||||
err := parse("--foo 1 2 xyz", &args)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestInvalidPositional(t *testing.T) {
|
||||
var args struct {
|
||||
Foo int `arg:"positional"`
|
||||
}
|
||||
err := parse("xyz", &args)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestInvalidPositionalSlice(t *testing.T) {
|
||||
var args struct {
|
||||
Foo []int `arg:"positional"`
|
||||
}
|
||||
err := parse("1 2 xyz", &args)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNoMoreOptions(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string
|
||||
Bar []string `arg:"positional"`
|
||||
}
|
||||
err := parse("abc -- --foo xyz", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", args.Foo)
|
||||
assert.Equal(t, []string{"abc", "--foo", "xyz"}, args.Bar)
|
||||
}
|
||||
|
||||
func TestHelpFlag(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string
|
||||
Bar interface{} `arg:"-"`
|
||||
}
|
||||
err := parse("--help", &args)
|
||||
assert.Equal(t, ErrHelp, err)
|
||||
}
|
||||
|
||||
func TestPanicOnNonPointer(t *testing.T) {
|
||||
var args struct{}
|
||||
assert.Panics(t, func() {
|
||||
parse("", args)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPanicOnNonStruct(t *testing.T) {
|
||||
var args string
|
||||
assert.Panics(t, func() {
|
||||
parse("", &args)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnsupportedType(t *testing.T) {
|
||||
var args struct {
|
||||
Foo interface{}
|
||||
}
|
||||
err := parse("--foo", &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUnsupportedSliceElement(t *testing.T) {
|
||||
var args struct {
|
||||
Foo []interface{}
|
||||
}
|
||||
err := parse("--foo", &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUnknownTag(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string `arg:"this_is_not_valid"`
|
||||
}
|
||||
err := parse("--foo xyz", &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string
|
||||
}
|
||||
os.Args = []string{"example", "--foo", "bar"}
|
||||
err := Parse(&args)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "bar", args.Foo)
|
||||
}
|
||||
|
||||
func TestParseError(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string `arg:"this_is_not_valid"`
|
||||
}
|
||||
os.Args = []string{"example", "--bar"}
|
||||
err := Parse(&args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMustParse(t *testing.T) {
|
||||
var args struct {
|
||||
Foo string
|
||||
}
|
||||
os.Args = []string{"example", "--foo", "bar"}
|
||||
MustParse(&args)
|
||||
assert.Equal(t, "bar", args.Foo)
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"go.wit.com/dev/alexflint/scalar"
|
||||
)
|
||||
|
||||
var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()
|
||||
|
||||
// cardinality tracks how many tokens are expected for a given spec
|
||||
// - zero is a boolean, which does to expect any value
|
||||
// - one is an ordinary option that will be parsed from a single token
|
||||
// - multiple is a slice or map that can accept zero or more tokens
|
||||
type cardinality int
|
||||
|
||||
const (
|
||||
zero cardinality = iota
|
||||
one
|
||||
multiple
|
||||
unsupported
|
||||
)
|
||||
|
||||
func (k cardinality) String() string {
|
||||
switch k {
|
||||
case zero:
|
||||
return "zero"
|
||||
case one:
|
||||
return "one"
|
||||
case multiple:
|
||||
return "multiple"
|
||||
case unsupported:
|
||||
return "unsupported"
|
||||
default:
|
||||
return fmt.Sprintf("unknown(%d)", int(k))
|
||||
}
|
||||
}
|
||||
|
||||
// cardinalityOf returns true if the type can be parsed from a string
|
||||
func cardinalityOf(t reflect.Type) (cardinality, error) {
|
||||
if scalar.CanParse(t) {
|
||||
if isBoolean(t) {
|
||||
return zero, nil
|
||||
}
|
||||
return one, nil
|
||||
}
|
||||
|
||||
// look inside pointer types
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
// look inside slice and map types
|
||||
switch t.Kind() {
|
||||
case reflect.Slice:
|
||||
if !scalar.CanParse(t.Elem()) {
|
||||
return unsupported, fmt.Errorf("cannot parse into %v because %v not supported", t, t.Elem())
|
||||
}
|
||||
return multiple, nil
|
||||
case reflect.Map:
|
||||
if !scalar.CanParse(t.Key()) {
|
||||
return unsupported, fmt.Errorf("cannot parse into %v because key type %v not supported", t, t.Elem())
|
||||
}
|
||||
if !scalar.CanParse(t.Elem()) {
|
||||
return unsupported, fmt.Errorf("cannot parse into %v because value type %v not supported", t, t.Elem())
|
||||
}
|
||||
return multiple, nil
|
||||
default:
|
||||
return unsupported, fmt.Errorf("cannot parse into %v", t)
|
||||
}
|
||||
}
|
||||
|
||||
// isBoolean returns true if the type is a boolean or a pointer to a boolean
|
||||
func isBoolean(t reflect.Type) bool {
|
||||
switch {
|
||||
case isTextUnmarshaler(t):
|
||||
return false
|
||||
case t.Kind() == reflect.Bool:
|
||||
return true
|
||||
case t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Bool:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// isTextUnmarshaler returns true if the type or its pointer implements encoding.TextUnmarshaler
|
||||
func isTextUnmarshaler(t reflect.Type) bool {
|
||||
return t.Implements(textUnmarshalerType) || reflect.PtrTo(t).Implements(textUnmarshalerType)
|
||||
}
|
||||
|
||||
// isExported returns true if the struct field name is exported
|
||||
func isExported(field string) bool {
|
||||
r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8
|
||||
return unicode.IsLetter(r) && unicode.IsUpper(r)
|
||||
}
|
||||
|
||||
// isZero returns true if v contains the zero value for its type
|
||||
func isZero(v reflect.Value) bool {
|
||||
t := v.Type()
|
||||
if t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice || t.Kind() == reflect.Map || t.Kind() == reflect.Chan || t.Kind() == reflect.Interface {
|
||||
return v.IsNil()
|
||||
}
|
||||
if !t.Comparable() {
|
||||
return false
|
||||
}
|
||||
return v.Interface() == reflect.Zero(t).Interface()
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"go.wit.com/dev/alexflint/scalar"
|
||||
)
|
||||
|
||||
// setSliceOrMap parses a sequence of strings into a slice or map. If clear is
|
||||
// true then any values already in the slice or map are first removed.
|
||||
func setSliceOrMap(dest reflect.Value, values []string, clear bool) error {
|
||||
if !dest.CanSet() {
|
||||
return fmt.Errorf("field is not writable")
|
||||
}
|
||||
|
||||
t := dest.Type()
|
||||
if t.Kind() == reflect.Ptr {
|
||||
dest = dest.Elem()
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
switch t.Kind() {
|
||||
case reflect.Slice:
|
||||
return setSlice(dest, values, clear)
|
||||
case reflect.Map:
|
||||
return setMap(dest, values, clear)
|
||||
default:
|
||||
return fmt.Errorf("setSliceOrMap cannot insert values into a %v", t)
|
||||
}
|
||||
}
|
||||
|
||||
// setSlice parses a sequence of strings and inserts them into a slice. If clear
|
||||
// is true then any values already in the slice are removed.
|
||||
func setSlice(dest reflect.Value, values []string, clear bool) error {
|
||||
var ptr bool
|
||||
elem := dest.Type().Elem()
|
||||
if elem.Kind() == reflect.Ptr && !elem.Implements(textUnmarshalerType) {
|
||||
ptr = true
|
||||
elem = elem.Elem()
|
||||
}
|
||||
|
||||
// clear the slice in case default values exist
|
||||
if clear && !dest.IsNil() {
|
||||
dest.SetLen(0)
|
||||
}
|
||||
|
||||
// parse the values one-by-one
|
||||
for _, s := range values {
|
||||
v := reflect.New(elem)
|
||||
if err := scalar.ParseValue(v.Elem(), s); err != nil {
|
||||
return err
|
||||
}
|
||||
if !ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
dest.Set(reflect.Append(dest, v))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setMap parses a sequence of name=value strings and inserts them into a map.
|
||||
// If clear is true then any values already in the map are removed.
|
||||
func setMap(dest reflect.Value, values []string, clear bool) error {
|
||||
// determine the key and value type
|
||||
var keyIsPtr bool
|
||||
keyType := dest.Type().Key()
|
||||
if keyType.Kind() == reflect.Ptr && !keyType.Implements(textUnmarshalerType) {
|
||||
keyIsPtr = true
|
||||
keyType = keyType.Elem()
|
||||
}
|
||||
|
||||
var valIsPtr bool
|
||||
valType := dest.Type().Elem()
|
||||
if valType.Kind() == reflect.Ptr && !valType.Implements(textUnmarshalerType) {
|
||||
valIsPtr = true
|
||||
valType = valType.Elem()
|
||||
}
|
||||
|
||||
// clear the slice in case default values exist
|
||||
if clear && !dest.IsNil() {
|
||||
for _, k := range dest.MapKeys() {
|
||||
dest.SetMapIndex(k, reflect.Value{})
|
||||
}
|
||||
}
|
||||
|
||||
// allocate the map if it is not allocated
|
||||
if dest.IsNil() {
|
||||
dest.Set(reflect.MakeMap(dest.Type()))
|
||||
}
|
||||
|
||||
// parse the values one-by-one
|
||||
for _, s := range values {
|
||||
// split at the first equals sign
|
||||
pos := strings.Index(s, "=")
|
||||
if pos == -1 {
|
||||
return fmt.Errorf("cannot parse %q into a map, expected format key=value", s)
|
||||
}
|
||||
|
||||
// parse the key
|
||||
k := reflect.New(keyType)
|
||||
if err := scalar.ParseValue(k.Elem(), s[:pos]); err != nil {
|
||||
return err
|
||||
}
|
||||
if !keyIsPtr {
|
||||
k = k.Elem()
|
||||
}
|
||||
|
||||
// parse the value
|
||||
v := reflect.New(valType)
|
||||
if err := scalar.ParseValue(v.Elem(), s[pos+1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if !valIsPtr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
// add it to the map
|
||||
dest.SetMapIndex(k, v)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package arg
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Subcommand returns the user struct for the subcommand selected by
|
||||
// the command line arguments most recently processed by the parser.
|
||||
// The return value is always a pointer to a struct. If no subcommand
|
||||
// was specified then it returns the top-level arguments struct. If
|
||||
// no command line arguments have been processed by this parser then it
|
||||
// returns nil.
|
||||
func (p *Parser) Subcommand() interface{} {
|
||||
if len(p.subcommand) == 0 {
|
||||
return nil
|
||||
}
|
||||
cmd, err := p.lookupCommand(p.subcommand...)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return p.val(cmd.dest).Interface()
|
||||
}
|
||||
|
||||
// SubcommandNames returns the sequence of subcommands specified by the
|
||||
// user. If no subcommands were given then it returns an empty slice.
|
||||
func (p *Parser) SubcommandNames() []string {
|
||||
return p.subcommand
|
||||
}
|
||||
|
||||
// lookupCommand finds a subcommand based on a sequence of subcommand names. The
|
||||
// first string should be a top-level subcommand, the next should be a child
|
||||
// subcommand of that subcommand, and so on. If no strings are given then the
|
||||
// root command is returned. If no such subcommand exists then an error is
|
||||
// returned.
|
||||
func (p *Parser) lookupCommand(path ...string) (*command, error) {
|
||||
cmd := p.cmd
|
||||
for _, name := range path {
|
||||
found := findSubcommand(cmd.subcommands, name)
|
||||
if found == nil {
|
||||
return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
|
||||
}
|
||||
cmd = found
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
all:
|
||||
@echo
|
||||
@echo
|
||||
|
||||
test:
|
||||
|
||||
redomod:
|
||||
rm -f go.*
|
||||
GO111MODULE= go mod init
|
||||
GO111MODULE= go mod tidy
|
|
@ -0,0 +1,546 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func split(s string) []string {
|
||||
return strings.Split(s, " ")
|
||||
}
|
||||
|
||||
// This example demonstrates basic usage
|
||||
func Example() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --foo=hello --bar")
|
||||
|
||||
var args struct {
|
||||
Foo string
|
||||
Bar bool
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println(args.Foo, args.Bar)
|
||||
// output: hello true
|
||||
}
|
||||
|
||||
// This example demonstrates arguments that have default values
|
||||
func Example_defaultValues() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example")
|
||||
|
||||
var args struct {
|
||||
Foo string `default:"abc"`
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println(args.Foo)
|
||||
// output: abc
|
||||
}
|
||||
|
||||
// This example demonstrates arguments that are required
|
||||
func Example_requiredArguments() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --foo=abc --bar")
|
||||
|
||||
var args struct {
|
||||
Foo string `arg:"required"`
|
||||
Bar bool
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println(args.Foo, args.Bar)
|
||||
// output: abc true
|
||||
}
|
||||
|
||||
// This example demonstrates positional arguments
|
||||
func Example_positionalArguments() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example in out1 out2 out3")
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional"`
|
||||
Output []string `arg:"positional"`
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println("In:", args.Input)
|
||||
fmt.Println("Out:", args.Output)
|
||||
// output:
|
||||
// In: in
|
||||
// Out: [out1 out2 out3]
|
||||
}
|
||||
|
||||
// This example demonstrates arguments that have multiple values
|
||||
func Example_multipleValues() {
|
||||
// The args you would pass in on the command line
|
||||
os.Args = split("./example --database localhost --ids 1 2 3")
|
||||
|
||||
var args struct {
|
||||
Database string
|
||||
IDs []int64
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Printf("Fetching the following IDs from %s: %v", args.Database, args.IDs)
|
||||
// output: Fetching the following IDs from localhost: [1 2 3]
|
||||
}
|
||||
|
||||
// This example demonstrates arguments with keys and values
|
||||
func Example_mappings() {
|
||||
// The args you would pass in on the command line
|
||||
os.Args = split("./example --userids john=123 mary=456")
|
||||
|
||||
var args struct {
|
||||
UserIDs map[string]int
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println(args.UserIDs)
|
||||
// output: map[john:123 mary:456]
|
||||
}
|
||||
|
||||
type commaSeparated struct {
|
||||
M map[string]string
|
||||
}
|
||||
|
||||
func (c *commaSeparated) UnmarshalText(b []byte) error {
|
||||
c.M = make(map[string]string)
|
||||
for _, part := range strings.Split(string(b), ",") {
|
||||
pos := strings.Index(part, "=")
|
||||
if pos == -1 {
|
||||
return fmt.Errorf("error parsing %q, expected format key=value", part)
|
||||
}
|
||||
c.M[part[:pos]] = part[pos+1:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// This example demonstrates arguments with keys and values separated by commas
|
||||
func Example_mappingWithCommas() {
|
||||
// The args you would pass in on the command line
|
||||
os.Args = split("./example --values one=two,three=four")
|
||||
|
||||
var args struct {
|
||||
Values commaSeparated
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println(args.Values.M)
|
||||
// output: map[one:two three:four]
|
||||
}
|
||||
|
||||
// This eample demonstrates multiple value arguments that can be mixed with
|
||||
// other arguments.
|
||||
func Example_multipleMixed() {
|
||||
os.Args = split("./example -c cmd1 db1 -f file1 db2 -c cmd2 -f file2 -f file3 db3 -c cmd3")
|
||||
var args struct {
|
||||
Commands []string `arg:"-c,separate"`
|
||||
Files []string `arg:"-f,separate"`
|
||||
Databases []string `arg:"positional"`
|
||||
}
|
||||
MustParse(&args)
|
||||
fmt.Println("Commands:", args.Commands)
|
||||
fmt.Println("Files:", args.Files)
|
||||
fmt.Println("Databases:", args.Databases)
|
||||
|
||||
// output:
|
||||
// Commands: [cmd1 cmd2 cmd3]
|
||||
// Files: [file1 file2 file3]
|
||||
// Databases: [db1 db2 db3]
|
||||
}
|
||||
|
||||
// This example shows the usage string generated by go-arg
|
||||
func Example_helpText() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --help")
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional,required"`
|
||||
Output []string `arg:"positional"`
|
||||
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||
Dataset string `help:"dataset to use"`
|
||||
Optimize int `arg:"-O,--optim" help:"optimization level"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
// Usage: example [--verbose] [--dataset DATASET] [--optim OPTIM] INPUT [OUTPUT [OUTPUT ...]]
|
||||
//
|
||||
// Positional arguments:
|
||||
// INPUT
|
||||
// OUTPUT
|
||||
//
|
||||
// Options:
|
||||
// --verbose, -v verbosity level
|
||||
// --dataset DATASET dataset to use
|
||||
// --optim OPTIM, -O OPTIM
|
||||
// optimization level
|
||||
// --help, -h display this help and exit
|
||||
}
|
||||
|
||||
// This example shows the usage string generated by go-arg with customized placeholders
|
||||
func Example_helpPlaceholder() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --help")
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional,required" placeholder:"SRC"`
|
||||
Output []string `arg:"positional" placeholder:"DST"`
|
||||
Optimize int `arg:"-O" help:"optimization level" placeholder:"LEVEL"`
|
||||
MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" placeholder:"N"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
// Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
|
||||
//
|
||||
// Positional arguments:
|
||||
// SRC
|
||||
// DST
|
||||
//
|
||||
// Options:
|
||||
// --optimize LEVEL, -O LEVEL
|
||||
// optimization level
|
||||
// --maxjobs N, -j N maximum number of simultaneous jobs
|
||||
// --help, -h display this help and exit
|
||||
}
|
||||
|
||||
// This example shows the usage string generated by go-arg when using subcommands
|
||||
func Example_helpTextWithSubcommand() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --help")
|
||||
|
||||
type getCmd struct {
|
||||
Item string `arg:"positional" help:"item to fetch"`
|
||||
}
|
||||
|
||||
type listCmd struct {
|
||||
Format string `help:"output format"`
|
||||
Limit int
|
||||
}
|
||||
|
||||
var args struct {
|
||||
Verbose bool
|
||||
Get *getCmd `arg:"subcommand" help:"fetch an item and print it"`
|
||||
List *listCmd `arg:"subcommand" help:"list available items"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
// Usage: example [--verbose] <command> [<args>]
|
||||
//
|
||||
// Options:
|
||||
// --verbose
|
||||
// --help, -h display this help and exit
|
||||
//
|
||||
// Commands:
|
||||
// get fetch an item and print it
|
||||
// list list available items
|
||||
}
|
||||
|
||||
// This example shows the usage string generated by go-arg when using subcommands
|
||||
func Example_helpTextWhenUsingSubcommand() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example get --help")
|
||||
|
||||
type getCmd struct {
|
||||
Item string `arg:"positional,required" help:"item to fetch"`
|
||||
}
|
||||
|
||||
type listCmd struct {
|
||||
Format string `help:"output format"`
|
||||
Limit int
|
||||
}
|
||||
|
||||
var args struct {
|
||||
Verbose bool
|
||||
Get *getCmd `arg:"subcommand" help:"fetch an item and print it"`
|
||||
List *listCmd `arg:"subcommand" help:"list available items"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
// Usage: example get ITEM
|
||||
//
|
||||
// Positional arguments:
|
||||
// ITEM item to fetch
|
||||
//
|
||||
// Global options:
|
||||
// --verbose
|
||||
// --help, -h display this help and exit
|
||||
}
|
||||
|
||||
// This example shows how to print help for an explicit subcommand
|
||||
func Example_writeHelpForSubcommand() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example get --help")
|
||||
|
||||
type getCmd struct {
|
||||
Item string `arg:"positional" help:"item to fetch"`
|
||||
}
|
||||
|
||||
type listCmd struct {
|
||||
Format string `help:"output format"`
|
||||
Limit int
|
||||
}
|
||||
|
||||
var args struct {
|
||||
Verbose bool
|
||||
Get *getCmd `arg:"subcommand" help:"fetch an item and print it"`
|
||||
List *listCmd `arg:"subcommand" help:"list available items"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
exit := func(int) {}
|
||||
|
||||
p, err := NewParser(Config{Exit: exit}, &args)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = p.WriteHelpForSubcommand(os.Stdout, "list")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// output:
|
||||
// Usage: example list [--format FORMAT] [--limit LIMIT]
|
||||
//
|
||||
// Options:
|
||||
// --format FORMAT output format
|
||||
// --limit LIMIT
|
||||
//
|
||||
// Global options:
|
||||
// --verbose
|
||||
// --help, -h display this help and exit
|
||||
}
|
||||
|
||||
// This example shows how to print help for a subcommand that is nested several levels deep
|
||||
func Example_writeHelpForSubcommandNested() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example get --help")
|
||||
|
||||
type mostNestedCmd struct {
|
||||
Item string
|
||||
}
|
||||
|
||||
type nestedCmd struct {
|
||||
MostNested *mostNestedCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
type topLevelCmd struct {
|
||||
Nested *nestedCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
var args struct {
|
||||
TopLevel *topLevelCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
exit := func(int) {}
|
||||
|
||||
p, err := NewParser(Config{Exit: exit}, &args)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = p.WriteHelpForSubcommand(os.Stdout, "toplevel", "nested", "mostnested")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// output:
|
||||
// Usage: example toplevel nested mostnested [--item ITEM]
|
||||
//
|
||||
// Options:
|
||||
// --item ITEM
|
||||
// --help, -h display this help and exit
|
||||
}
|
||||
|
||||
// This example shows the error string generated by go-arg when an invalid option is provided
|
||||
func Example_errorText() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example --optimize INVALID")
|
||||
|
||||
var args struct {
|
||||
Input string `arg:"positional,required"`
|
||||
Output []string `arg:"positional"`
|
||||
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||
Dataset string `help:"dataset to use"`
|
||||
Optimize int `arg:"-O,help:optimization level"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
// Usage: example [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]]
|
||||
// error: error processing --optimize: strconv.ParseInt: parsing "INVALID": invalid syntax
|
||||
}
|
||||
|
||||
// This example shows the error string generated by go-arg when an invalid option is provided
|
||||
func Example_errorTextForSubcommand() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example get --count INVALID")
|
||||
|
||||
type getCmd struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
var args struct {
|
||||
Get *getCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
// Usage: example get [--count COUNT]
|
||||
// error: error processing --count: strconv.ParseInt: parsing "INVALID": invalid syntax
|
||||
}
|
||||
|
||||
// This example demonstrates use of subcommands
|
||||
func Example_subcommand() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = split("./example commit -a -m what-this-commit-is-about")
|
||||
|
||||
type CheckoutCmd struct {
|
||||
Branch string `arg:"positional"`
|
||||
Track bool `arg:"-t"`
|
||||
}
|
||||
type CommitCmd struct {
|
||||
All bool `arg:"-a"`
|
||||
Message string `arg:"-m"`
|
||||
}
|
||||
type PushCmd struct {
|
||||
Remote string `arg:"positional"`
|
||||
Branch string `arg:"positional"`
|
||||
SetUpstream bool `arg:"-u"`
|
||||
}
|
||||
var args struct {
|
||||
Checkout *CheckoutCmd `arg:"subcommand:checkout"`
|
||||
Commit *CommitCmd `arg:"subcommand:commit"`
|
||||
Push *PushCmd `arg:"subcommand:push"`
|
||||
Quiet bool `arg:"-q"` // this flag is global to all subcommands
|
||||
}
|
||||
|
||||
// This is only necessary when running inside golang's runnable example harness
|
||||
mustParseExit = func(int) {}
|
||||
mustParseOut = os.Stdout
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
switch {
|
||||
case args.Checkout != nil:
|
||||
fmt.Printf("checkout requested for branch %s\n", args.Checkout.Branch)
|
||||
case args.Commit != nil:
|
||||
fmt.Printf("commit requested with message \"%s\"\n", args.Commit.Message)
|
||||
case args.Push != nil:
|
||||
fmt.Printf("push requested from %s to %s\n", args.Push.Branch, args.Push.Remote)
|
||||
}
|
||||
|
||||
// output:
|
||||
// commit requested with message "what-this-commit-is-about"
|
||||
}
|
||||
|
||||
func Example_allSupportedTypes() {
|
||||
// These are the args you would pass in on the command line
|
||||
os.Args = []string{}
|
||||
|
||||
var args struct {
|
||||
Bool bool
|
||||
Byte byte
|
||||
Rune rune
|
||||
Int int
|
||||
Int8 int8
|
||||
Int16 int16
|
||||
Int32 int32
|
||||
Int64 int64
|
||||
Float32 float32
|
||||
Float64 float64
|
||||
String string
|
||||
Duration time.Duration
|
||||
URL url.URL
|
||||
Email mail.Address
|
||||
MAC net.HardwareAddr
|
||||
}
|
||||
|
||||
// go-arg supports each of the types above, as well as pointers to any of
|
||||
// the above and slices of any of the above. It also supports any types that
|
||||
// implements encoding.TextUnmarshaler.
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
// output:
|
||||
}
|
||||
|
||||
func Example_envVarOnly() {
|
||||
os.Args = split("./example")
|
||||
_ = os.Setenv("AUTH_KEY", "my_key")
|
||||
|
||||
defer os.Unsetenv("AUTH_KEY")
|
||||
|
||||
var args struct {
|
||||
AuthKey string `arg:"--,env:AUTH_KEY"`
|
||||
}
|
||||
|
||||
MustParse(&args)
|
||||
|
||||
fmt.Println(args.AuthKey)
|
||||
// output: my_key
|
||||
}
|
||||
|
||||
func Example_envVarOnlyShouldIgnoreFlag() {
|
||||
os.Args = split("./example --=my_key")
|
||||
|
||||
var args struct {
|
||||
AuthKey string `arg:"--,env:AUTH_KEY"`
|
||||
}
|
||||
|
||||
err := Parse(&args)
|
||||
|
||||
fmt.Println(err)
|
||||
// output: unknown argument --=my_key
|
||||
}
|
||||
|
||||
func Example_envVarOnlyShouldIgnoreShortFlag() {
|
||||
os.Args = split("./example -=my_key")
|
||||
|
||||
var args struct {
|
||||
AuthKey string `arg:"--,env:AUTH_KEY"`
|
||||
}
|
||||
|
||||
err := Parse(&args)
|
||||
|
||||
fmt.Println(err)
|
||||
// output: unknown argument -=my_key
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,112 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func assertCardinality(t *testing.T, typ reflect.Type, expected cardinality) {
|
||||
actual, err := cardinalityOf(typ)
|
||||
assert.Equal(t, expected, actual, "expected %v to have cardinality %v but got %v", typ, expected, actual)
|
||||
if expected == unsupported {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCardinalityOf(t *testing.T) {
|
||||
var b bool
|
||||
var i int
|
||||
var s string
|
||||
var f float64
|
||||
var bs []bool
|
||||
var is []int
|
||||
var m map[string]int
|
||||
var unsupported1 struct{}
|
||||
var unsupported2 []struct{}
|
||||
var unsupported3 map[string]struct{}
|
||||
var unsupported4 map[struct{}]string
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(b), zero)
|
||||
assertCardinality(t, reflect.TypeOf(i), one)
|
||||
assertCardinality(t, reflect.TypeOf(s), one)
|
||||
assertCardinality(t, reflect.TypeOf(f), one)
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(&b), zero)
|
||||
assertCardinality(t, reflect.TypeOf(&s), one)
|
||||
assertCardinality(t, reflect.TypeOf(&i), one)
|
||||
assertCardinality(t, reflect.TypeOf(&f), one)
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(bs), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(is), multiple)
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(&bs), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(&is), multiple)
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(m), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(&m), multiple)
|
||||
|
||||
assertCardinality(t, reflect.TypeOf(unsupported1), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(&unsupported1), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(unsupported2), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(&unsupported2), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(unsupported3), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(&unsupported3), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(unsupported4), unsupported)
|
||||
assertCardinality(t, reflect.TypeOf(&unsupported4), unsupported)
|
||||
}
|
||||
|
||||
type implementsTextUnmarshaler struct{}
|
||||
|
||||
func (*implementsTextUnmarshaler) UnmarshalText(text []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCardinalityTextUnmarshaler(t *testing.T) {
|
||||
var x implementsTextUnmarshaler
|
||||
var s []implementsTextUnmarshaler
|
||||
var m []implementsTextUnmarshaler
|
||||
assertCardinality(t, reflect.TypeOf(x), one)
|
||||
assertCardinality(t, reflect.TypeOf(&x), one)
|
||||
assertCardinality(t, reflect.TypeOf(s), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(&s), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(m), multiple)
|
||||
assertCardinality(t, reflect.TypeOf(&m), multiple)
|
||||
}
|
||||
|
||||
func TestIsExported(t *testing.T) {
|
||||
assert.True(t, isExported("Exported"))
|
||||
assert.False(t, isExported("notExported"))
|
||||
assert.False(t, isExported(""))
|
||||
assert.False(t, isExported(string([]byte{255})))
|
||||
}
|
||||
|
||||
func TestCardinalityString(t *testing.T) {
|
||||
assert.Equal(t, "zero", zero.String())
|
||||
assert.Equal(t, "one", one.String())
|
||||
assert.Equal(t, "multiple", multiple.String())
|
||||
assert.Equal(t, "unsupported", unsupported.String())
|
||||
assert.Equal(t, "unknown(42)", cardinality(42).String())
|
||||
}
|
||||
|
||||
func TestIsZero(t *testing.T) {
|
||||
var zero int
|
||||
var notZero = 3
|
||||
var nilSlice []int
|
||||
var nonNilSlice = []int{1, 2, 3}
|
||||
var nilMap map[string]string
|
||||
var nonNilMap = map[string]string{"foo": "bar"}
|
||||
var uncomparable = func() {}
|
||||
|
||||
assert.True(t, isZero(reflect.ValueOf(zero)))
|
||||
assert.False(t, isZero(reflect.ValueOf(notZero)))
|
||||
|
||||
assert.True(t, isZero(reflect.ValueOf(nilSlice)))
|
||||
assert.False(t, isZero(reflect.ValueOf(nonNilSlice)))
|
||||
|
||||
assert.True(t, isZero(reflect.ValueOf(nilMap)))
|
||||
assert.False(t, isZero(reflect.ValueOf(nonNilMap)))
|
||||
|
||||
assert.False(t, isZero(reflect.ValueOf(uncomparable)))
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSetSliceWithoutClearing(t *testing.T) {
|
||||
xs := []int{10}
|
||||
entries := []string{"1", "2", "3"}
|
||||
err := setSlice(reflect.ValueOf(&xs).Elem(), entries, false)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{10, 1, 2, 3}, xs)
|
||||
}
|
||||
|
||||
func TestSetSliceAfterClearing(t *testing.T) {
|
||||
xs := []int{100}
|
||||
entries := []string{"1", "2", "3"}
|
||||
err := setSlice(reflect.ValueOf(&xs).Elem(), entries, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 2, 3}, xs)
|
||||
}
|
||||
|
||||
func TestSetSliceInvalid(t *testing.T) {
|
||||
xs := []int{100}
|
||||
entries := []string{"invalid"}
|
||||
err := setSlice(reflect.ValueOf(&xs).Elem(), entries, true)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetSlicePtr(t *testing.T) {
|
||||
var xs []*int
|
||||
entries := []string{"1", "2", "3"}
|
||||
err := setSlice(reflect.ValueOf(&xs).Elem(), entries, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, xs, 3)
|
||||
assert.Equal(t, 1, *xs[0])
|
||||
assert.Equal(t, 2, *xs[1])
|
||||
assert.Equal(t, 3, *xs[2])
|
||||
}
|
||||
|
||||
func TestSetSliceTextUnmarshaller(t *testing.T) {
|
||||
// textUnmarshaler is a struct that captures the length of the string passed to it
|
||||
var xs []*textUnmarshaler
|
||||
entries := []string{"a", "aa", "aaa"}
|
||||
err := setSlice(reflect.ValueOf(&xs).Elem(), entries, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, xs, 3)
|
||||
assert.Equal(t, 1, xs[0].val)
|
||||
assert.Equal(t, 2, xs[1].val)
|
||||
assert.Equal(t, 3, xs[2].val)
|
||||
}
|
||||
|
||||
func TestSetMapWithoutClearing(t *testing.T) {
|
||||
m := map[string]int{"foo": 10}
|
||||
entries := []string{"a=1", "b=2"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m, 3)
|
||||
assert.Equal(t, 1, m["a"])
|
||||
assert.Equal(t, 2, m["b"])
|
||||
assert.Equal(t, 10, m["foo"])
|
||||
}
|
||||
|
||||
func TestSetMapAfterClearing(t *testing.T) {
|
||||
m := map[string]int{"foo": 10}
|
||||
entries := []string{"a=1", "b=2"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m, 2)
|
||||
assert.Equal(t, 1, m["a"])
|
||||
assert.Equal(t, 2, m["b"])
|
||||
}
|
||||
|
||||
func TestSetMapWithKeyPointer(t *testing.T) {
|
||||
// textUnmarshaler is a struct that captures the length of the string passed to it
|
||||
var m map[*string]int
|
||||
entries := []string{"abc=123"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m, 1)
|
||||
}
|
||||
|
||||
func TestSetMapWithValuePointer(t *testing.T) {
|
||||
// textUnmarshaler is a struct that captures the length of the string passed to it
|
||||
var m map[string]*int
|
||||
entries := []string{"abc=123"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m, 1)
|
||||
assert.Equal(t, 123, *m["abc"])
|
||||
}
|
||||
|
||||
func TestSetMapTextUnmarshaller(t *testing.T) {
|
||||
// textUnmarshaler is a struct that captures the length of the string passed to it
|
||||
var m map[textUnmarshaler]*textUnmarshaler
|
||||
entries := []string{"a=123", "aa=12", "aaa=1"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m, 3)
|
||||
assert.Equal(t, &textUnmarshaler{3}, m[textUnmarshaler{1}])
|
||||
assert.Equal(t, &textUnmarshaler{2}, m[textUnmarshaler{2}])
|
||||
assert.Equal(t, &textUnmarshaler{1}, m[textUnmarshaler{3}])
|
||||
}
|
||||
|
||||
func TestSetMapInvalidKey(t *testing.T) {
|
||||
var m map[int]int
|
||||
entries := []string{"invalid=123"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetMapInvalidValue(t *testing.T) {
|
||||
var m map[int]int
|
||||
entries := []string{"123=invalid"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetMapMalformed(t *testing.T) {
|
||||
// textUnmarshaler is a struct that captures the length of the string passed to it
|
||||
var m map[string]string
|
||||
entries := []string{"missing_equals_sign"}
|
||||
err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetSliceOrMapErrors(t *testing.T) {
|
||||
var err error
|
||||
var dest reflect.Value
|
||||
|
||||
// converting a slice to a reflect.Value in this way will make it read only
|
||||
var cannotSet []int
|
||||
dest = reflect.ValueOf(cannotSet)
|
||||
err = setSliceOrMap(dest, nil, false)
|
||||
assert.Error(t, err)
|
||||
|
||||
// check what happens when we pass in something that is not a slice or a map
|
||||
var notSliceOrMap string
|
||||
dest = reflect.ValueOf(¬SliceOrMap).Elem()
|
||||
err = setSliceOrMap(dest, nil, false)
|
||||
assert.Error(t, err)
|
||||
|
||||
// check what happens when we pass in a pointer to something that is not a slice or a map
|
||||
var stringPtr *string
|
||||
dest = reflect.ValueOf(&stringPtr).Elem()
|
||||
err = setSliceOrMap(dest, nil, false)
|
||||
assert.Error(t, err)
|
||||
}
|
|
@ -0,0 +1,508 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// This file contains tests for parse.go but I decided to put them here
|
||||
// since that file is getting large
|
||||
|
||||
func TestSubcommandNotAPointer(t *testing.T) {
|
||||
var args struct {
|
||||
A string `arg:"subcommand"`
|
||||
}
|
||||
_, err := NewParser(Config{}, &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSubcommandNotAPointerToStruct(t *testing.T) {
|
||||
var args struct {
|
||||
A struct{} `arg:"subcommand"`
|
||||
}
|
||||
_, err := NewParser(Config{}, &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestPositionalAndSubcommandNotAllowed(t *testing.T) {
|
||||
var args struct {
|
||||
A string `arg:"positional"`
|
||||
B *struct{} `arg:"subcommand"`
|
||||
}
|
||||
_, err := NewParser(Config{}, &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMinimalSubcommand(t *testing.T) {
|
||||
type listCmd struct {
|
||||
}
|
||||
var args struct {
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
p, err := pparse("list", &args)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List, p.Subcommand())
|
||||
assert.Equal(t, []string{"list"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
func TestSubcommandNamesBeforeParsing(t *testing.T) {
|
||||
type listCmd struct{}
|
||||
var args struct {
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
p, err := NewParser(Config{}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, p.Subcommand())
|
||||
assert.Nil(t, p.SubcommandNames())
|
||||
}
|
||||
|
||||
func TestNoSuchSubcommand(t *testing.T) {
|
||||
type listCmd struct {
|
||||
}
|
||||
var args struct {
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
_, err := pparse("invalid", &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNamedSubcommand(t *testing.T) {
|
||||
type listCmd struct {
|
||||
}
|
||||
var args struct {
|
||||
List *listCmd `arg:"subcommand:ls"`
|
||||
}
|
||||
p, err := pparse("ls", &args)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List, p.Subcommand())
|
||||
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
func TestSubcommandAliases(t *testing.T) {
|
||||
type listCmd struct {
|
||||
}
|
||||
var args struct {
|
||||
List *listCmd `arg:"subcommand:list|ls"`
|
||||
}
|
||||
p, err := pparse("ls", &args)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List, p.Subcommand())
|
||||
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
func TestEmptySubcommand(t *testing.T) {
|
||||
type listCmd struct {
|
||||
}
|
||||
var args struct {
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
p, err := pparse("", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.List)
|
||||
assert.Nil(t, p.Subcommand())
|
||||
assert.Empty(t, p.SubcommandNames())
|
||||
}
|
||||
|
||||
func TestTwoSubcommands(t *testing.T) {
|
||||
type getCmd struct {
|
||||
}
|
||||
type listCmd struct {
|
||||
}
|
||||
var args struct {
|
||||
Get *getCmd `arg:"subcommand"`
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
p, err := pparse("list", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List, p.Subcommand())
|
||||
assert.Equal(t, []string{"list"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
func TestTwoSubcommandsWithAliases(t *testing.T) {
|
||||
type getCmd struct {
|
||||
}
|
||||
type listCmd struct {
|
||||
}
|
||||
var args struct {
|
||||
Get *getCmd `arg:"subcommand:get|g"`
|
||||
List *listCmd `arg:"subcommand:list|ls"`
|
||||
}
|
||||
p, err := pparse("ls", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List, p.Subcommand())
|
||||
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
func TestSubcommandsWithOptions(t *testing.T) {
|
||||
type getCmd struct {
|
||||
Name string
|
||||
}
|
||||
type listCmd struct {
|
||||
Limit int
|
||||
}
|
||||
type cmd struct {
|
||||
Verbose bool
|
||||
Get *getCmd `arg:"subcommand"`
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("list", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("list --limit 3", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List.Limit, 3)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("list --limit 3 --verbose", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List.Limit, 3)
|
||||
assert.True(t, args.Verbose)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("list --verbose --limit 3", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List.Limit, 3)
|
||||
assert.True(t, args.Verbose)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("--verbose list --limit 3", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.Get)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, args.List.Limit, 3)
|
||||
assert.True(t, args.Verbose)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("get", &args)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Nil(t, args.List)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("get --name test", &args)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Nil(t, args.List)
|
||||
assert.Equal(t, args.Get.Name, "test")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubcommandsWithEnvVars(t *testing.T) {
|
||||
type getCmd struct {
|
||||
Name string `arg:"env"`
|
||||
}
|
||||
type listCmd struct {
|
||||
Limit int `arg:"env"`
|
||||
}
|
||||
type cmd struct {
|
||||
Verbose bool
|
||||
Get *getCmd `arg:"subcommand"`
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
setenv(t, "LIMIT", "123")
|
||||
err := parse("list", &args)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.List)
|
||||
assert.Equal(t, 123, args.List.Limit)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
setenv(t, "LIMIT", "not_an_integer")
|
||||
err := parse("list", &args)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedSubcommands(t *testing.T) {
|
||||
type child struct{}
|
||||
type parent struct {
|
||||
Child *child `arg:"subcommand"`
|
||||
}
|
||||
type grandparent struct {
|
||||
Parent *parent `arg:"subcommand"`
|
||||
}
|
||||
type root struct {
|
||||
Grandparent *grandparent `arg:"subcommand"`
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := pparse("grandparent parent child", &args)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.Grandparent)
|
||||
require.NotNil(t, args.Grandparent.Parent)
|
||||
require.NotNil(t, args.Grandparent.Parent.Child)
|
||||
assert.Equal(t, args.Grandparent.Parent.Child, p.Subcommand())
|
||||
assert.Equal(t, []string{"grandparent", "parent", "child"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := pparse("grandparent parent", &args)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.Grandparent)
|
||||
require.NotNil(t, args.Grandparent.Parent)
|
||||
require.Nil(t, args.Grandparent.Parent.Child)
|
||||
assert.Equal(t, args.Grandparent.Parent, p.Subcommand())
|
||||
assert.Equal(t, []string{"grandparent", "parent"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := pparse("grandparent", &args)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.Grandparent)
|
||||
require.Nil(t, args.Grandparent.Parent)
|
||||
assert.Equal(t, args.Grandparent, p.Subcommand())
|
||||
assert.Equal(t, []string{"grandparent"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := pparse("", &args)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, args.Grandparent)
|
||||
assert.Nil(t, p.Subcommand())
|
||||
assert.Empty(t, p.SubcommandNames())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedSubcommandsWithAliases(t *testing.T) {
|
||||
type child struct{}
|
||||
type parent struct {
|
||||
Child *child `arg:"subcommand:child|ch"`
|
||||
}
|
||||
type grandparent struct {
|
||||
Parent *parent `arg:"subcommand:parent|pa"`
|
||||
}
|
||||
type root struct {
|
||||
Grandparent *grandparent `arg:"subcommand:grandparent|gp"`
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := pparse("gp parent child", &args)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.Grandparent)
|
||||
require.NotNil(t, args.Grandparent.Parent)
|
||||
require.NotNil(t, args.Grandparent.Parent.Child)
|
||||
assert.Equal(t, args.Grandparent.Parent.Child, p.Subcommand())
|
||||
assert.Equal(t, []string{"gp", "parent", "child"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := pparse("grandparent pa", &args)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.Grandparent)
|
||||
require.NotNil(t, args.Grandparent.Parent)
|
||||
require.Nil(t, args.Grandparent.Parent.Child)
|
||||
assert.Equal(t, args.Grandparent.Parent, p.Subcommand())
|
||||
assert.Equal(t, []string{"grandparent", "pa"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := pparse("grandparent", &args)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, args.Grandparent)
|
||||
require.Nil(t, args.Grandparent.Parent)
|
||||
assert.Equal(t, args.Grandparent, p.Subcommand())
|
||||
assert.Equal(t, []string{"grandparent"}, p.SubcommandNames())
|
||||
}
|
||||
|
||||
{
|
||||
var args root
|
||||
p, err := pparse("", &args)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, args.Grandparent)
|
||||
assert.Nil(t, p.Subcommand())
|
||||
assert.Empty(t, p.SubcommandNames())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubcommandsWithPositionals(t *testing.T) {
|
||||
type listCmd struct {
|
||||
Pattern string `arg:"positional"`
|
||||
}
|
||||
type cmd struct {
|
||||
Format string
|
||||
List *listCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("list", &args)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "", args.List.Pattern)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("list --format json", &args)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "", args.List.Pattern)
|
||||
assert.Equal(t, "json", args.Format)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("list somepattern", &args)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "somepattern", args.List.Pattern)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("list somepattern --format json", &args)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "somepattern", args.List.Pattern)
|
||||
assert.Equal(t, "json", args.Format)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("list --format json somepattern", &args)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "somepattern", args.List.Pattern)
|
||||
assert.Equal(t, "json", args.Format)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("--format json list somepattern", &args)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.List)
|
||||
assert.Equal(t, "somepattern", args.List.Pattern)
|
||||
assert.Equal(t, "json", args.Format)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("--format json", &args)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, args.List)
|
||||
assert.Equal(t, "json", args.Format)
|
||||
}
|
||||
}
|
||||
func TestSubcommandsWithMultiplePositionals(t *testing.T) {
|
||||
type getCmd struct {
|
||||
Items []string `arg:"positional"`
|
||||
}
|
||||
type cmd struct {
|
||||
Limit int
|
||||
Get *getCmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("get", &args)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Empty(t, args.Get.Items)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("get --limit 5", &args)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Empty(t, args.Get.Items)
|
||||
assert.Equal(t, 5, args.Limit)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("get item1", &args)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Equal(t, []string{"item1"}, args.Get.Items)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("get item1 item2 item3", &args)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Equal(t, []string{"item1", "item2", "item3"}, args.Get.Items)
|
||||
}
|
||||
|
||||
{
|
||||
var args cmd
|
||||
err := parse("get item1 --limit 5 item2", &args)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, args.Get)
|
||||
assert.Equal(t, []string{"item1", "item2"}, args.Get.Items)
|
||||
assert.Equal(t, 5, args.Limit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValForNilStruct(t *testing.T) {
|
||||
type subcmd struct{}
|
||||
var cmd struct {
|
||||
Sub *subcmd `arg:"subcommand"`
|
||||
}
|
||||
|
||||
p, err := NewParser(Config{}, &cmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
typ := reflect.TypeOf(cmd)
|
||||
subField, _ := typ.FieldByName("Sub")
|
||||
|
||||
v := p.val(path{fields: []reflect.StructField{subField, subField}})
|
||||
assert.False(t, v.IsValid())
|
||||
}
|
||||
|
||||
func TestSubcommandInvalidInternal(t *testing.T) {
|
||||
// this situation should never arise in practice but still good to test for it
|
||||
var cmd struct{}
|
||||
p, err := NewParser(Config{}, &cmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
p.subcommand = []string{"should", "never", "happen"}
|
||||
sub := p.Subcommand()
|
||||
assert.Nil(t, sub)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
339
usage.go
339
usage.go
|
@ -3,37 +3,82 @@ package arg
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// the width of the left column
|
||||
const colWidth = 25
|
||||
|
||||
// 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.WriteUsage(os.Stderr)
|
||||
fmt.Fprintln(os.Stderr, "error:", msg)
|
||||
os.Exit(-1)
|
||||
p.FailSubcommand(msg)
|
||||
}
|
||||
|
||||
// FailSubcommand prints usage information for a specified subcommand to p.Config.Out,
|
||||
// then exits with status code 2. To write usage information for a top-level
|
||||
// subcommand, provide just the name of that subcommand. To write usage
|
||||
// information for a subcommand that is nested under another subcommand, provide
|
||||
// a sequence of subcommand names starting with the top-level subcommand and so
|
||||
// on down the tree.
|
||||
func (p *Parser) FailSubcommand(msg string, subcommand ...string) error {
|
||||
err := p.WriteUsageForSubcommand(p.config.Out, subcommand...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(p.config.Out, "error:", msg)
|
||||
p.config.Exit(2)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteUsage writes usage information to the given writer
|
||||
func (p *Parser) WriteUsage(w io.Writer) {
|
||||
var positionals, options []*spec
|
||||
for _, spec := range p.spec {
|
||||
if spec.positional {
|
||||
p.WriteUsageForSubcommand(w, p.subcommand...)
|
||||
}
|
||||
|
||||
// WriteUsageForSubcommand writes the usage information for a specified
|
||||
// subcommand. To write usage information for a top-level subcommand, provide
|
||||
// just the name of that subcommand. To write usage information for a subcommand
|
||||
// that is nested under another subcommand, provide a sequence of subcommand
|
||||
// names starting with the top-level subcommand and so on down the tree.
|
||||
func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) error {
|
||||
cmd, err := p.lookupCommand(subcommand...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var positionals, longOptions, shortOptions []*spec
|
||||
for _, spec := range cmd.specs {
|
||||
switch {
|
||||
case spec.positional:
|
||||
positionals = append(positionals, spec)
|
||||
} else {
|
||||
options = append(options, spec)
|
||||
case spec.long != "":
|
||||
longOptions = append(longOptions, spec)
|
||||
case spec.short != "":
|
||||
shortOptions = append(shortOptions, spec)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "usage: %s", filepath.Base(os.Args[0]))
|
||||
// print the beginning of the usage string
|
||||
fmt.Fprintf(w, "Usage: %s", p.cmd.name)
|
||||
for _, s := range subcommand {
|
||||
fmt.Fprint(w, " "+s)
|
||||
}
|
||||
|
||||
// write the option component of the usage message
|
||||
for _, spec := range options {
|
||||
for _, spec := range shortOptions {
|
||||
// prefix with a space
|
||||
fmt.Fprint(w, " ")
|
||||
if !spec.required {
|
||||
fmt.Fprint(w, "[")
|
||||
}
|
||||
fmt.Fprint(w, synopsis(spec, "-"+spec.short))
|
||||
if !spec.required {
|
||||
fmt.Fprint(w, "]")
|
||||
}
|
||||
}
|
||||
|
||||
for _, spec := range longOptions {
|
||||
// prefix with a space
|
||||
fmt.Fprint(w, " ")
|
||||
if !spec.required {
|
||||
|
@ -45,90 +90,254 @@ func (p *Parser) WriteUsage(w io.Writer) {
|
|||
}
|
||||
}
|
||||
|
||||
// write the positional component of the usage message
|
||||
// When we parse positionals, we check that:
|
||||
// 1. required positionals come before non-required positionals
|
||||
// 2. there is at most one multiple-value positional
|
||||
// 3. if there is a multiple-value positional then it comes after all other positionals
|
||||
// Here we merely print the usage string, so we do not explicitly re-enforce those rules
|
||||
|
||||
// write the positionals in following form:
|
||||
// REQUIRED1 REQUIRED2
|
||||
// REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]
|
||||
// REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]
|
||||
// REQUIRED1 REQUIRED2 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]
|
||||
// REQUIRED1 REQUIRED2 [OPTIONAL1 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]]
|
||||
var closeBrackets int
|
||||
for _, spec := range positionals {
|
||||
// prefix with a space
|
||||
fmt.Fprint(w, " ")
|
||||
up := strings.ToUpper(spec.long)
|
||||
if spec.multiple {
|
||||
fmt.Fprintf(w, "[%s [%s ...]]", up, up)
|
||||
if !spec.required {
|
||||
fmt.Fprint(w, "[")
|
||||
closeBrackets += 1
|
||||
}
|
||||
if spec.cardinality == multiple {
|
||||
fmt.Fprintf(w, "%s [%s ...]", spec.placeholder, spec.placeholder)
|
||||
} else {
|
||||
fmt.Fprint(w, up)
|
||||
fmt.Fprint(w, spec.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")
|
||||
return nil
|
||||
}
|
||||
|
||||
// print prints a line like this:
|
||||
//
|
||||
// --option FOO A description of the option [default: 123]
|
||||
//
|
||||
// If the text on the left is longer than a certain threshold, the description is moved to the next line:
|
||||
//
|
||||
// --verylongoptionoption VERY_LONG_VARIABLE
|
||||
// A description of the option [default: 123]
|
||||
//
|
||||
// If multiple "extras" are provided then they are put inside a single set of square brackets:
|
||||
//
|
||||
// --option FOO A description of the option [default: 123, env: FOO]
|
||||
func print(w io.Writer, item, description string, bracketed ...string) {
|
||||
lhs := " " + item
|
||||
fmt.Fprint(w, lhs)
|
||||
if description != "" {
|
||||
if len(lhs)+2 < colWidth {
|
||||
fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs)))
|
||||
} else {
|
||||
fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
|
||||
}
|
||||
fmt.Fprint(w, description)
|
||||
}
|
||||
|
||||
var brack string
|
||||
for _, s := range bracketed {
|
||||
if s != "" {
|
||||
if brack != "" {
|
||||
brack += ", "
|
||||
}
|
||||
brack += s
|
||||
}
|
||||
}
|
||||
|
||||
if brack != "" {
|
||||
fmt.Fprintf(w, " [%s]", brack)
|
||||
}
|
||||
fmt.Fprint(w, "\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) {
|
||||
var positionals, options []*spec
|
||||
for _, spec := range p.spec {
|
||||
if spec.positional {
|
||||
p.WriteHelpForSubcommand(w, p.subcommand...)
|
||||
}
|
||||
|
||||
// WriteHelpForSubcommand writes the usage string followed by the full help
|
||||
// string for a specified subcommand. To write help for a top-level subcommand,
|
||||
// provide just the name of that subcommand. To write help for a subcommand that
|
||||
// is nested under another subcommand, provide a sequence of subcommand names
|
||||
// starting with the top-level subcommand and so on down the tree.
|
||||
func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error {
|
||||
cmd, err := p.lookupCommand(subcommand...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var positionals, longOptions, shortOptions, envOnlyOptions []*spec
|
||||
var hasVersionOption bool
|
||||
for _, spec := range cmd.specs {
|
||||
switch {
|
||||
case spec.positional:
|
||||
positionals = append(positionals, spec)
|
||||
} else {
|
||||
options = append(options, 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)
|
||||
}
|
||||
}
|
||||
|
||||
p.WriteUsage(w)
|
||||
// obtain a flattened list of options from all ancestors
|
||||
// also determine if any ancestor has a version option spec
|
||||
var globals []*spec
|
||||
ancestor := cmd.parent
|
||||
for ancestor != nil {
|
||||
for _, spec := range ancestor.specs {
|
||||
if spec.long == "version" {
|
||||
hasVersionOption = true
|
||||
break
|
||||
}
|
||||
}
|
||||
globals = append(globals, ancestor.specs...)
|
||||
ancestor = ancestor.parent
|
||||
}
|
||||
|
||||
if p.description != "" {
|
||||
fmt.Fprintln(w, p.description)
|
||||
}
|
||||
|
||||
if !hasVersionOption && p.version != "" {
|
||||
fmt.Fprintln(w, p.version)
|
||||
}
|
||||
|
||||
p.WriteUsageForSubcommand(w, subcommand...)
|
||||
|
||||
// write the list of positionals
|
||||
if len(positionals) > 0 {
|
||||
fmt.Fprint(w, "\npositional arguments:\n")
|
||||
fmt.Fprint(w, "\nPositional arguments:\n")
|
||||
for _, spec := range positionals {
|
||||
left := " " + spec.long
|
||||
fmt.Fprint(w, left)
|
||||
if spec.help != "" {
|
||||
if len(left)+2 < colWidth {
|
||||
fmt.Fprint(w, strings.Repeat(" ", colWidth-len(left)))
|
||||
} else {
|
||||
fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
|
||||
}
|
||||
fmt.Fprint(w, spec.help)
|
||||
}
|
||||
fmt.Fprint(w, "\n")
|
||||
print(w, spec.placeholder, spec.help, withDefault(spec.defaultString), withEnv(spec.env))
|
||||
}
|
||||
}
|
||||
|
||||
// write the list of options
|
||||
fmt.Fprint(w, "\noptions:\n")
|
||||
for _, spec := range options {
|
||||
printOption(w, spec)
|
||||
// write the list of options with the short-only ones first to match the usage string
|
||||
if len(shortOptions)+len(longOptions) > 0 || cmd.parent == nil {
|
||||
fmt.Fprint(w, "\nOptions:\n")
|
||||
for _, spec := range shortOptions {
|
||||
p.printOption(w, spec)
|
||||
}
|
||||
for _, spec := range longOptions {
|
||||
p.printOption(w, spec)
|
||||
}
|
||||
}
|
||||
|
||||
// write the list of global options
|
||||
if len(globals) > 0 {
|
||||
fmt.Fprint(w, "\nGlobal options:\n")
|
||||
for _, spec := range globals {
|
||||
p.printOption(w, spec)
|
||||
}
|
||||
}
|
||||
|
||||
// write the list of built in options
|
||||
printOption(w, &spec{isBool: true, long: "help", short: "h", help: "display this help and exit"})
|
||||
p.printOption(w, &spec{
|
||||
cardinality: zero,
|
||||
long: "help",
|
||||
short: "h",
|
||||
help: "display this help and exit",
|
||||
})
|
||||
if !hasVersionOption && p.version != "" {
|
||||
p.printOption(w, &spec{
|
||||
cardinality: zero,
|
||||
long: "version",
|
||||
help: "display version and exit",
|
||||
})
|
||||
}
|
||||
|
||||
// write the list of environment only variables
|
||||
if len(envOnlyOptions) > 0 {
|
||||
fmt.Fprint(w, "\nEnvironment variables:\n")
|
||||
for _, spec := range envOnlyOptions {
|
||||
p.printEnvOnlyVar(w, spec)
|
||||
}
|
||||
}
|
||||
|
||||
// write the list of subcommands
|
||||
if len(cmd.subcommands) > 0 {
|
||||
fmt.Fprint(w, "\nCommands:\n")
|
||||
for _, subcmd := range cmd.subcommands {
|
||||
names := append([]string{subcmd.name}, subcmd.aliases...)
|
||||
print(w, strings.Join(names, ", "), subcmd.help)
|
||||
}
|
||||
}
|
||||
|
||||
if p.epilogue != "" {
|
||||
fmt.Fprintln(w, "\n"+p.epilogue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printOption(w io.Writer, spec *spec) {
|
||||
left := " " + synopsis(spec, "--"+spec.long)
|
||||
func (p *Parser) printOption(w io.Writer, spec *spec) {
|
||||
ways := make([]string, 0, 2)
|
||||
if spec.long != "" {
|
||||
ways = append(ways, synopsis(spec, "--"+spec.long))
|
||||
}
|
||||
if spec.short != "" {
|
||||
left += ", " + synopsis(spec, "-"+spec.short)
|
||||
ways = append(ways, synopsis(spec, "-"+spec.short))
|
||||
}
|
||||
fmt.Fprint(w, left)
|
||||
if len(ways) > 0 {
|
||||
print(w, strings.Join(ways, ", "), spec.help, withDefault(spec.defaultString), withEnv(spec.env))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) {
|
||||
ways := make([]string, 0, 2)
|
||||
if spec.required {
|
||||
ways = append(ways, "Required.")
|
||||
} else {
|
||||
ways = append(ways, "Optional.")
|
||||
}
|
||||
|
||||
if spec.help != "" {
|
||||
if len(left)+2 < colWidth {
|
||||
fmt.Fprint(w, strings.Repeat(" ", colWidth-len(left)))
|
||||
} else {
|
||||
fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
|
||||
}
|
||||
fmt.Fprint(w, spec.help)
|
||||
ways = append(ways, spec.help)
|
||||
}
|
||||
// Check if spec.dest is zero value or not
|
||||
// If it isn't a default value have been added
|
||||
v := spec.dest
|
||||
if v.IsValid() {
|
||||
z := reflect.Zero(v.Type())
|
||||
if v.Type().Comparable() && z.Type().Comparable() && v.Interface() != z.Interface() {
|
||||
fmt.Fprintf(w, " [default: %v]", v)
|
||||
}
|
||||
}
|
||||
fmt.Fprint(w, "\n")
|
||||
|
||||
print(w, spec.env, strings.Join(ways, " "), withDefault(spec.defaultString))
|
||||
}
|
||||
|
||||
func synopsis(spec *spec, form string) string {
|
||||
if spec.isBool {
|
||||
// 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 + " " + strings.ToUpper(spec.long)
|
||||
return form + " " + spec.placeholder
|
||||
}
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
package arg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWriteUsage(t *testing.T) {
|
||||
expectedUsage := "usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] INPUT [OUTPUT [OUTPUT ...]]\n"
|
||||
|
||||
expectedHelp := `usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] 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
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
Input string `arg:"positional"`
|
||||
Output []string `arg:"positional,help:list of outputs"`
|
||||
Name string `arg:"help:name to use"`
|
||||
Value int `arg:"help:secret value"`
|
||||
Verbose bool `arg:"-v,help:verbosity level"`
|
||||
Dataset string `arg:"help:dataset to use"`
|
||||
Optimize int `arg:"-O,help:optimization level"`
|
||||
Ids []int64 `arg:"help:Ids"`
|
||||
}
|
||||
args.Name = "Foo Bar"
|
||||
args.Value = 42
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
os.Args[0] = "example"
|
||||
|
||||
var usage bytes.Buffer
|
||||
p.WriteUsage(&usage)
|
||||
assert.Equal(t, expectedUsage, usage.String())
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp, help.String())
|
||||
}
|
||||
|
||||
func TestUsageLongPositionalWithHelp(t *testing.T) {
|
||||
expectedHelp := `usage: example VERYLONGPOSITIONALWITHHELP
|
||||
|
||||
positional arguments:
|
||||
verylongpositionalwithhelp
|
||||
this positional argument is very long
|
||||
|
||||
options:
|
||||
--help, -h display this help and exit
|
||||
`
|
||||
var args struct {
|
||||
VeryLongPositionalWithHelp string `arg:"positional,help:this positional argument is very long"`
|
||||
}
|
||||
|
||||
p, err := NewParser(&args)
|
||||
require.NoError(t, err)
|
||||
|
||||
os.Args[0] = "example"
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
assert.Equal(t, expectedHelp, help.String())
|
||||
}
|
Loading…
Reference in New Issue