Compare commits

...

151 Commits

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

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

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

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

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

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

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

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

Resolves #189
2022-09-17 12:55:00 +02:00
Alex Flint ebd7a68a06
Merge pull request #172 from SebastiaanPasterkamp/ignore-default-option
Add 'IgnoreDefault' option
2022-06-11 09:06:03 -04:00
Alex Flint 23b2b67fe2 fix issue #184 2022-06-09 11:21:29 -04:00
Sebastiaan Pasterkamp b48371a62f Simplify sub-command initialization w/o IgnoreDefault 2022-06-05 17:54:46 +02:00
Alex Flint f0f44b65d1
Merge pull request #175 from alexflint/bracketing-positionals
Fix bracketing for non-required positionals in usage
2022-02-09 18:04:03 -08:00
Alex Flint 5fb236a65d fix bracketing for non-required positionals 2022-02-09 06:31:34 -08:00
Alex Flint d3706100bf
Merge pull request #173 from GreyXor/master
Update testify dependency to 1.7.0
2022-01-05 10:02:04 -05:00
GreyXor 25d4d1c864
Update go.sum 2022-01-05 15:58:54 +01:00
GreyXor 3bf2a5e78a
Update testify dependency to 1.7.0 2022-01-05 15:43:52 +01:00
Sebastiaan Pasterkamp a87d80089a Add 'IgnoreDefault' option 2022-01-02 15:17:09 +01:00
Alex Flint bf32f08247
Merge pull request #166 from alexflint/env-in-error
Put name of environment variable in error message
2021-10-01 04:44:19 -07:00
Alex Flint b47d6e3da6 put name of environment variable in error message 2021-10-01 04:35:15 -07:00
Alex Flint a4afd6a849
Merge pull request #156 from alexflint/usage-for-subcommands
add FailSubcommand, WriteUsageForSubcommand, WriteHelpForSubcommand
2021-09-18 08:57:29 -07:00
Alex Flint f2f876420c Merge remote-tracking branch 'origin/master' into usage-for-subcommands 2021-09-18 08:55:40 -07:00
Alex Flint 66cb696e79
Merge pull request #164 from evgenv123/evgenv123-patch-1
Update README.md
2021-09-18 08:53:02 -07:00
Alex Flint 0f0b4b5c3f
Update README.md 2021-09-18 08:50:33 -07:00
evgenv123 b157e8d10a
Update README.md
Hi! As a first-time user of your great package I got a little bit confused on using command line args and env vars together, so it took me some time to make testing and I propose to save this time for other people by adding relevant edits to README.md
2021-09-18 22:23:26 +07:00
Alex Flint ff38a63b36
Merge pull request #162 from alexflint/support-for-urls
Add support for URLs
2021-08-20 19:59:18 -07:00
Alex Flint 3d59e5e89e bump go-scalar to v1.1 and add documentation about supported types 2021-08-20 19:52:48 -07:00
Alex Flint eb0393e9bc
Merge pull request #158 from alexflint/unexported-embedded
Recurse into unexported embedded structs
2021-05-24 21:50:33 -07:00
Alex Flint fa12c02e81 recurse into unexported embedded structs 2021-05-24 21:45:11 -07:00
Alex Flint 7cc8da61cf simplify the error string logic 2021-05-09 14:01:08 -07:00
Alex Flint c9b504edc1 add FailSubcommand, WriteUsageForSubcommand, WriteHelpForSubcommand 2021-05-09 13:55:34 -07:00
Alex Flint 679be43af3
Merge pull request #153 from alexflint/test-empty-map
Fix case where an empty environment variable is parsed in a slice or map
2021-04-20 19:11:21 -07:00
Alex Flint 2e81334206 fix case where an environment variable containing an empty string is parsed into a slice or map 2021-04-20 19:09:47 -07:00
Alex Flint 9d937ba6c9
Merge pull request #152 from alexflint/mappings-with-commas
Add an example of mappings with commas
2021-04-20 12:26:56 -07:00
Alex Flint 1e81bb6866 fix the mappings-with-commas example 2021-04-20 12:23:49 -07:00
Alex Flint 4354574615
Merge pull request #151 from alexflint/fix-lint
Fix lint issue in reflect.go
2021-04-20 12:20:36 -07:00
Alex Flint 473453d8c8 fix lint issue 2021-04-20 12:14:14 -07:00
Alex Flint a84487a43a updated the example for mappings with commas 2021-04-19 21:35:09 -07:00
Alex Flint a0937d1b58
Merge pull request #150 from alexflint/push-coverage-up-more
Push test coverage up to 100%
2021-04-19 21:27:53 -07:00
Alex Flint 01a9fab8d7 clean up environment variable tests 2021-04-19 21:22:12 -07:00
Alex Flint fe4a138ac8 test coverage 100% !! 2021-04-19 21:03:43 -07:00
Alex Flint 6a01a15f75
Merge pull request #149 from alexflint/parse-into-map
Add support for parsing into a map
2021-04-19 19:27:31 -07:00
Alex Flint d4b9b2a008 push coverage up even more 2021-04-19 19:23:08 -07:00
Alex Flint a80336128c more test coverage 2021-04-19 14:50:05 -07:00
Alex Flint 57f610284f add an runnable example for mappings 2021-04-19 14:01:29 -07:00
Alex Flint 91214e01ea add an example of parsing into a map to the readme 2021-04-19 13:59:00 -07:00
Alex Flint 0100c0a411 push code coverage up 2021-04-19 13:48:07 -07:00
Alex Flint bb4e7fd4b0 add unittests for maps as environment variables with the separate flag 2021-04-19 13:33:31 -07:00
Alex Flint ccf882dca7 finish adding comments to spec 2021-04-19 13:25:11 -07:00
Alex Flint 9949860eb3 change "kind" to "cardinality", add support for maps to parser 2021-04-19 13:21:04 -07:00
Alex Flint 23b96d7aac refactor canParse into kindOf 2021-04-19 12:49:49 -07:00
Alex Flint 1dfefdc43e factor setSlice into its own file, add setMap, and add tests for both 2021-04-19 12:10:53 -07:00
Alex Flint f4eb7f3a58
Merge pull request #137 from alexflint/optional-long
Optional long names
2021-04-16 21:03:14 -07:00
Alex Flint 113aef7114
Merge pull request #147 from alexflint/ci-go16
Add go 1.16 to CI
2021-04-16 21:01:28 -07:00
Alex Flint cec6b0d378 add go 1.6 to CI 2021-04-16 20:45:13 -07:00
Alex Flint 172800ff9a fix a comment 2021-04-16 20:44:18 -07:00
Alex Flint cf2205c84d
Merge pull request #144 from leozhantw/refactor/remove-unused-function
refactor: remove unused function
2021-04-07 11:32:48 -07:00
LeoZhan 4f2ab5c009 refactor: remove unused function 2021-04-08 00:20:56 +08:00
Alex Flint ec285c8ec4 Merge remote-tracking branch 'origin/master' into optional-long 2021-01-31 19:43:30 -08:00
Alex Flint bd6844a20d
Merge pull request #138 from alexflint/ci-go15
Add go1.15 to CI
2021-01-31 19:43:20 -08:00
Alex Flint efe5cdf4da replace "name" and "typ" by storing the original StructField 2021-01-31 19:40:38 -08:00
Alex Flint b099bc916b fix typo 2021-01-31 19:27:06 -08:00
Alex Flint bfa189218e add go1.15 to CI, drop go1.11 2021-01-31 19:25:17 -08:00
Alex Flint 2a91531140 Merge remote-tracking branch 'origin/master' into optional-long 2021-01-31 19:20:28 -08:00
Alex Flint 788c166025 test that short-only options are printed first in the help message 2021-01-31 19:15:49 -08:00
Alex Flint 2a23168641
Merge pull request #136 from alexflint/ignore-unexported
Skip unexported fields
2021-01-31 18:46:05 -08:00
Andrew Morozko 438a91dba1 Skip right column if the left is empty 2020-12-20 03:51:33 +03:00
Andrew Morozko 04c3fdbd80 Updated readme 2020-12-20 03:07:18 +03:00
Andrew Morozko faebd3e0f2 Optional long arguments 2020-12-20 02:54:03 +03:00
23 changed files with 3446 additions and 831 deletions

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

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

View File

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

2
.gitignore vendored
View File

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

11
Makefile Normal file
View File

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

362
README.md
View File

@ -64,7 +64,7 @@ fmt.Println("Input:", args.Input)
fmt.Println("Output:", args.Output)
```
```
```shell
$ ./example src.txt x.out y.out z.out
Input: src.txt
Output: [x.out y.out z.out]
@ -80,12 +80,12 @@ arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```
```shell
$ WORKERS=4 ./example
Workers: 4
```
```
```shell
$ WORKERS=4 ./example --workers=6
Workers: 6
```
@ -100,12 +100,12 @@ arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```
```shell
$ NUM_WORKERS=4 ./example
Workers: 4
```
You can provide multiple values using the CSV (RFC 4180) format:
You can provide multiple values in environment variables using commas:
```go
var args struct {
@ -115,12 +115,50 @@ arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```
```shell
$ WORKERS='1,99' ./example
Workers: [1 99]
```
Command line arguments take precedence over environment variables:
```go
var args struct {
Workers int `arg:"--count,env:NUM_WORKERS"`
}
arg.MustParse(&args)
fmt.Println("Workers:", args.Workers)
```
```shell
$ NUM_WORKERS=6 ./example
Workers: 6
$ NUM_WORKERS=6 ./example --count 4
Workers: 4
```
Configuring a global environment variable name prefix is also possible:
```go
var args struct {
Workers int `arg:"--count,env:NUM_WORKERS"`
}
p, err := arg.NewParser(arg.Config{
EnvPrefix: "MYAPP_",
}, &args)
p.MustParse(os.Args[1:])
fmt.Println("Workers:", args.Workers)
```
```shell
$ MYAPP_NUM_WORKERS=6 ./example
Workers: 6
```
### Usage strings
```go
var args struct {
Input string `arg:"positional"`
@ -134,10 +172,10 @@ arg.MustParse(&args)
```shell
$ ./example -h
Usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]]
Usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]]
Positional arguments:
INPUT
INPUT
OUTPUT
Options:
@ -158,18 +196,32 @@ var args struct {
arg.MustParse(&args)
```
### Default values (before v1.2)
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 {
Foo string
Bar bool
Test string `arg:"-t,env:TEST" default:"something"`
}
arg.Foo = "abc"
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
@ -185,12 +237,14 @@ Fetching the following IDs from foo: [1 2 3]
```
### Arguments that can be specified multiple times, mixed with positionals
```go
var args struct {
Commands []string `arg:"-c,separate"`
Files []string `arg:"-f,separate"`
Databases []string `arg:"positional"`
}
arg.MustParse(&args)
```
```shell
@ -200,22 +254,19 @@ Files [file1 file2 file3]
Databases [db1 db2 db3]
```
### Custom validation
### Arguments with keys and values
```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")
UserIDs map[string]int
}
arg.MustParse(&args)
fmt.Println(args.UserIDs)
```
```shell
./example
Usage: samples [--foo FOO] [--bar BAR]
error: you must provide either --foo or --bar
./example --userids john=123 mary=456
map[john:123 mary:456]
```
### Version strings
@ -240,35 +291,57 @@ $ ./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"`
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: [--short SHORT] [--custom-long-option CUSTOM-LONG-OPTION] [--my-option MY-OPTION]
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
@ -291,6 +364,22 @@ func main() {
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.
@ -320,6 +409,7 @@ func main() {
fmt.Printf("%#v\n", args.Name)
}
```
```shell
$ ./example --name=foo.bar
main.NameDotName{Head:"foo", Tail:"bar"}
@ -356,6 +446,7 @@ func main() {
fmt.Printf("%#v\n", args.Name)
}
```
```shell
$ ./example --help
Usage: test [--name NAME]
@ -370,8 +461,6 @@ main.NameDotName{Head:"file", Tail:"txt"}
### Custom placeholders
*Introduced in version 1.3.0*
Use the `placeholder` tag to control which placeholder text is used in the usage text.
```go
@ -383,6 +472,7 @@ var args struct {
}
arg.MustParse(&args)
```
```shell
$ ./example -h
Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
@ -400,6 +490,9 @@ Options:
### Description strings
A descriptive message can be added at the top of the help text by implementing
a `Description` function that returns a string.
```go
type args struct {
Foo string
@ -425,9 +518,36 @@ Options:
--help, -h display this help and exit
```
### Subcommands
Similarly an epilogue can be added at the end of the help text by implementing
the `Epilogue` function.
*Introduced in version 1.1.0*
```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
@ -489,15 +609,187 @@ if p.Subcommand() == nil {
}
```
### 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://godoc.org/github.com/alexflint/go-arg
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 `flag` library that ships in the standard library seems awkward to me. Positional arguments must preceed options, so `./prog x --foo=1` does what you expect but `./prog --foo=1 x` does not. It also does not allow arguments to have both long (`--foo`) and short (`-f`) forms.
The `flag` library that ships in the standard library seems awkward to me. Positional arguments must precede options, so `./prog x --foo=1` does what you expect but `./prog --foo=1 x` does not. It also does not allow arguments to have both long (`--foo`) and short (`-f`) forms.
Many third-party argument parsing libraries are great for writing sophisticated command line interfaces, but feel to me like overkill for a simple script with a few flags.
@ -505,4 +797,4 @@ The idea behind `go-arg` is that Go already has an excellent way to describe dat
### 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 removes most of the limits on the text you can write. In particular, you will need to use the new `help` tag if your help text includes any commas.
Earlier versions of this library required the help text to be part of the `arg` tag. This is still supported but is now deprecated. Instead, you should use a separate `help` tag, described above, which makes it possible to include commas inside help text.

8
go.mod
View File

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

8
go.sum
View File

@ -1,8 +0,0 @@
github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi+A70=
github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw=
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.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=

458
parse.go
View File

@ -5,17 +5,15 @@ import (
"encoding/csv"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strings"
scalar "github.com/alexflint/go-scalar"
"go.wit.com/dev/alexflint/scalar"
)
// to enable monkey-patching during tests
var osExit = os.Exit
// path represents a sequence of steps to find the output location for an
// argument or subcommand in the final destination struct
type path struct {
@ -46,24 +44,25 @@ func (p path) Child(f reflect.StructField) path {
// spec represents a command line option
type spec struct {
dest path
typ reflect.Type
long string
short string
multiple bool
required bool
positional bool
separate bool
help string
env string
boolean bool
defaultVal string // default value for this option
placeholder string // name of the data in help
dest path
field reflect.StructField // the struct field from which this option was created
long string // the --long form for this option, or empty if none
short string // the -s short form for this option, or empty if none
cardinality cardinality // determines how many tokens will be present (possible values: zero, one, multiple)
required bool // if true, this option must be present on the command line
positional bool // if true, this option will be looked for in the positional flags
separate bool // if true, each slice and map entry will have its own --flag
help string // the help text for this option
env string // the name of the environment variable for this option, or empty for none
defaultValue reflect.Value // default value for this option
defaultString string // default value for this option, in string form to be displayed in help text
placeholder string // placeholder string in help
}
// command represents a named subcommand, or the top-level command
type command struct {
name string
aliases []string
help string
dest path
specs []*spec
@ -71,33 +70,49 @@ type command struct {
parent *command
}
// ErrHelp indicates that -h or --help were provided
// ErrHelp indicates that the builtin -h or --help were provided
var ErrHelp = errors.New("help requested by user")
// ErrVersion indicates that --version was provided
// ErrVersion indicates that the builtin --version was provided
var ErrVersion = errors.New("version requested by user")
// for monkey patching in example and test code
var mustParseExit = os.Exit
var mustParseOut io.Writer = os.Stdout
// This stores the args sent from modules
var register []interface{}
/*
Use this in your packages to register
variables with go-arg. Then add this to your init()
package 'foo'
function init() {
args.Register(&argsFoo)
}
*/
func Register(dest ...interface{}) {
register = append(register, dest...)
}
// MustParse processes command line arguments and exits upon failure
func MustParse(dest ...interface{}) *Parser {
p, err := NewParser(Config{}, dest...)
register = append(register, dest...)
return mustParse(Config{Exit: mustParseExit, Out: mustParseOut}, register...)
}
// mustParse is a helper that facilitates testing
func mustParse(config Config, dest ...interface{}) *Parser {
p, err := NewParser(config, dest...)
if err != nil {
fmt.Println(err)
osExit(-1)
return nil // just in case osExit was monkey-patched
}
err = p.Parse(flags())
switch {
case err == ErrHelp:
p.writeHelpForCommand(os.Stdout, p.lastCmd)
osExit(0)
case err == ErrVersion:
fmt.Println(p.version)
osExit(0)
case err != nil:
p.failWithCommand(err.Error(), p.lastCmd)
fmt.Fprintln(config.Out, err)
config.Exit(2)
return nil
}
p.MustParse(flags())
return p
}
@ -125,6 +140,23 @@ type Config struct {
// IgnoreEnv instructs the library not to read environment variables
IgnoreEnv bool
// IgnoreDefault instructs the library not to reset the variables to the
// default values, including pointers to sub commands
IgnoreDefault bool
// StrictSubcommands intructs the library not to allow global commands after
// subcommand
StrictSubcommands bool
// EnvPrefix instructs the library to use a name prefix when reading environment variables.
EnvPrefix string
// Exit is called to terminate the process with an error code (defaults to os.Exit)
Exit func(int)
// Out is where help text, usage text, and failure messages are printed (defaults to os.Stdout)
Out io.Writer
}
// Parser represents a set of command line options with destination values
@ -134,9 +166,10 @@ type Parser struct {
config Config
version string
description string
epilogue string
// the following field changes during processing of command line arguments
lastCmd *command
subcommand []string
}
// Versioned is the interface that the destination struct should implement to
@ -155,6 +188,14 @@ type Described interface {
Description() string
}
// Epilogued is the interface that the destination struct should implement to
// add an epilogue string at the bottom of the help message.
type Epilogued interface {
// Epilogue returns the string that will be printed on a line by itself
// at the end of the help message.
Epilogue() string
}
// walkFields calls a function for each field of a struct, recursively expanding struct fields.
func walkFields(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool) {
walkFieldsImpl(t, visit, nil)
@ -178,6 +219,14 @@ func walkFieldsImpl(t reflect.Type, visit func(field reflect.StructField, owner
// NewParser constructs a parser from a list of destination structs
func NewParser(config Config, dests ...interface{}) (*Parser, error) {
// fill in defaults
if config.Exit == nil {
config.Exit = os.Exit
}
if config.Out == nil {
config.Out = os.Stdout
}
// first pick a name for the command for use in the usage text
var name string
switch {
@ -207,23 +256,36 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", t))
}
cmd, err := cmdFromStruct(name, path{root: i}, t)
cmd, err := cmdFromStruct(name, path{root: i}, t, config.EnvPrefix)
if err != nil {
return nil, err
}
// add nonzero field values as defaults
// for backwards compatibility, add nonzero field values as defaults
// this applies only to the top-level command, not to subcommands (this inconsistency
// is the reason that this method for setting default values was deprecated)
for _, spec := range cmd.specs {
if v := p.val(spec.dest); v.IsValid() && !isZero(v) {
if defaultVal, ok := v.Interface().(encoding.TextMarshaler); ok {
str, err := defaultVal.MarshalText()
if err != nil {
return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
}
spec.defaultVal = string(str)
} else {
spec.defaultVal = fmt.Sprintf("%v", v)
// get the value
v := p.val(spec.dest)
// if the value is the "zero value" (e.g. nil pointer, empty struct) then ignore
if isZero(v) {
continue
}
// store as a default
spec.defaultValue = v
// we need a string to display in help text
// if MarshalText is implemented then use that
if m, ok := v.Interface().(encoding.TextMarshaler); ok {
s, err := m.MarshalText()
if err != nil {
return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
}
spec.defaultString = string(s)
} else {
spec.defaultString = fmt.Sprintf("%v", v)
}
}
@ -236,12 +298,15 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
if dest, ok := dest.(Described); ok {
p.description = dest.Description()
}
if dest, ok := dest.(Epilogued); ok {
p.epilogue = dest.Epilogue()
}
}
return &p, nil
}
func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*command, error) {
// commands can only be created from pointers to structs
if t.Kind() != reflect.Ptr {
return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a %s",
@ -261,23 +326,30 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
var errs []string
walkFields(t, func(field reflect.StructField, t reflect.Type) bool {
// Check for the ignore switch in the tag
// check for the ignore switch in the tag
tag := field.Tag.Get("arg")
if tag == "-" || !isExported(field.Name) {
if tag == "-" {
return false
}
// If this is an embedded struct then recurse into its fields
// if this is an embedded struct then recurse into its fields, even if
// it is unexported, because exported fields on unexported embedded
// structs are still writable
if field.Anonymous && field.Type.Kind() == reflect.Struct {
return true
}
// ignore any other unexported field
if !isExported(field.Name) {
return false
}
// duplicate the entire path to avoid slice overwrites
subdest := dest.Child(field)
spec := spec{
dest: subdest,
long: strings.ToLower(field.Name),
typ: field.Type,
dest: subdest,
field: field,
long: strings.ToLower(field.Name),
}
help, exists := field.Tag.Lookup("help")
@ -285,13 +357,8 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
spec.help = help
}
defaultVal, hasDefault := field.Tag.Lookup("default")
if hasDefault {
spec.defaultVal = defaultVal
}
// Look at the tag
var isSubcommand bool // tracks whether this field is a subcommand
// process each comma-separated part of the tag
var isSubcommand bool
for _, key := range strings.Split(tag, ",") {
if key == "" {
continue
@ -309,18 +376,13 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
case strings.HasPrefix(key, "--"):
spec.long = key[2:]
case strings.HasPrefix(key, "-"):
if len(key) != 2 {
if len(key) > 2 {
errs = append(errs, fmt.Sprintf("%s.%s: short arguments must be one character only",
t.Name(), field.Name))
return false
}
spec.short = key[1:]
case key == "required":
if hasDefault {
errs = append(errs, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified",
t.Name(), field.Name))
return false
}
spec.required = true
case key == "positional":
spec.positional = true
@ -331,24 +393,30 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
case key == "env":
// Use override name if provided
if value != "" {
spec.env = value
spec.env = envPrefix + value
} else {
spec.env = strings.ToUpper(field.Name)
spec.env = envPrefix + strings.ToUpper(field.Name)
}
case key == "subcommand":
// decide on a name for the subcommand
cmdname := value
if cmdname == "" {
cmdname = strings.ToLower(field.Name)
var cmdnames []string
if value == "" {
cmdnames = []string{strings.ToLower(field.Name)}
} else {
cmdnames = strings.Split(value, "|")
}
for i := range cmdnames {
cmdnames[i] = strings.TrimSpace(cmdnames[i])
}
// parse the subcommand recursively
subcmd, err := cmdFromStruct(cmdname, subdest, field.Type)
subcmd, err := cmdFromStruct(cmdnames[0], subdest, field.Type, envPrefix)
if err != nil {
errs = append(errs, err.Error())
return false
}
subcmd.aliases = cmdnames[1:]
subcmd.parent = &cmd
subcmd.help = field.Tag.Get("help")
@ -360,34 +428,70 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
}
}
// placeholder is the string used in the help text like this: "--somearg PLACEHOLDER"
placeholder, hasPlaceholder := field.Tag.Lookup("placeholder")
if hasPlaceholder {
spec.placeholder = placeholder
} else {
} else if spec.long != "" {
spec.placeholder = strings.ToUpper(spec.long)
} else {
spec.placeholder = strings.ToUpper(spec.field.Name)
}
// Check whether this field is supported. It's good to do this here rather than
// if this is a subcommand then we've done everything we need to do
if isSubcommand {
return false
}
// check whether this field is supported. It's good to do this here rather than
// wait until ParseValue because it means that a program with invalid argument
// fields will always fail regardless of whether the arguments it received
// exercised those fields.
if !isSubcommand {
cmd.specs = append(cmd.specs, &spec)
var err error
spec.cardinality, err = cardinalityOf(field.Type)
if err != nil {
errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported",
t.Name(), field.Name, field.Type.String()))
return false
}
var parseable bool
parseable, spec.boolean, spec.multiple = canParse(field.Type)
if !parseable {
errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported",
t.Name(), field.Name, field.Type.String()))
return false
}
if spec.multiple && hasDefault {
errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice fields",
defaultString, hasDefault := field.Tag.Lookup("default")
if hasDefault {
// we do not support default values for maps and slices
if spec.cardinality == multiple {
errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields",
t.Name(), field.Name))
return false
}
// a required field cannot also have a default value
if spec.required {
errs = append(errs, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified",
t.Name(), field.Name))
return false
}
// parse the default value
spec.defaultString = defaultString
if field.Type.Kind() == reflect.Ptr {
// here we have a field of type *T and we create a new T, no need to dereference
// in order for the value to be settable
spec.defaultValue = reflect.New(field.Type.Elem())
} else {
// here we have a field of type T and we create a new T and then dereference it
// so that the resulting value is settable
spec.defaultValue = reflect.New(field.Type).Elem()
}
err := scalar.ParseValue(spec.defaultValue, defaultString)
if err != nil {
errs = append(errs, fmt.Sprintf("%s.%s: error processing default value: %v", t.Name(), field.Name, err))
return false
}
}
// add the spec to the list of specs
cmd.specs = append(cmd.specs, &spec)
// if this was an embedded field then we already returned true up above
return false
})
@ -410,8 +514,15 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
return &cmd, nil
}
// Parse processes the given command line option, storing the results in the field
// of the structs from which NewParser was constructed
// Parse processes the given command line option, storing the results in the fields
// of the structs from which NewParser was constructed.
//
// It returns ErrHelp if "--help" is one of the command line args and ErrVersion if
// "--version" is one of the command line args (the latter only applies if the
// destination struct passed to NewParser implements Versioned.)
//
// To respond to --help and --version in the way that MustParse does, see examples
// in the README under "Custom handling of --help and --version".
func (p *Parser) Parse(args []string) error {
err := p.process(args)
if err != nil {
@ -428,6 +539,20 @@ func (p *Parser) Parse(args []string) error {
return err
}
func (p *Parser) MustParse(args []string) {
err := p.Parse(args)
switch {
case err == ErrHelp:
p.WriteHelpForSubcommand(p.config.Out, p.subcommand...)
p.config.Exit(0)
case err == ErrVersion:
fmt.Fprintln(p.config.Out, p.version)
p.config.Exit(0)
case err != nil:
p.FailSubcommand(err.Error(), p.subcommand...)
}
}
// process environment vars for the given arguments
func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error {
for _, spec := range specs {
@ -440,18 +565,22 @@ func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error
continue
}
if spec.multiple {
if spec.cardinality == multiple {
// expect a CSV string in an environment
// variable in the case of multiple values
values, err := csv.NewReader(strings.NewReader(value)).Read()
if err != nil {
return fmt.Errorf(
"error reading a CSV string from environment variable %s with multiple values: %v",
spec.env,
err,
)
var values []string
var err error
if len(strings.TrimSpace(value)) > 0 {
values, err = csv.NewReader(strings.NewReader(value)).Read()
if err != nil {
return fmt.Errorf(
"error reading a CSV string from environment variable %s with multiple values: %v",
spec.env,
err,
)
}
}
if err = setSlice(p.val(spec.dest), values, !spec.separate); err != nil {
if err = setSliceOrMap(p.val(spec.dest), values, !spec.separate); err != nil {
return fmt.Errorf(
"error processing environment variable %s with multiple values: %v",
spec.env,
@ -477,7 +606,7 @@ func (p *Parser) process(args []string) error {
// union of specs for the chain of subcommands encountered so far
curCmd := p.cmd
p.lastCmd = curCmd
p.subcommand = nil
// make a copy of the specs because we will add to this list each time we expand a subcommand
specs := make([]*spec, len(curCmd.specs))
@ -491,6 +620,15 @@ func (p *Parser) process(args []string) error {
}
}
// determine if the current command has a version option spec
var hasVersionOption bool
for _, spec := range curCmd.specs {
if spec.long == "version" {
hasVersionOption = true
break
}
}
// process each string from the command line
var allpositional bool
var positionals []string
@ -498,7 +636,7 @@ func (p *Parser) process(args []string) error {
// must use explicit for loop, not range, because we manipulate i inside the loop
for i := 0; i < len(args); i++ {
arg := args[i]
if arg == "--" {
if arg == "--" && !allpositional {
allpositional = true
continue
}
@ -518,10 +656,17 @@ func (p *Parser) process(args []string) error {
// instantiate the field to point to a new struct
v := p.val(subcmd.dest)
v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers
}
// add the new options to the set of allowed options
specs = append(specs, subcmd.specs...)
if p.config.StrictSubcommands {
specs = make([]*spec, len(subcmd.specs))
copy(specs, subcmd.specs)
} else {
specs = append(specs, subcmd.specs...)
}
// capture environment vars for these new options
if !p.config.IgnoreEnv {
@ -532,7 +677,7 @@ func (p *Parser) process(args []string) error {
}
curCmd = subcmd
p.lastCmd = curCmd
p.subcommand = append(p.subcommand, arg)
continue
}
@ -541,7 +686,9 @@ func (p *Parser) process(args []string) error {
case "-h", "--help":
return ErrHelp
case "--version":
return ErrVersion
if !hasVersionOption && p.version != "" {
return ErrVersion
}
}
// check for an equals sign, as in "--foo=bar"
@ -555,13 +702,13 @@ func (p *Parser) process(args []string) error {
// lookup the spec for this option (note that the "specs" slice changes as
// we expand subcommands so it is better not to use a map)
spec := findOption(specs, opt)
if spec == nil {
if spec == nil || opt == "" {
return fmt.Errorf("unknown argument %s", arg)
}
wasPresent[spec] = true
// deal with the case of multiple values
if spec.multiple {
if spec.cardinality == multiple {
var values []string
if value == "" {
for i+1 < len(args) && !isFlag(args[i+1]) && args[i+1] != "--" {
@ -574,7 +721,7 @@ func (p *Parser) process(args []string) error {
} else {
values = append(values, value)
}
err := setSlice(p.val(spec.dest), values, !spec.separate)
err := setSliceOrMap(p.val(spec.dest), values, !spec.separate)
if err != nil {
return fmt.Errorf("error processing %s: %v", arg, err)
}
@ -583,7 +730,7 @@ func (p *Parser) process(args []string) error {
// if it's a flag and it has no value then set the value to true
// use boolean because this takes account of TextUnmarshaler
if spec.boolean && value == "" {
if spec.cardinality == zero && value == "" {
value = "true"
}
@ -592,7 +739,7 @@ func (p *Parser) process(args []string) error {
if i+1 == len(args) {
return fmt.Errorf("missing value for %s", arg)
}
if !nextIsNumeric(spec.typ, args[i+1]) && isFlag(args[i+1]) {
if !nextIsNumeric(spec.field.Type, args[i+1]) && isFlag(args[i+1]) {
return fmt.Errorf("missing value for %s", arg)
}
value = args[i+1]
@ -614,16 +761,16 @@ func (p *Parser) process(args []string) error {
break
}
wasPresent[spec] = true
if spec.multiple {
err := setSlice(p.val(spec.dest), positionals, true)
if spec.cardinality == multiple {
err := setSliceOrMap(p.val(spec.dest), positionals, true)
if err != nil {
return fmt.Errorf("error processing %s: %v", spec.long, err)
return fmt.Errorf("error processing %s: %v", spec.field.Name, err)
}
positionals = nil
} else {
err := scalar.ParseValue(p.val(spec.dest), positionals[0])
if err != nil {
return fmt.Errorf("error processing %s: %v", spec.long, err)
return fmt.Errorf("error processing %s: %v", spec.field.Name, err)
}
positionals = positionals[1:]
}
@ -638,19 +785,32 @@ func (p *Parser) process(args []string) error {
continue
}
name := spec.long
if !spec.positional {
name := strings.ToLower(spec.field.Name)
if spec.long != "" && !spec.positional {
name = "--" + spec.long
}
if spec.required {
return fmt.Errorf("%s is required", name)
}
if spec.defaultVal != "" {
err := scalar.ParseValue(p.val(spec.dest), spec.defaultVal)
if err != nil {
return fmt.Errorf("error processing default value for %s: %v", name, err)
if spec.short == "" && spec.long == "" {
msg := fmt.Sprintf("environment variable %s is required", spec.env)
return errors.New(msg)
}
msg := fmt.Sprintf("%s is required", name)
if spec.env != "" {
msg += " (or environment variable " + spec.env + ")"
}
return errors.New(msg)
}
if spec.defaultValue.IsValid() && !p.config.IgnoreDefault {
// One issue here is that if the user now modifies the value then
// the default value stored in the spec will be corrupted. There
// is no general way to "deep-copy" values in Go, and we still
// support the old-style method for specifying defaults as
// Go values assigned directly to the struct field, so we are stuck.
p.val(spec.dest).Set(spec.defaultValue)
}
}
@ -687,50 +847,11 @@ func (p *Parser) val(dest path) reflect.Value {
v = v.Elem()
}
next := v.FieldByIndex(field.Index)
if !next.IsValid() {
// it is appropriate to panic here because this can only happen due to
// an internal bug in this library (since we construct the path ourselves
// by reflecting on the same struct)
panic(fmt.Errorf("error resolving path %v: %v has no field named %v",
dest.fields, v.Type(), field))
}
v = next
v = v.FieldByIndex(field.Index)
}
return v
}
// parse a value as the appropriate type and store it in the struct
func setSlice(dest reflect.Value, values []string, trunc bool) error {
if !dest.CanSet() {
return fmt.Errorf("field is not writable")
}
var ptr bool
elem := dest.Type().Elem()
if elem.Kind() == reflect.Ptr && !elem.Implements(textUnmarshalerType) {
ptr = true
elem = elem.Elem()
}
// Truncate the dest slice in case default values exist
if trunc && !dest.IsNil() {
dest.SetLen(0)
}
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
}
// findOption finds an option from its name, or returns null if no spec is found
func findOption(specs []*spec, name string) *spec {
for _, spec := range specs {
@ -750,18 +871,11 @@ func findSubcommand(cmds []*command, name string) *command {
if cmd.name == name {
return cmd
}
for _, alias := range cmd.aliases {
if alias == name {
return cmd
}
}
}
return nil
}
// isZero returns true if v contains the zero value for its type
func isZero(v reflect.Value) bool {
t := v.Type()
if t.Kind() == reflect.Slice {
return v.IsNil()
}
if !t.Comparable() {
return false
}
return v.Interface() == reflect.Zero(t).Interface()
}

View File

@ -2,57 +2,82 @@ package arg
import (
"encoding"
"fmt"
"reflect"
"unicode"
"unicode/utf8"
scalar "github.com/alexflint/go-scalar"
"go.wit.com/dev/alexflint/scalar"
)
var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()
// canParse returns true if the type can be parsed from a string
func canParse(t reflect.Type) (parseable, boolean, multiple bool) {
parseable = scalar.CanParse(t)
boolean = isBoolean(t)
if parseable {
return
}
// 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
// Look inside pointer types
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
// Look inside slice types
if t.Kind() == reflect.Slice {
multiple = true
t = t.Elem()
}
const (
zero cardinality = iota
one
multiple
unsupported
)
parseable = scalar.CanParse(t)
boolean = isBoolean(t)
if parseable {
return
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))
}
// Look inside pointer types (again, in case of []*Type)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
parseable = scalar.CanParse(t)
boolean = isBoolean(t)
if parseable {
return
}
return false, false, false
}
// isBoolean returns true if the type can be parsed from a single string
// 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 t.Implements(textUnmarshalerType):
case isTextUnmarshaler(t):
return false
case t.Kind() == reflect.Bool:
return true
@ -63,8 +88,25 @@ func isBoolean(t reflect.Type) bool {
}
}
// isTextUnmarshaler returns true if the type or its pointer implements encoding.TextUnmarshaler
func isTextUnmarshaler(t reflect.Type) bool {
return t.Implements(textUnmarshalerType) || reflect.PtrTo(t).Implements(textUnmarshalerType)
}
// isExported returns true if the struct field name is exported
func isExported(field string) bool {
r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8
return unicode.IsLetter(r) && unicode.IsUpper(r)
}
// isZero returns true if v contains the zero value for its type
func isZero(v reflect.Value) bool {
t := v.Type()
if t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice || t.Kind() == reflect.Map || t.Kind() == reflect.Chan || t.Kind() == reflect.Interface {
return v.IsNil()
}
if !t.Comparable() {
return false
}
return v.Interface() == reflect.Zero(t).Interface()
}

View File

@ -1,62 +0,0 @@
package arg
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func assertCanParse(t *testing.T, typ reflect.Type, parseable, boolean, multiple bool) {
p, b, m := canParse(typ)
assert.Equal(t, parseable, p, "expected %v to have parseable=%v but was %v", typ, parseable, p)
assert.Equal(t, boolean, b, "expected %v to have boolean=%v but was %v", typ, boolean, b)
assert.Equal(t, multiple, m, "expected %v to have multiple=%v but was %v", typ, multiple, m)
}
func TestCanParse(t *testing.T) {
var b bool
var i int
var s string
var f float64
var bs []bool
var is []int
assertCanParse(t, reflect.TypeOf(b), true, true, false)
assertCanParse(t, reflect.TypeOf(i), true, false, false)
assertCanParse(t, reflect.TypeOf(s), true, false, false)
assertCanParse(t, reflect.TypeOf(f), true, false, false)
assertCanParse(t, reflect.TypeOf(&b), true, true, false)
assertCanParse(t, reflect.TypeOf(&s), true, false, false)
assertCanParse(t, reflect.TypeOf(&i), true, false, false)
assertCanParse(t, reflect.TypeOf(&f), true, false, false)
assertCanParse(t, reflect.TypeOf(bs), true, true, true)
assertCanParse(t, reflect.TypeOf(&bs), true, true, true)
assertCanParse(t, reflect.TypeOf(is), true, false, true)
assertCanParse(t, reflect.TypeOf(&is), true, false, true)
}
type implementsTextUnmarshaler struct{}
func (*implementsTextUnmarshaler) UnmarshalText(text []byte) error {
return nil
}
func TestCanParseTextUnmarshaler(t *testing.T) {
var u implementsTextUnmarshaler
var su []implementsTextUnmarshaler
assertCanParse(t, reflect.TypeOf(u), true, false, false)
assertCanParse(t, reflect.TypeOf(&u), true, false, false)
assertCanParse(t, reflect.TypeOf(su), true, false, true)
assertCanParse(t, reflect.TypeOf(&su), true, false, true)
}
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})))
}

123
sequence.go Normal file
View File

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

View File

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

10
test/Makefile Normal file
View File

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

View File

@ -2,8 +2,12 @@ package arg
import (
"fmt"
"net"
"net/mail"
"net/url"
"os"
"strings"
"time"
)
func split(s string) []string {
@ -82,6 +86,48 @@ func Example_multipleValues() {
// 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() {
@ -108,7 +154,7 @@ func Example_helpText() {
os.Args = split("./example --help")
var args struct {
Input string `arg:"positional"`
Input string `arg:"positional,required"`
Output []string `arg:"positional"`
Verbose bool `arg:"-v" help:"verbosity level"`
Dataset string `help:"dataset to use"`
@ -116,7 +162,8 @@ func Example_helpText() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
@ -141,25 +188,25 @@ func Example_helpPlaceholder() {
os.Args = split("./example --help")
var args struct {
Input string `arg:"positional" placeholder:"SRC"`
Input string `arg:"positional,required" placeholder:"SRC"`
Output []string `arg:"positional" placeholder:"DST"`
Optimize int `arg:"-O" help:"optimization level" placeholder:"LEVEL"`
MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" placeholder:"N"`
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
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
@ -188,7 +235,8 @@ func Example_helpTextWithSubcommand() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
@ -205,7 +253,44 @@ func Example_helpTextWithSubcommand() {
}
// This example shows the usage string generated by go-arg when using subcommands
func Example_helpTextForSubcommand() {
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")
@ -225,28 +310,83 @@ func Example_helpTextForSubcommand() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
exit := func(int) {}
MustParse(&args)
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 get ITEM
// Usage: example list [--format FORMAT] [--limit LIMIT]
//
// Positional arguments:
// ITEM item to fetch
// 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"`
Input string `arg:"positional,required"`
Output []string `arg:"positional"`
Verbose bool `arg:"-v" help:"verbosity level"`
Dataset string `help:"dataset to use"`
@ -254,8 +394,8 @@ func Example_errorText() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stderr = os.Stdout
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
@ -278,8 +418,8 @@ func Example_errorTextForSubcommand() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stderr = os.Stdout
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
@ -314,8 +454,8 @@ func Example_subcommand() {
}
// This is only necessary when running inside golang's runnable example harness
osExit = func(int) {}
stderr = os.Stdout
mustParseExit = func(int) {}
mustParseOut = os.Stdout
MustParse(&args)
@ -331,3 +471,76 @@ func Example_subcommand() {
// 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
}

11
test/go.mod Normal file
View File

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

10
test/go.sum Normal file
View File

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

View File

@ -1,8 +1,12 @@
package arg
import (
"bytes"
"encoding/json"
"fmt"
"net"
"net/mail"
"net/url"
"os"
"strings"
"testing"
@ -24,14 +28,34 @@ func parse(cmdline string, dest interface{}) error {
}
func pparse(cmdline string, dest interface{}) (*Parser, error) {
p, err := NewParser(Config{}, dest)
return parseWithEnv(Config{}, cmdline, nil, dest)
}
func parseWithEnv(config Config, cmdline string, env []string, dest interface{}) (*Parser, error) {
p, err := NewParser(config, dest)
if err != nil {
return nil, err
}
// split the command line
var parts []string
if len(cmdline) > 0 {
parts = strings.Split(cmdline, " ")
}
// split the environment vars
for _, s := range env {
pos := strings.Index(s, "=")
if pos == -1 {
return nil, fmt.Errorf("missing equals sign in %q", s)
}
err := os.Setenv(s[:pos], s[pos+1:])
if err != nil {
return nil, err
}
}
// execute the parser
return p, p.Parse(parts)
}
@ -72,6 +96,21 @@ func TestInt(t *testing.T) {
assert.EqualValues(t, 8, *args.Ptr)
}
func TestHexOctBin(t *testing.T) {
var args struct {
Hex int
Oct int
Bin int
Underscored int
}
err := parse("--hex 0xA --oct 0o10 --bin 0b101 --underscored 123_456", &args)
require.NoError(t, err)
assert.EqualValues(t, 10, args.Hex)
assert.EqualValues(t, 8, args.Oct)
assert.EqualValues(t, 5, args.Bin)
assert.EqualValues(t, 123456, args.Underscored)
}
func TestNegativeInt(t *testing.T) {
var args struct {
Foo int
@ -180,6 +219,22 @@ func TestRequired(t *testing.T) {
require.Error(t, err, "--foo is required")
}
func TestRequiredWithEnv(t *testing.T) {
var args struct {
Foo string `arg:"required,env:FOO"`
}
err := parse("", &args)
require.Error(t, err, "--foo is required (or environment variable FOO)")
}
func TestRequiredWithEnvOnly(t *testing.T) {
var args struct {
Foo string `arg:"required,--,-,env:FOO"`
}
_, err := parseWithEnv(Config{}, "", []string{}, &args)
require.Error(t, err, "environment variable FOO is required")
}
func TestShortFlag(t *testing.T) {
var args struct {
Foo string `arg:"-f"`
@ -220,6 +275,60 @@ func TestLongFlag(t *testing.T) {
assert.Equal(t, "xyz", args.Foo)
}
func TestSlice(t *testing.T) {
var args struct {
Strings []string
}
err := parse("--strings a b c", &args)
require.NoError(t, err)
assert.Equal(t, []string{"a", "b", "c"}, args.Strings)
}
func TestSliceOfBools(t *testing.T) {
var args struct {
B []bool
}
err := parse("--b true false true", &args)
require.NoError(t, err)
assert.Equal(t, []bool{true, false, true}, args.B)
}
func TestMap(t *testing.T) {
var args struct {
Values map[string]int
}
err := parse("--values a=1 b=2 c=3", &args)
require.NoError(t, err)
assert.Len(t, args.Values, 3)
assert.Equal(t, 1, args.Values["a"])
assert.Equal(t, 2, args.Values["b"])
assert.Equal(t, 3, args.Values["c"])
}
func TestMapPositional(t *testing.T) {
var args struct {
Values map[string]int `arg:"positional"`
}
err := parse("a=1 b=2 c=3", &args)
require.NoError(t, err)
assert.Len(t, args.Values, 3)
assert.Equal(t, 1, args.Values["a"])
assert.Equal(t, 2, args.Values["b"])
assert.Equal(t, 3, args.Values["c"])
}
func TestMapWithSeparate(t *testing.T) {
var args struct {
Values map[string]int `arg:"separate"`
}
err := parse("--values a=1 --values b=2 --values c=3", &args)
require.NoError(t, err)
assert.Len(t, args.Values, 3)
assert.Equal(t, 1, args.Values["a"])
assert.Equal(t, 2, args.Values["b"])
assert.Equal(t, 3, args.Values["c"])
}
func TestPlaceholder(t *testing.T) {
var args struct {
Input string `arg:"positional" placeholder:"SRC"`
@ -231,6 +340,18 @@ func TestPlaceholder(t *testing.T) {
assert.NoError(t, err)
}
func TestNoLongName(t *testing.T) {
var args struct {
ShortOnly string `arg:"-s,--"`
EnvOnly string `arg:"--,env"`
}
setenv(t, "ENVONLY", "TestVal")
err := parse("-s TestVal2", &args)
assert.NoError(t, err)
assert.Equal(t, "TestVal", args.EnvOnly)
assert.Equal(t, "TestVal2", args.ShortOnly)
}
func TestCaseSensitive(t *testing.T) {
var args struct {
Lower bool `arg:"-v"`
@ -395,7 +516,7 @@ func TestMissingValueAtEnd(t *testing.T) {
assert.Error(t, err)
}
func TestMissingValueInMIddle(t *testing.T) {
func TestMissingValueInMiddle(t *testing.T) {
var args struct {
Foo string
Bar string
@ -480,6 +601,23 @@ func TestNoMoreOptions(t *testing.T) {
assert.Equal(t, []string{"abc", "--foo", "xyz"}, args.Bar)
}
func TestNoMoreOptionsBeforeHelp(t *testing.T) {
var args struct {
Foo int
}
err := parse("not_an_integer -- --help", &args)
assert.NotEqual(t, ErrHelp, err)
}
func TestNoMoreOptionsTwice(t *testing.T) {
var args struct {
X []string `arg:"positional"`
}
err := parse("-- --", &args)
require.NoError(t, err)
assert.Equal(t, []string{"--"}, args.X)
}
func TestHelpFlag(t *testing.T) {
var args struct {
Foo string
@ -563,13 +701,27 @@ func TestMustParse(t *testing.T) {
assert.NotNil(t, parser)
}
func TestMustParseError(t *testing.T) {
var args struct {
Foo []string `default:""`
}
var exitCode int
var stdout bytes.Buffer
mustParseExit = func(code int) { exitCode = code }
mustParseOut = &stdout
os.Args = []string{"example"}
parser := MustParse(&args)
assert.Nil(t, parser)
assert.Equal(t, 2, exitCode)
assert.Contains(t, stdout.String(), "default values are not supported for slice or map fields")
}
func TestEnvironmentVariable(t *testing.T) {
var args struct {
Foo string `arg:"env"`
}
setenv(t, "FOO", "bar")
os.Args = []string{"example"}
MustParse(&args)
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
require.NoError(t, err)
assert.Equal(t, "bar", args.Foo)
}
@ -577,8 +729,8 @@ func TestEnvironmentVariableNotPresent(t *testing.T) {
var args struct {
NotPresent string `arg:"env"`
}
os.Args = []string{"example"}
MustParse(&args)
_, err := parseWithEnv(Config{}, "", nil, &args)
require.NoError(t, err)
assert.Equal(t, "", args.NotPresent)
}
@ -586,9 +738,8 @@ func TestEnvironmentVariableOverrideName(t *testing.T) {
var args struct {
Foo string `arg:"env:BAZ"`
}
setenv(t, "BAZ", "bar")
os.Args = []string{"example"}
MustParse(&args)
_, err := parseWithEnv(Config{}, "", []string{"BAZ=bar"}, &args)
require.NoError(t, err)
assert.Equal(t, "bar", args.Foo)
}
@ -596,19 +747,16 @@ func TestEnvironmentVariableOverrideArgument(t *testing.T) {
var args struct {
Foo string `arg:"env"`
}
setenv(t, "FOO", "bar")
os.Args = []string{"example", "--foo", "baz"}
MustParse(&args)
assert.Equal(t, "baz", args.Foo)
_, err := parseWithEnv(Config{}, "--foo zzz", []string{"FOO=bar"}, &args)
require.NoError(t, err)
assert.Equal(t, "zzz", args.Foo)
}
func TestEnvironmentVariableError(t *testing.T) {
var args struct {
Foo int `arg:"env"`
}
setenv(t, "FOO", "bar")
os.Args = []string{"example"}
err := Parse(&args)
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
assert.Error(t, err)
}
@ -616,9 +764,8 @@ func TestEnvironmentVariableRequired(t *testing.T) {
var args struct {
Foo string `arg:"env,required"`
}
setenv(t, "FOO", "bar")
os.Args = []string{"example"}
MustParse(&args)
_, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
require.NoError(t, err)
assert.Equal(t, "bar", args.Foo)
}
@ -626,17 +773,26 @@ func TestEnvironmentVariableSliceArgumentString(t *testing.T) {
var args struct {
Foo []string `arg:"env"`
}
setenv(t, "FOO", `bar,"baz, qux"`)
MustParse(&args)
_, err := parseWithEnv(Config{}, "", []string{`FOO=bar,"baz, qux"`}, &args)
require.NoError(t, err)
assert.Equal(t, []string{"bar", "baz, qux"}, args.Foo)
}
func TestEnvironmentVariableSliceEmpty(t *testing.T) {
var args struct {
Foo []string `arg:"env"`
}
_, err := parseWithEnv(Config{}, "", []string{`FOO=`}, &args)
require.NoError(t, err)
assert.Len(t, args.Foo, 0)
}
func TestEnvironmentVariableSliceArgumentInteger(t *testing.T) {
var args struct {
Foo []int `arg:"env"`
}
setenv(t, "FOO", "1,99")
MustParse(&args)
_, err := parseWithEnv(Config{}, "", []string{`FOO=1,99`}, &args)
require.NoError(t, err)
assert.Equal(t, []int{1, 99}, args.Foo)
}
@ -644,8 +800,8 @@ func TestEnvironmentVariableSliceArgumentFloat(t *testing.T) {
var args struct {
Foo []float32 `arg:"env"`
}
setenv(t, "FOO", "1.1,99.9")
MustParse(&args)
_, err := parseWithEnv(Config{}, "", []string{`FOO=1.1,99.9`}, &args)
require.NoError(t, err)
assert.Equal(t, []float32{1.1, 99.9}, args.Foo)
}
@ -653,8 +809,8 @@ func TestEnvironmentVariableSliceArgumentBool(t *testing.T) {
var args struct {
Foo []bool `arg:"env"`
}
setenv(t, "FOO", "true,false,0,1")
MustParse(&args)
_, err := parseWithEnv(Config{}, "", []string{`FOO=true,false,0,1`}, &args)
require.NoError(t, err)
assert.Equal(t, []bool{true, false, false, true}, args.Foo)
}
@ -662,8 +818,7 @@ func TestEnvironmentVariableSliceArgumentWrongCsv(t *testing.T) {
var args struct {
Foo []int `arg:"env"`
}
setenv(t, "FOO", "1,99\"")
err := Parse(&args)
_, err := parseWithEnv(Config{}, "", []string{`FOO=1,99\"`}, &args)
assert.Error(t, err)
}
@ -671,11 +826,40 @@ func TestEnvironmentVariableSliceArgumentWrongType(t *testing.T) {
var args struct {
Foo []bool `arg:"env"`
}
setenv(t, "FOO", "one,two")
err := Parse(&args)
_, err := parseWithEnv(Config{}, "", []string{`FOO=one,two`}, &args)
assert.Error(t, err)
}
func TestEnvironmentVariableMap(t *testing.T) {
var args struct {
Foo map[int]string `arg:"env"`
}
_, err := parseWithEnv(Config{}, "", []string{`FOO=1=one,99=ninetynine`}, &args)
require.NoError(t, err)
assert.Len(t, args.Foo, 2)
assert.Equal(t, "one", args.Foo[1])
assert.Equal(t, "ninetynine", args.Foo[99])
}
func TestEnvironmentVariableEmptyMap(t *testing.T) {
var args struct {
Foo map[int]string `arg:"env"`
}
_, err := parseWithEnv(Config{}, "", []string{`FOO=`}, &args)
require.NoError(t, err)
assert.Len(t, args.Foo, 0)
}
func TestEnvironmentVariableWithPrefix(t *testing.T) {
var args struct {
Foo string `arg:"env"`
}
_, err := parseWithEnv(Config{EnvPrefix: "MYAPP_"}, "", []string{"MYAPP_FOO=bar"}, &args)
require.NoError(t, err)
assert.Equal(t, "bar", args.Foo)
}
func TestEnvironmentVariableIgnored(t *testing.T) {
var args struct {
Foo string `arg:"env"`
@ -690,6 +874,37 @@ func TestEnvironmentVariableIgnored(t *testing.T) {
assert.Equal(t, "", args.Foo)
}
func TestDefaultValuesIgnored(t *testing.T) {
var args struct {
Foo string `default:"bad"`
}
p, err := NewParser(Config{IgnoreDefault: true}, &args)
require.NoError(t, err)
err = p.Parse(nil)
assert.NoError(t, err)
assert.Equal(t, "", args.Foo)
}
func TestRequiredEnvironmentOnlyVariableIsMissing(t *testing.T) {
var args struct {
Foo string `arg:"required,--,env:FOO"`
}
_, err := parseWithEnv(Config{}, "", []string{""}, &args)
assert.Error(t, err)
}
func TestOptionalEnvironmentOnlyVariable(t *testing.T) {
var args struct {
Foo string `arg:"env:FOO"`
}
_, err := parseWithEnv(Config{}, "", []string{}, &args)
assert.NoError(t, err)
}
func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
var args struct {
Sub *struct {
@ -702,10 +917,51 @@ func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
require.NoError(t, err)
err = p.Parse([]string{"sub"})
assert.NoError(t, err)
require.NoError(t, err)
require.NotNil(t, args.Sub)
assert.Equal(t, "", args.Sub.Foo)
}
func TestParserMustParseEmptyArgs(t *testing.T) {
// this mirrors TestEmptyArgs
p, err := NewParser(Config{}, &struct{}{})
require.NoError(t, err)
assert.NotNil(t, p)
p.MustParse(nil)
}
func TestParserMustParse(t *testing.T) {
tests := []struct {
name string
args versioned
cmdLine []string
code int
output string
}{
{name: "help", args: struct{}{}, cmdLine: []string{"--help"}, code: 0, output: "display this help and exit"},
{name: "version", args: versioned{}, cmdLine: []string{"--version"}, code: 0, output: "example 3.2.1"},
{name: "invalid", args: struct{}{}, cmdLine: []string{"invalid"}, code: 2, output: ""},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
var exitCode int
var stdout bytes.Buffer
exit := func(code int) { exitCode = code }
p, err := NewParser(Config{Exit: exit, Out: &stdout}, &tt.args)
require.NoError(t, err)
assert.NotNil(t, p)
p.MustParse(tt.cmdLine)
assert.NotNil(t, exitCode)
assert.Equal(t, tt.code, exitCode)
assert.Contains(t, stdout.String(), tt.output)
})
}
}
type textUnmarshaler struct {
val int
}
@ -844,6 +1100,24 @@ func TestPtrToIP(t *testing.T) {
assert.Equal(t, "192.168.0.1", args.Host.String())
}
func TestURL(t *testing.T) {
var args struct {
URL url.URL
}
err := parse("--url https://example.com/get?item=xyz", &args)
require.NoError(t, err)
assert.Equal(t, "https://example.com/get?item=xyz", args.URL.String())
}
func TestPtrToURL(t *testing.T) {
var args struct {
URL *url.URL
}
err := parse("--url http://example.com/#xyz", &args)
require.NoError(t, err)
assert.Equal(t, "http://example.com/#xyz", args.URL.String())
}
func TestIPSlice(t *testing.T) {
var args struct {
Host []net.IP
@ -978,6 +1252,29 @@ func TestEmbeddedWithDuplicateField2(t *testing.T) {
assert.Equal(t, "", args.U.A)
}
func TestUnexportedEmbedded(t *testing.T) {
type embeddedArgs struct {
Foo string
}
var args struct {
embeddedArgs
}
err := parse("--foo bar", &args)
require.NoError(t, err)
assert.Equal(t, "bar", args.Foo)
}
func TestIgnoredEmbedded(t *testing.T) {
type embeddedArgs struct {
Foo string
}
var args struct {
embeddedArgs `arg:"-"`
}
err := parse("--foo bar", &args)
require.Error(t, err)
}
func TestEmptyArgs(t *testing.T) {
origArgs := os.Args
@ -1118,11 +1415,55 @@ func TestReuseParser(t *testing.T) {
assert.Error(t, err)
}
func TestVersion(t *testing.T) {
func TestNoVersion(t *testing.T) {
var args struct{}
err := parse("--version", &args)
assert.Equal(t, ErrVersion, err)
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
err = p.Parse([]string{"--version"})
assert.Error(t, err)
assert.NotEqual(t, ErrVersion, err)
}
func TestBuiltinVersion(t *testing.T) {
var args struct{}
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
p.version = "example 3.2.1"
err = p.Parse([]string{"--version"})
assert.Equal(t, ErrVersion, err)
}
func TestArgsVersion(t *testing.T) {
var args struct {
Version bool `arg:"--version"`
}
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
err = p.Parse([]string{"--version"})
require.NoError(t, err)
require.Equal(t, args.Version, true)
}
func TestArgsAndBuiltinVersion(t *testing.T) {
var args struct {
Version bool `arg:"--version"`
}
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
p.version = "example 3.2.1"
err = p.Parse([]string{"--version"})
require.NoError(t, err)
require.Equal(t, args.Version, true)
}
func TestMultipleTerminates(t *testing.T) {
@ -1153,13 +1494,21 @@ func TestDefaultOptionValues(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 123, args.A)
assert.Equal(t, 123, *args.B)
if assert.NotNil(t, args.B) {
assert.Equal(t, 123, *args.B)
}
assert.Equal(t, "xyz", args.C)
assert.Equal(t, "abc", *args.D)
if assert.NotNil(t, args.D) {
assert.Equal(t, "abc", *args.D)
}
assert.Equal(t, 4.56, args.E)
assert.Equal(t, 1.23, *args.F)
assert.True(t, args.G)
if assert.NotNil(t, args.F) {
assert.Equal(t, 1.23, *args.F)
}
assert.True(t, args.G)
if assert.NotNil(t, args.H) {
assert.True(t, *args.H)
}
}
func TestDefaultUnparseable(t *testing.T) {
@ -1168,7 +1517,7 @@ func TestDefaultUnparseable(t *testing.T) {
}
err := parse("", &args)
assert.EqualError(t, err, `error processing default value for --a: strconv.ParseInt: parsing "x": invalid syntax`)
assert.EqualError(t, err, `.A: error processing default value: strconv.ParseInt: parsing "x": invalid syntax`)
}
func TestDefaultPositionalValues(t *testing.T) {
@ -1187,13 +1536,21 @@ func TestDefaultPositionalValues(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 456, args.A)
assert.Equal(t, 789, *args.B)
if assert.NotNil(t, args.B) {
assert.Equal(t, 789, *args.B)
}
assert.Equal(t, "abc", args.C)
assert.Equal(t, "abc", *args.D)
if assert.NotNil(t, args.D) {
assert.Equal(t, "abc", *args.D)
}
assert.Equal(t, 1.23, args.E)
assert.Equal(t, 1.23, *args.F)
assert.True(t, args.G)
if assert.NotNil(t, args.F) {
assert.Equal(t, 1.23, *args.F)
}
assert.True(t, args.G)
if assert.NotNil(t, args.H) {
assert.True(t, *args.H)
}
}
func TestDefaultValuesNotAllowedWithRequired(t *testing.T) {
@ -1207,11 +1564,11 @@ func TestDefaultValuesNotAllowedWithRequired(t *testing.T) {
func TestDefaultValuesNotAllowedWithSlice(t *testing.T) {
var args struct {
A []int `default:"123"` // required not allowed with default!
A []int `default:"invalid"` // default values not allowed with slices
}
err := parse("", &args)
assert.EqualError(t, err, ".A: default values are not supported for slice fields")
assert.EqualError(t, err, ".A: default values are not supported for slice or map fields")
}
func TestUnexportedFieldsSkipped(t *testing.T) {
@ -1222,3 +1579,203 @@ func TestUnexportedFieldsSkipped(t *testing.T) {
_, err := NewParser(Config{}, &args)
require.NoError(t, err)
}
func TestMustParseInvalidParser(t *testing.T) {
var exitCode int
var stdout bytes.Buffer
exit := func(code int) { exitCode = code }
var args struct {
CannotParse struct{}
}
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
assert.Nil(t, parser)
assert.Equal(t, 2, exitCode)
}
func TestMustParsePrintsHelp(t *testing.T) {
originalArgs := os.Args
defer func() {
os.Args = originalArgs
}()
os.Args = []string{"someprogram", "--help"}
var exitCode int
var stdout bytes.Buffer
exit := func(code int) { exitCode = code }
var args struct{}
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
assert.NotNil(t, parser)
assert.Equal(t, 0, exitCode)
}
func TestMustParsePrintsVersion(t *testing.T) {
originalArgs := os.Args
defer func() {
os.Args = originalArgs
}()
var exitCode int
var stdout bytes.Buffer
exit := func(code int) { exitCode = code }
os.Args = []string{"someprogram", "--version"}
var args versioned
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
require.NotNil(t, parser)
assert.Equal(t, 0, exitCode)
assert.Equal(t, "example 3.2.1\n", stdout.String())
}
type mapWithUnmarshalText struct {
val map[string]string
}
func (v *mapWithUnmarshalText) UnmarshalText(data []byte) error {
return json.Unmarshal(data, &v.val)
}
func TestTextUnmarshalerEmpty(t *testing.T) {
// based on https://github.com/alexflint/go-arg/issues/184
var args struct {
Config mapWithUnmarshalText `arg:"--config"`
}
err := parse("", &args)
require.NoError(t, err)
assert.Empty(t, args.Config)
}
func TestTextUnmarshalerEmptyPointer(t *testing.T) {
// a slight variant on https://github.com/alexflint/go-arg/issues/184
var args struct {
Config *mapWithUnmarshalText `arg:"--config"`
}
err := parse("", &args)
require.NoError(t, err)
assert.Nil(t, args.Config)
}
// similar to the above but also implements MarshalText
type mapWithMarshalText struct {
val map[string]string
}
func (v *mapWithMarshalText) MarshalText(data []byte) error {
return json.Unmarshal(data, &v.val)
}
func (v *mapWithMarshalText) UnmarshalText(data []byte) error {
return json.Unmarshal(data, &v.val)
}
func TestTextMarshalerUnmarshalerEmpty(t *testing.T) {
// based on https://github.com/alexflint/go-arg/issues/184
var args struct {
Config mapWithMarshalText `arg:"--config"`
}
err := parse("", &args)
require.NoError(t, err)
assert.Empty(t, args.Config)
}
func TestTextMarshalerUnmarshalerEmptyPointer(t *testing.T) {
// a slight variant on https://github.com/alexflint/go-arg/issues/184
var args struct {
Config *mapWithMarshalText `arg:"--config"`
}
err := parse("", &args)
require.NoError(t, err)
assert.Nil(t, args.Config)
}
func TestSubcommandGlobalFlag_Before(t *testing.T) {
var args struct {
Global bool `arg:"-g"`
Sub *struct {
} `arg:"subcommand"`
}
p, err := NewParser(Config{StrictSubcommands: false}, &args)
require.NoError(t, err)
err = p.Parse([]string{"-g", "sub"})
assert.NoError(t, err)
assert.True(t, args.Global)
}
func TestSubcommandGlobalFlag_InCommand(t *testing.T) {
var args struct {
Global bool `arg:"-g"`
Sub *struct {
} `arg:"subcommand"`
}
p, err := NewParser(Config{StrictSubcommands: false}, &args)
require.NoError(t, err)
err = p.Parse([]string{"sub", "-g"})
assert.NoError(t, err)
assert.True(t, args.Global)
}
func TestSubcommandGlobalFlag_Before_Strict(t *testing.T) {
var args struct {
Global bool `arg:"-g"`
Sub *struct {
} `arg:"subcommand"`
}
p, err := NewParser(Config{StrictSubcommands: true}, &args)
require.NoError(t, err)
err = p.Parse([]string{"-g", "sub"})
assert.NoError(t, err)
assert.True(t, args.Global)
}
func TestSubcommandGlobalFlag_InCommand_Strict(t *testing.T) {
var args struct {
Global bool `arg:"-g"`
Sub *struct {
} `arg:"subcommand"`
}
p, err := NewParser(Config{StrictSubcommands: true}, &args)
require.NoError(t, err)
err = p.Parse([]string{"sub", "-g"})
assert.Error(t, err)
}
func TestSubcommandGlobalFlag_InCommand_Strict_Inner(t *testing.T) {
var args struct {
Global bool `arg:"-g"`
Sub *struct {
Guard bool `arg:"-g"`
} `arg:"subcommand"`
}
p, err := NewParser(Config{StrictSubcommands: true}, &args)
require.NoError(t, err)
err = p.Parse([]string{"sub", "-g"})
require.NoError(t, err)
assert.False(t, args.Global)
require.NotNil(t, args.Sub)
assert.True(t, args.Sub.Guard)
}
func TestExitFunctionAndOutStreamGetFilledIn(t *testing.T) {
var args struct{}
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
assert.NotNil(t, p.config.Exit) // go prohibits function pointer comparison
assert.Equal(t, p.config.Out, os.Stdout)
}

112
test/reflect_test.go Normal file
View File

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

152
test/sequence_test.go Normal file
View File

@ -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(&notSliceOrMap).Elem()
err = setSliceOrMap(dest, nil, false)
assert.Error(t, err)
// check what happens when we pass in a pointer to something that is not a slice or a map
var stringPtr *string
dest = reflect.ValueOf(&stringPtr).Elem()
err = setSliceOrMap(dest, nil, false)
assert.Error(t, err)
}

View File

@ -1,6 +1,7 @@
package arg
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
@ -48,6 +49,17 @@ func TestMinimalSubcommand(t *testing.T) {
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 {
}
@ -71,6 +83,19 @@ func TestNamedSubcommand(t *testing.T) {
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
}
func TestSubcommandAliases(t *testing.T) {
type listCmd struct {
}
var args struct {
List *listCmd `arg:"subcommand:list|ls"`
}
p, err := pparse("ls", &args)
require.NoError(t, err)
assert.NotNil(t, args.List)
assert.Equal(t, args.List, p.Subcommand())
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
}
func TestEmptySubcommand(t *testing.T) {
type listCmd struct {
}
@ -101,6 +126,23 @@ func TestTwoSubcommands(t *testing.T) {
assert.Equal(t, []string{"list"}, p.SubcommandNames())
}
func TestTwoSubcommandsWithAliases(t *testing.T) {
type getCmd struct {
}
type listCmd struct {
}
var args struct {
Get *getCmd `arg:"subcommand:get|g"`
List *listCmd `arg:"subcommand:list|ls"`
}
p, err := pparse("ls", &args)
require.NoError(t, err)
assert.Nil(t, args.Get)
assert.NotNil(t, args.List)
assert.Equal(t, args.List, p.Subcommand())
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
}
func TestSubcommandsWithOptions(t *testing.T) {
type getCmd struct {
Name string
@ -179,6 +221,36 @@ func TestSubcommandsWithOptions(t *testing.T) {
}
}
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 {
@ -233,6 +305,60 @@ func TestNestedSubcommands(t *testing.T) {
}
}
func TestNestedSubcommandsWithAliases(t *testing.T) {
type child struct{}
type parent struct {
Child *child `arg:"subcommand:child|ch"`
}
type grandparent struct {
Parent *parent `arg:"subcommand:parent|pa"`
}
type root struct {
Grandparent *grandparent `arg:"subcommand:grandparent|gp"`
}
{
var args root
p, err := pparse("gp parent child", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.NotNil(t, args.Grandparent.Parent)
require.NotNil(t, args.Grandparent.Parent.Child)
assert.Equal(t, args.Grandparent.Parent.Child, p.Subcommand())
assert.Equal(t, []string{"gp", "parent", "child"}, p.SubcommandNames())
}
{
var args root
p, err := pparse("grandparent pa", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.NotNil(t, args.Grandparent.Parent)
require.Nil(t, args.Grandparent.Parent.Child)
assert.Equal(t, args.Grandparent.Parent, p.Subcommand())
assert.Equal(t, []string{"grandparent", "pa"}, p.SubcommandNames())
}
{
var args root
p, err := pparse("grandparent", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.Nil(t, args.Grandparent.Parent)
assert.Equal(t, args.Grandparent, p.Subcommand())
assert.Equal(t, []string{"grandparent"}, p.SubcommandNames())
}
{
var args root
p, err := pparse("", &args)
require.NoError(t, err)
require.Nil(t, args.Grandparent)
assert.Nil(t, p.Subcommand())
assert.Empty(t, p.SubcommandNames())
}
}
func TestSubcommandsWithPositionals(t *testing.T) {
type listCmd struct {
Pattern string `arg:"positional"`
@ -353,3 +479,30 @@ func TestSubcommandsWithMultiplePositionals(t *testing.T) {
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)
}

1086
test/usage_test.go Normal file

File diff suppressed because it is too large Load Diff

325
usage.go
View File

@ -3,68 +3,82 @@ package arg
import (
"fmt"
"io"
"os"
"strings"
)
// the width of the left column
const colWidth = 25
// to allow monkey patching in tests
var stderr = os.Stderr
// 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.failWithCommand(msg, p.cmd)
p.FailSubcommand(msg)
}
// failWithCommand prints usage information for the given subcommand to stderr and exits with non-zero status
func (p *Parser) failWithCommand(msg string, cmd *command) {
p.writeUsageForCommand(stderr, cmd)
fmt.Fprintln(stderr, "error:", msg)
osExit(-1)
// 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) {
cmd := p.cmd
if p.lastCmd != nil {
cmd = p.lastCmd
}
p.writeUsageForCommand(w, cmd)
p.WriteUsageForSubcommand(w, p.subcommand...)
}
// writeUsageForCommand writes usage information for the given subcommand
func (p *Parser) writeUsageForCommand(w io.Writer, cmd *command) {
var positionals, options []*spec
// 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 {
if spec.positional {
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)
}
}
if p.version != "" {
fmt.Fprintln(w, p.version)
}
// make a list of ancestor commands so that we print with full context
var ancestors []string
ancestor := cmd
for ancestor != nil {
ancestors = append(ancestors, ancestor.name)
ancestor = ancestor.parent
}
// print the beginning of the usage string
fmt.Fprint(w, "Usage:")
for i := len(ancestors) - 1; i >= 0; i-- {
fmt.Fprint(w, " "+ancestors[i])
fmt.Fprintf(w, "Usage: %s", p.cmd.name)
for _, s := range subcommand {
fmt.Fprint(w, " "+s)
}
// write the option component of the usage message
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 {
@ -76,22 +90,32 @@ func (p *Parser) writeUsageForCommand(w io.Writer, cmd *command) {
}
}
// 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, " ")
if spec.multiple {
if !spec.required {
fmt.Fprint(w, "[")
}
if !spec.required {
fmt.Fprint(w, "[")
closeBrackets += 1
}
if spec.cardinality == multiple {
fmt.Fprintf(w, "%s [%s ...]", spec.placeholder, spec.placeholder)
if !spec.required {
fmt.Fprint(w, "]")
}
} else {
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 {
@ -99,87 +123,139 @@ func (p *Parser) writeUsageForCommand(w io.Writer, cmd *command) {
}
fmt.Fprint(w, "\n")
return nil
}
func printTwoCols(w io.Writer, left, help string, defaultVal string, envVal string) {
lhs := " " + left
// print prints a line like this:
//
// --option FOO A description of the option [default: 123]
//
// If the text on the left is longer than a certain threshold, the description is moved to the next line:
//
// --verylongoptionoption VERY_LONG_VARIABLE
// A description of the option [default: 123]
//
// If multiple "extras" are provided then they are put inside a single set of square brackets:
//
// --option FOO A description of the option [default: 123, env: FOO]
func print(w io.Writer, item, description string, bracketed ...string) {
lhs := " " + item
fmt.Fprint(w, lhs)
if help != "" {
if description != "" {
if len(lhs)+2 < colWidth {
fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs)))
} else {
fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
}
fmt.Fprint(w, help)
fmt.Fprint(w, description)
}
bracketsContent := []string{}
if defaultVal != "" {
bracketsContent = append(bracketsContent,
fmt.Sprintf("default: %s", defaultVal),
)
var brack string
for _, s := range bracketed {
if s != "" {
if brack != "" {
brack += ", "
}
brack += s
}
}
if envVal != "" {
bracketsContent = append(bracketsContent,
fmt.Sprintf("env: %s", envVal),
)
}
if len(bracketsContent) > 0 {
fmt.Fprintf(w, " [%s]", strings.Join(bracketsContent, ", "))
if brack != "" {
fmt.Fprintf(w, " [%s]", brack)
}
fmt.Fprint(w, "\n")
}
// WriteHelp writes the usage string followed by the full help string for each option
func (p *Parser) WriteHelp(w io.Writer) {
cmd := p.cmd
if p.lastCmd != nil {
cmd = p.lastCmd
func withDefault(s string) string {
if s == "" {
return ""
}
p.writeHelpForCommand(w, cmd)
return "default: " + s
}
// writeHelp writes the usage string for the given subcommand
func (p *Parser) writeHelpForCommand(w io.Writer, cmd *command) {
var positionals, options []*spec
func withEnv(env string) string {
if env == "" {
return ""
}
return "env: " + env
}
// WriteHelp writes the usage string followed by the full help string for each option
func (p *Parser) WriteHelp(w io.Writer) {
p.WriteHelpForSubcommand(w, p.subcommand...)
}
// WriteHelpForSubcommand writes the usage string followed by the full help
// string for a specified subcommand. To write help for a top-level subcommand,
// provide just the name of that subcommand. To write help for a subcommand that
// is nested under another subcommand, provide a sequence of subcommand names
// starting with the top-level subcommand and so on down the tree.
func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error {
cmd, err := p.lookupCommand(subcommand...)
if err != nil {
return err
}
var positionals, longOptions, shortOptions, envOnlyOptions []*spec
var hasVersionOption bool
for _, spec := range cmd.specs {
if spec.positional {
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)
}
}
// obtain a flattened list of options from all ancestors
// also determine if any ancestor has a version option spec
var globals []*spec
ancestor := cmd.parent
for ancestor != nil {
for _, spec := range ancestor.specs {
if spec.long == "version" {
hasVersionOption = true
break
}
}
globals = append(globals, ancestor.specs...)
ancestor = ancestor.parent
}
if p.description != "" {
fmt.Fprintln(w, p.description)
}
p.writeUsageForCommand(w, cmd)
if !hasVersionOption && p.version != "" {
fmt.Fprintln(w, p.version)
}
p.WriteUsageForSubcommand(w, subcommand...)
// write the list of positionals
if len(positionals) > 0 {
fmt.Fprint(w, "\nPositional arguments:\n")
for _, spec := range positionals {
printTwoCols(w, spec.placeholder, spec.help, "", "")
print(w, spec.placeholder, spec.help, withDefault(spec.defaultString), withEnv(spec.env))
}
}
// write the list of options
if len(options) > 0 || cmd.parent == nil {
// 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 options {
for _, spec := range shortOptions {
p.printOption(w, spec)
}
for _, spec := range longOptions {
p.printOption(w, spec)
}
}
// obtain a flattened list of options from all ancestors
var globals []*spec
ancestor := cmd.parent
for ancestor != nil {
globals = append(globals, ancestor.specs...)
ancestor = ancestor.parent
}
// write the list of global options
@ -192,43 +268,76 @@ func (p *Parser) writeHelpForCommand(w io.Writer, cmd *command) {
// write the list of built in options
p.printOption(w, &spec{
boolean: true,
long: "help",
short: "h",
help: "display this help and exit",
cardinality: zero,
long: "help",
short: "h",
help: "display this help and exit",
})
if p.version != "" {
if !hasVersionOption && p.version != "" {
p.printOption(w, &spec{
boolean: true,
long: "version",
help: "display version and exit",
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 {
printTwoCols(w, subcmd.name, subcmd.help, "", "")
names := append([]string{subcmd.name}, subcmd.aliases...)
print(w, strings.Join(names, ", "), subcmd.help)
}
}
if p.epilogue != "" {
fmt.Fprintln(w, "\n"+p.epilogue)
}
return nil
}
func (p *Parser) printOption(w io.Writer, spec *spec) {
left := synopsis(spec, "--"+spec.long)
if spec.short != "" {
left += ", " + synopsis(spec, "-"+spec.short)
ways := make([]string, 0, 2)
if spec.long != "" {
ways = append(ways, synopsis(spec, "--"+spec.long))
}
printTwoCols(w, left, spec.help, spec.defaultVal, spec.env)
if spec.short != "" {
ways = append(ways, synopsis(spec, "-"+spec.short))
}
if len(ways) > 0 {
print(w, strings.Join(ways, ", "), spec.help, withDefault(spec.defaultString), withEnv(spec.env))
}
}
func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) {
ways := make([]string, 0, 2)
if spec.required {
ways = append(ways, "Required.")
} else {
ways = append(ways, "Optional.")
}
if spec.help != "" {
ways = append(ways, spec.help)
}
print(w, spec.env, strings.Join(ways, " "), withDefault(spec.defaultString))
}
func synopsis(spec *spec, form string) string {
if spec.boolean {
// if the user omits the placeholder tag then we pick one automatically,
// but if the user explicitly specifies an empty placeholder then we
// leave out the placeholder in the help message
if spec.cardinality == zero || spec.placeholder == "" {
return form
}
return form + " " + spec.placeholder
}
func ptrTo(s string) *string {
return &s
}

View File

@ -1,311 +0,0 @@
package arg
import (
"bytes"
"errors"
"fmt"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type NameDotName struct {
Head, Tail string
}
func (n *NameDotName) UnmarshalText(b []byte) error {
s := string(b)
pos := strings.Index(s, ".")
if pos == -1 {
return fmt.Errorf("missing period in %s", s)
}
n.Head = s[:pos]
n.Tail = s[pos+1:]
return nil
}
func (n *NameDotName) MarshalText() (text []byte, err error) {
text = []byte(fmt.Sprintf("%s.%s", n.Head, n.Tail))
return
}
func TestWriteUsage(t *testing.T) {
expectedUsage := "Usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] [--testenv TESTENV] [--file FILE] INPUT [OUTPUT [OUTPUT ...]]\n"
expectedHelp := `Usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] [--testenv TESTENV] [--file FILE] INPUT [OUTPUT [OUTPUT ...]]
Positional arguments:
INPUT
OUTPUT list of outputs
Options:
--name NAME name to use [default: Foo Bar]
--value VALUE secret value [default: 42]
--verbose, -v verbosity level
--dataset DATASET dataset to use
--optimize OPTIMIZE, -O OPTIMIZE
optimization level
--ids IDS Ids
--values VALUES Values [default: [3.14 42 256]]
--workers WORKERS, -w WORKERS
number of workers to start [default: 10, env: WORKERS]
--testenv TESTENV, -a TESTENV [env: TEST_ENV]
--file FILE, -f FILE File with mandatory extension [default: scratch.txt]
--help, -h display this help and exit
`
var args struct {
Input string `arg:"positional"`
Output []string `arg:"positional" help:"list of outputs"`
Name string `help:"name to use"`
Value int `help:"secret value"`
Verbose bool `arg:"-v" help:"verbosity level"`
Dataset string `help:"dataset to use"`
Optimize int `arg:"-O" help:"optimization level"`
Ids []int64 `help:"Ids"`
Values []float64 `help:"Values"`
Workers int `arg:"-w,env:WORKERS" help:"number of workers to start" default:"10"`
TestEnv string `arg:"-a,env:TEST_ENV"`
File *NameDotName `arg:"-f" help:"File with mandatory extension"`
}
args.Name = "Foo Bar"
args.Value = 42
args.Values = []float64{3.14, 42, 256}
args.File = &NameDotName{"scratch", "txt"}
p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err)
os.Args[0] = "example"
var 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())
}
type MyEnum int
func (n *MyEnum) UnmarshalText(b []byte) error {
return nil
}
func (n *MyEnum) MarshalText() ([]byte, error) {
return nil, errors.New("There was a problem")
}
func TestUsageWithDefaults(t *testing.T) {
expectedHelp := `Usage: example [--label LABEL] [--content CONTENT]
Options:
--label LABEL [default: cat]
--content CONTENT [default: dog]
--help, -h display this help and exit
`
var args struct {
Label string
Content string `default:"dog"`
}
args.Label = "cat"
p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err)
args.Label = "should_ignore_this"
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp, help.String())
}
func TestUsageCannotMarshalToString(t *testing.T) {
var args struct {
Name *MyEnum
}
v := MyEnum(42)
args.Name = &v
_, err := NewParser(Config{Program: "example"}, &args)
assert.EqualError(t, err, `args.Name: error marshaling default value to string: There was a problem`)
}
func TestUsageLongPositionalWithHelp_legacyForm(t *testing.T) {
expectedHelp := `Usage: example VERYLONGPOSITIONALWITHHELP
Positional arguments:
VERYLONGPOSITIONALWITHHELP
this positional argument is very long but cannot include commas
Options:
--help, -h display this help and exit
`
var args struct {
VeryLongPositionalWithHelp string `arg:"positional,help:this positional argument is very long but cannot include commas"`
}
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
os.Args[0] = "example"
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp, help.String())
}
func TestUsageLongPositionalWithHelp_newForm(t *testing.T) {
expectedHelp := `Usage: example VERYLONGPOSITIONALWITHHELP
Positional arguments:
VERYLONGPOSITIONALWITHHELP
this positional argument is very long, and includes: commas, colons etc
Options:
--help, -h display this help and exit
`
var args struct {
VeryLongPositionalWithHelp string `arg:"positional" help:"this positional argument is very long, and includes: commas, colons etc"`
}
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
os.Args[0] = "example"
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp, help.String())
}
func TestUsageWithProgramName(t *testing.T) {
expectedHelp := `Usage: myprogram
Options:
--help, -h display this help and exit
`
config := Config{
Program: "myprogram",
}
p, err := NewParser(config, &struct{}{})
require.NoError(t, err)
os.Args[0] = "example"
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp, help.String())
}
type versioned struct{}
// Version returns the version for this program
func (versioned) Version() string {
return "example 3.2.1"
}
func TestUsageWithVersion(t *testing.T) {
expectedHelp := `example 3.2.1
Usage: example
Options:
--help, -h display this help and exit
--version display version and exit
`
os.Args[0] = "example"
p, err := NewParser(Config{}, &versioned{})
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
actual := help.String()
if expectedHelp != actual {
t.Logf("Expected:\n%s", expectedHelp)
t.Logf("Actual:\n%s", actual)
t.Fail()
}
}
type described struct{}
// Described returns the description for this program
func (described) Description() string {
return "this program does this and that"
}
func TestUsageWithDescription(t *testing.T) {
expectedHelp := `this program does this and that
Usage: example
Options:
--help, -h display this help and exit
`
os.Args[0] = "example"
p, err := NewParser(Config{}, &described{})
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
actual := help.String()
if expectedHelp != actual {
t.Logf("Expected:\n%s", expectedHelp)
t.Logf("Actual:\n%s", actual)
t.Fail()
}
}
func TestRequiredMultiplePositionals(t *testing.T) {
expectedHelp := `Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]
Positional arguments:
REQUIREDMULTIPLE required multiple positional
Options:
--help, -h display this help and exit
`
var args struct {
RequiredMultiple []string `arg:"positional,required" help:"required multiple positional"`
}
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp, help.String())
}
func TestUsageWithNestedSubcommands(t *testing.T) {
expectedHelp := `Usage: example child nested [--enable] OUTPUT
Positional arguments:
OUTPUT
Options:
--enable
Global options:
--values VALUES Values
--verbose, -v verbosity level
--help, -h display this help and exit
`
var args struct {
Verbose bool `arg:"-v" help:"verbosity level"`
Child *struct {
Values []float64 `help:"Values"`
Nested *struct {
Enable bool
Output string `arg:"positional,required"`
} `arg:"subcommand:nested"`
} `arg:"subcommand:child"`
}
os.Args[0] = "example"
p, err := NewParser(Config{}, &args)
require.NoError(t, err)
_ = p.Parse([]string{"child", "nested", "value"})
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp, help.String())
}