├── .github
├── FUNDING.yml
├── banner.jpg
└── workflows
│ └── go.yml
├── .gitignore
├── LICENSE
├── README.md
├── doc.go
├── example_test.go
├── go.mod
├── go.sum
├── parse.go
├── parse_test.go
├── reflect.go
├── reflect_test.go
├── sequence.go
├── sequence_test.go
├── subcommand.go
├── subcommand_test.go
├── usage.go
└── usage_test.go
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [alexflint]
2 |
--------------------------------------------------------------------------------
/.github/banner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexflint/go-arg/a36ed1e7b3fe64227244048a90289a5d365bf2f1/.github/banner.jpg
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 |
11 | build_and_test:
12 | name: Build and test
13 | runs-on: ubuntu-latest
14 |
15 | strategy:
16 | fail-fast: false
17 | matrix:
18 | go: ['1.20', '1.21', '1.22']
19 |
20 | steps:
21 | - id: go
22 | name: Set up Go
23 | uses: actions/setup-go@v5
24 | with:
25 | go-version: ${{ matrix.go }}
26 |
27 | - name: Checkout
28 | uses: actions/checkout@v4
29 |
30 | - name: Build
31 | run: go build -v .
32 |
33 | - name: Test
34 | run: go test -v -coverprofile=profile.cov .
35 |
36 | - name: Send coverage
37 | run: bash <(curl -s https://codecov.io/bash) -f profile.cov
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 |
10 | # Architecture specific extensions/prefixes
11 | *.[568vq]
12 | [568vq].out
13 |
14 | *.cgo1.go
15 | *.cgo2.c
16 | _cgo_defun.c
17 | _cgo_gotypes.go
18 | _cgo_export.*
19 |
20 | _testmain.go
21 |
22 | *.exe
23 | *.test
24 | *.prof
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, Alex Flint
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 |
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | go-arg
5 |
6 |
7 | Struct-based argument parsing for Go
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Declare command line arguments for your program by defining a struct.
18 |
19 | ```go
20 | var args struct {
21 | Foo string
22 | Bar bool
23 | }
24 | arg.MustParse(&args)
25 | fmt.Println(args.Foo, args.Bar)
26 | ```
27 |
28 | ```shell
29 | $ ./example --foo=hello --bar
30 | hello true
31 | ```
32 |
33 | ### Installation
34 |
35 | ```shell
36 | go get github.com/alexflint/go-arg
37 | ```
38 |
39 | ### Required arguments
40 |
41 | ```go
42 | var args struct {
43 | ID int `arg:"required"`
44 | Timeout time.Duration
45 | }
46 | arg.MustParse(&args)
47 | ```
48 |
49 | ```shell
50 | $ ./example
51 | Usage: example --id ID [--timeout TIMEOUT]
52 | error: --id is required
53 | ```
54 |
55 | ### Positional arguments
56 |
57 | ```go
58 | var args struct {
59 | Input string `arg:"positional"`
60 | Output []string `arg:"positional"`
61 | }
62 | arg.MustParse(&args)
63 | fmt.Println("Input:", args.Input)
64 | fmt.Println("Output:", args.Output)
65 | ```
66 |
67 | ```shell
68 | $ ./example src.txt x.out y.out z.out
69 | Input: src.txt
70 | Output: [x.out y.out z.out]
71 | ```
72 |
73 | ### Environment variables
74 |
75 | ```go
76 | var args struct {
77 | Workers int `arg:"env"`
78 | }
79 | arg.MustParse(&args)
80 | fmt.Println("Workers:", args.Workers)
81 | ```
82 |
83 | ```shell
84 | $ WORKERS=4 ./example
85 | Workers: 4
86 | ```
87 |
88 | ```shell
89 | $ WORKERS=4 ./example --workers=6
90 | Workers: 6
91 | ```
92 |
93 | You can also override the name of the environment variable:
94 |
95 | ```go
96 | var args struct {
97 | Workers int `arg:"env:NUM_WORKERS"`
98 | }
99 | arg.MustParse(&args)
100 | fmt.Println("Workers:", args.Workers)
101 | ```
102 |
103 | ```shell
104 | $ NUM_WORKERS=4 ./example
105 | Workers: 4
106 | ```
107 |
108 | You can provide multiple values in environment variables using commas:
109 |
110 | ```go
111 | var args struct {
112 | Workers []int `arg:"env"`
113 | }
114 | arg.MustParse(&args)
115 | fmt.Println("Workers:", args.Workers)
116 | ```
117 |
118 | ```shell
119 | $ WORKERS='1,99' ./example
120 | Workers: [1 99]
121 | ```
122 |
123 | Command line arguments take precedence over environment variables:
124 |
125 | ```go
126 | var args struct {
127 | Workers int `arg:"--count,env:NUM_WORKERS"`
128 | }
129 | arg.MustParse(&args)
130 | fmt.Println("Workers:", args.Workers)
131 | ```
132 |
133 | ```shell
134 | $ NUM_WORKERS=6 ./example
135 | Workers: 6
136 | $ NUM_WORKERS=6 ./example --count 4
137 | Workers: 4
138 | ```
139 |
140 | Configuring a global environment variable name prefix is also possible:
141 |
142 | ```go
143 | var args struct {
144 | Workers int `arg:"--count,env:NUM_WORKERS"`
145 | }
146 |
147 | p, err := arg.NewParser(arg.Config{
148 | EnvPrefix: "MYAPP_",
149 | }, &args)
150 |
151 | p.MustParse(os.Args[1:])
152 | fmt.Println("Workers:", args.Workers)
153 | ```
154 |
155 | ```shell
156 | $ MYAPP_NUM_WORKERS=6 ./example
157 | Workers: 6
158 | ```
159 |
160 | ### Usage strings
161 |
162 | ```go
163 | var args struct {
164 | Input string `arg:"positional"`
165 | Output []string `arg:"positional"`
166 | Verbose bool `arg:"-v,--verbose" help:"verbosity level"`
167 | Dataset string `help:"dataset to use"`
168 | Optimize int `arg:"-O" help:"optimization level"`
169 | }
170 | arg.MustParse(&args)
171 | ```
172 |
173 | ```shell
174 | $ ./example -h
175 | Usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]]
176 |
177 | Positional arguments:
178 | INPUT
179 | OUTPUT
180 |
181 | Options:
182 | --verbose, -v verbosity level
183 | --dataset DATASET dataset to use
184 | --optimize OPTIMIZE, -O OPTIMIZE
185 | optimization level
186 | --help, -h print this help message
187 | ```
188 |
189 | ### Default values
190 |
191 | ```go
192 | var args struct {
193 | Foo string `default:"abc"`
194 | Bar bool
195 | }
196 | arg.MustParse(&args)
197 | ```
198 |
199 | 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.
200 |
201 | ```go
202 | var args struct {
203 | Test string `arg:"-t,env:TEST" default:"something"`
204 | }
205 | arg.MustParse(&args)
206 | ```
207 |
208 | #### Ignoring environment variables and/or default values
209 |
210 | ```go
211 | var args struct {
212 | Test string `arg:"-t,env:TEST" default:"something"`
213 | }
214 |
215 | p, err := arg.NewParser(arg.Config{
216 | IgnoreEnv: true,
217 | IgnoreDefault: true,
218 | }, &args)
219 |
220 | err = p.Parse(os.Args[1:])
221 | ```
222 |
223 | ### Arguments with multiple values
224 |
225 | ```go
226 | var args struct {
227 | Database string
228 | IDs []int64
229 | }
230 | arg.MustParse(&args)
231 | fmt.Printf("Fetching the following IDs from %s: %q", args.Database, args.IDs)
232 | ```
233 |
234 | ```shell
235 | ./example -database foo -ids 1 2 3
236 | Fetching the following IDs from foo: [1 2 3]
237 | ```
238 |
239 | ### Arguments that can be specified multiple times, mixed with positionals
240 |
241 | ```go
242 | var args struct {
243 | Commands []string `arg:"-c,separate"`
244 | Files []string `arg:"-f,separate"`
245 | Databases []string `arg:"positional"`
246 | }
247 | arg.MustParse(&args)
248 | ```
249 |
250 | ```shell
251 | ./example -c cmd1 db1 -f file1 db2 -c cmd2 -f file2 -f file3 db3 -c cmd3
252 | Commands: [cmd1 cmd2 cmd3]
253 | Files [file1 file2 file3]
254 | Databases [db1 db2 db3]
255 | ```
256 |
257 | ### Arguments with keys and values
258 |
259 | ```go
260 | var args struct {
261 | UserIDs map[string]int
262 | }
263 | arg.MustParse(&args)
264 | fmt.Println(args.UserIDs)
265 | ```
266 |
267 | ```shell
268 | ./example --userids john=123 mary=456
269 | map[john:123 mary:456]
270 | ```
271 |
272 | ### Version strings
273 |
274 | ```go
275 | type args struct {
276 | ...
277 | }
278 |
279 | func (args) Version() string {
280 | return "someprogram 4.3.0"
281 | }
282 |
283 | func main() {
284 | var args args
285 | arg.MustParse(&args)
286 | }
287 | ```
288 |
289 | ```shell
290 | $ ./example --version
291 | someprogram 4.3.0
292 | ```
293 |
294 | > **Note**
295 | > If a `--version` flag is defined in `args` or any subcommand, it overrides the built-in versioning.
296 |
297 | ### Custom validation
298 |
299 | ```go
300 | var args struct {
301 | Foo string
302 | Bar string
303 | }
304 | p := arg.MustParse(&args)
305 | if args.Foo == "" && args.Bar == "" {
306 | p.Fail("you must provide either --foo or --bar")
307 | }
308 | ```
309 |
310 | ```shell
311 | ./example
312 | Usage: samples [--foo FOO] [--bar BAR]
313 | error: you must provide either --foo or --bar
314 | ```
315 |
316 | ### Overriding option names
317 |
318 | ```go
319 | var args struct {
320 | Short string `arg:"-s"`
321 | Long string `arg:"--custom-long-option"`
322 | ShortAndLong string `arg:"-x,--my-option"`
323 | OnlyShort string `arg:"-o,--"`
324 | }
325 | arg.MustParse(&args)
326 | ```
327 |
328 | ```shell
329 | $ ./example --help
330 | Usage: example [-o ONLYSHORT] [--short SHORT] [--custom-long-option CUSTOM-LONG-OPTION] [--my-option MY-OPTION]
331 |
332 | Options:
333 | --short SHORT, -s SHORT
334 | --custom-long-option CUSTOM-LONG-OPTION
335 | --my-option MY-OPTION, -x MY-OPTION
336 | -o ONLYSHORT
337 | --help, -h display this help and exit
338 | ```
339 |
340 | ### Embedded structs
341 |
342 | The fields of embedded structs are treated just like regular fields:
343 |
344 | ```go
345 | type DatabaseOptions struct {
346 | Host string
347 | Username string
348 | Password string
349 | }
350 |
351 | type LogOptions struct {
352 | LogFile string
353 | Verbose bool
354 | }
355 |
356 | func main() {
357 | var args struct {
358 | DatabaseOptions
359 | LogOptions
360 | }
361 | arg.MustParse(&args)
362 | }
363 | ```
364 |
365 | As usual, any field tagged with `arg:"-"` is ignored.
366 |
367 | ### Supported types
368 |
369 | The following types may be used as arguments:
370 | - built-in integer types: `int, int8, int16, int32, int64, byte, rune`
371 | - built-in floating point types: `float32, float64`
372 | - strings
373 | - booleans
374 | - URLs represented as `url.URL`
375 | - time durations represented as `time.Duration`
376 | - email addresses represented as `mail.Address`
377 | - MAC addresses represented as `net.HardwareAddr`
378 | - pointers to any of the above
379 | - slices of any of the above
380 | - maps using any of the above as keys and values
381 | - any type that implements `encoding.TextUnmarshaler`
382 |
383 | ### Custom parsing
384 |
385 | Implement `encoding.TextUnmarshaler` to define your own parsing logic.
386 |
387 | ```go
388 | // Accepts command line arguments of the form "head.tail"
389 | type NameDotName struct {
390 | Head, Tail string
391 | }
392 |
393 | func (n *NameDotName) UnmarshalText(b []byte) error {
394 | s := string(b)
395 | pos := strings.Index(s, ".")
396 | if pos == -1 {
397 | return fmt.Errorf("missing period in %s", s)
398 | }
399 | n.Head = s[:pos]
400 | n.Tail = s[pos+1:]
401 | return nil
402 | }
403 |
404 | func main() {
405 | var args struct {
406 | Name NameDotName
407 | }
408 | arg.MustParse(&args)
409 | fmt.Printf("%#v\n", args.Name)
410 | }
411 | ```
412 |
413 | ```shell
414 | $ ./example --name=foo.bar
415 | main.NameDotName{Head:"foo", Tail:"bar"}
416 |
417 | $ ./example --name=oops
418 | Usage: example [--name NAME]
419 | error: error processing --name: missing period in "oops"
420 | ```
421 |
422 | ### Custom parsing with default values
423 |
424 | Implement `encoding.TextMarshaler` to define your own default value strings:
425 |
426 | ```go
427 | // Accepts command line arguments of the form "head.tail"
428 | type NameDotName struct {
429 | Head, Tail string
430 | }
431 |
432 | func (n *NameDotName) UnmarshalText(b []byte) error {
433 | // same as previous example
434 | }
435 |
436 | // this is only needed if you want to display a default value in the usage string
437 | func (n *NameDotName) MarshalText() ([]byte, error) {
438 | return []byte(fmt.Sprintf("%s.%s", n.Head, n.Tail)), nil
439 | }
440 |
441 | func main() {
442 | var args struct {
443 | Name NameDotName `default:"file.txt"`
444 | }
445 | arg.MustParse(&args)
446 | fmt.Printf("%#v\n", args.Name)
447 | }
448 | ```
449 |
450 | ```shell
451 | $ ./example --help
452 | Usage: test [--name NAME]
453 |
454 | Options:
455 | --name NAME [default: file.txt]
456 | --help, -h display this help and exit
457 |
458 | $ ./example
459 | main.NameDotName{Head:"file", Tail:"txt"}
460 | ```
461 |
462 | ### Custom placeholders
463 |
464 | Use the `placeholder` tag to control which placeholder text is used in the usage text.
465 |
466 | ```go
467 | var args struct {
468 | Input string `arg:"positional" placeholder:"SRC"`
469 | Output []string `arg:"positional" placeholder:"DST"`
470 | Optimize int `arg:"-O" help:"optimization level" placeholder:"LEVEL"`
471 | MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" placeholder:"N"`
472 | }
473 | arg.MustParse(&args)
474 | ```
475 |
476 | ```shell
477 | $ ./example -h
478 | Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
479 |
480 | Positional arguments:
481 | SRC
482 | DST
483 |
484 | Options:
485 | --optimize LEVEL, -O LEVEL
486 | optimization level
487 | --maxjobs N, -j N maximum number of simultaneous jobs
488 | --help, -h display this help and exit
489 | ```
490 |
491 | ### Description strings
492 |
493 | A descriptive message can be added at the top of the help text by implementing
494 | a `Description` function that returns a string.
495 |
496 | ```go
497 | type args struct {
498 | Foo string
499 | }
500 |
501 | func (args) Description() string {
502 | return "this program does this and that"
503 | }
504 |
505 | func main() {
506 | var args args
507 | arg.MustParse(&args)
508 | }
509 | ```
510 |
511 | ```shell
512 | $ ./example -h
513 | this program does this and that
514 | Usage: example [--foo FOO]
515 |
516 | Options:
517 | --foo FOO
518 | --help, -h display this help and exit
519 | ```
520 |
521 | Similarly an epilogue can be added at the end of the help text by implementing
522 | the `Epilogue` function.
523 |
524 | ```go
525 | type args struct {
526 | Foo string
527 | }
528 |
529 | func (args) Epilogue() string {
530 | return "For more information visit github.com/alexflint/go-arg"
531 | }
532 |
533 | func main() {
534 | var args args
535 | arg.MustParse(&args)
536 | }
537 | ```
538 |
539 | ```shell
540 | $ ./example -h
541 | Usage: example [--foo FOO]
542 |
543 | Options:
544 | --foo FOO
545 | --help, -h display this help and exit
546 |
547 | For more information visit github.com/alexflint/go-arg
548 | ```
549 |
550 | ### Subcommands
551 |
552 | Subcommands are commonly used in tools that wish to group multiple functions into a single program. An example is the `git` tool:
553 | ```shell
554 | $ git checkout [arguments specific to checking out code]
555 | $ git commit [arguments specific to committing]
556 | $ git push [arguments specific to pushing]
557 | ```
558 |
559 | The strings "checkout", "commit", and "push" are different from simple positional arguments because the options available to the user change depending on which subcommand they choose.
560 |
561 | This can be implemented with `go-arg` as follows:
562 |
563 | ```go
564 | type CheckoutCmd struct {
565 | Branch string `arg:"positional"`
566 | Track bool `arg:"-t"`
567 | }
568 | type CommitCmd struct {
569 | All bool `arg:"-a"`
570 | Message string `arg:"-m"`
571 | }
572 | type PushCmd struct {
573 | Remote string `arg:"positional"`
574 | Branch string `arg:"positional"`
575 | SetUpstream bool `arg:"-u"`
576 | }
577 | var args struct {
578 | Checkout *CheckoutCmd `arg:"subcommand:checkout"`
579 | Commit *CommitCmd `arg:"subcommand:commit"`
580 | Push *PushCmd `arg:"subcommand:push"`
581 | Quiet bool `arg:"-q"` // this flag is global to all subcommands
582 | }
583 |
584 | arg.MustParse(&args)
585 |
586 | switch {
587 | case args.Checkout != nil:
588 | fmt.Printf("checkout requested for branch %s\n", args.Checkout.Branch)
589 | case args.Commit != nil:
590 | fmt.Printf("commit requested with message \"%s\"\n", args.Commit.Message)
591 | case args.Push != nil:
592 | fmt.Printf("push requested from %s to %s\n", args.Push.Branch, args.Push.Remote)
593 | }
594 | ```
595 |
596 | Some additional rules apply when working with subcommands:
597 | * The `subcommand` tag can only be used with fields that are pointers to structs
598 | * Any struct that contains a subcommand must not contain any positionals
599 |
600 | This package allows to have a program that accepts subcommands, but also does something else
601 | when no subcommands are specified.
602 | If on the other hand you want the program to terminate when no subcommands are specified,
603 | the recommended way is:
604 |
605 | ```go
606 | p := arg.MustParse(&args)
607 | if p.Subcommand() == nil {
608 | p.Fail("missing subcommand")
609 | }
610 | ```
611 |
612 | ### Custom handling of --help and --version
613 |
614 | The following reproduces the internal logic of `MustParse` for the simple case where
615 | you are not using subcommands or --version. This allows you to respond
616 | programatically to --help, and to any errors that come up.
617 |
618 | ```go
619 | var args struct {
620 | Something string
621 | }
622 |
623 | p, err := arg.NewParser(arg.Config{}, &args)
624 | if err != nil {
625 | log.Fatalf("there was an error in the definition of the Go struct: %v", err)
626 | }
627 |
628 | err = p.Parse(os.Args[1:])
629 | switch {
630 | case err == arg.ErrHelp: // indicates that user wrote "--help" on command line
631 | p.WriteHelp(os.Stdout)
632 | os.Exit(0)
633 | case err != nil:
634 | fmt.Printf("error: %v\n", err)
635 | p.WriteUsage(os.Stdout)
636 | os.Exit(1)
637 | }
638 | ```
639 |
640 | ```shell
641 | $ go run ./example --help
642 | Usage: ./example --something SOMETHING
643 |
644 | Options:
645 | --something SOMETHING
646 | --help, -h display this help and exit
647 |
648 | $ ./example --wrong
649 | error: unknown argument --wrong
650 | Usage: ./example --something SOMETHING
651 |
652 | $ ./example
653 | error: --something is required
654 | Usage: ./example --something SOMETHING
655 | ```
656 |
657 | To also handle --version programatically, use the following:
658 |
659 | ```go
660 | type args struct {
661 | Something string
662 | }
663 |
664 | func (args) Version() string {
665 | return "1.2.3"
666 | }
667 |
668 | func main() {
669 | var args args
670 | p, err := arg.NewParser(arg.Config{}, &args)
671 | if err != nil {
672 | log.Fatalf("there was an error in the definition of the Go struct: %v", err)
673 | }
674 |
675 | err = p.Parse(os.Args[1:])
676 | switch {
677 | case err == arg.ErrHelp: // found "--help" on command line
678 | p.WriteHelp(os.Stdout)
679 | os.Exit(0)
680 | case err == arg.ErrVersion: // found "--version" on command line
681 | fmt.Println(args.Version())
682 | os.Exit(0)
683 | case err != nil:
684 | fmt.Printf("error: %v\n", err)
685 | p.WriteUsage(os.Stdout)
686 | os.Exit(1)
687 | }
688 |
689 | fmt.Printf("got %q\n", args.Something)
690 | }
691 | ```
692 |
693 | ```shell
694 | $ ./example --version
695 | 1.2.3
696 |
697 | $ go run ./example --help
698 | 1.2.3
699 | Usage: example --something SOMETHING
700 |
701 | Options:
702 | --something SOMETHING
703 | --help, -h display this help and exit
704 |
705 | $ ./example --wrong
706 | 1.2.3
707 | error: unknown argument --wrong
708 | Usage: example --something SOMETHING
709 |
710 | $ ./example
711 | error: --something is required
712 | Usage: example --something SOMETHING
713 | ```
714 |
715 | To generate subcommand-specific help messages, use the following most general version
716 | (this also works in absence of subcommands but is a bit more complex):
717 |
718 | ```go
719 | type fetchCmd struct {
720 | Count int
721 | }
722 |
723 | type args struct {
724 | Something string
725 | Fetch *fetchCmd `arg:"subcommand"`
726 | }
727 |
728 | func (args) Version() string {
729 | return "1.2.3"
730 | }
731 |
732 | func main() {
733 | var args args
734 | p, err := arg.NewParser(arg.Config{}, &args)
735 | if err != nil {
736 | log.Fatalf("there was an error in the definition of the Go struct: %v", err)
737 | }
738 |
739 | err = p.Parse(os.Args[1:])
740 | switch {
741 | case err == arg.ErrHelp: // found "--help" on command line
742 | p.WriteHelpForSubcommand(os.Stdout, p.SubcommandNames()...)
743 | os.Exit(0)
744 | case err == arg.ErrVersion: // found "--version" on command line
745 | fmt.Println(args.Version())
746 | os.Exit(0)
747 | case err != nil:
748 | fmt.Printf("error: %v\n", err)
749 | p.WriteUsageForSubcommand(os.Stdout, p.SubcommandNames()...)
750 | os.Exit(1)
751 | }
752 | }
753 | ```
754 |
755 | ```shell
756 | $ ./example --version
757 | 1.2.3
758 |
759 | $ ./example --help
760 | 1.2.3
761 | Usage: example [--something SOMETHING] []
762 |
763 | Options:
764 | --something SOMETHING
765 | --help, -h display this help and exit
766 | --version display version and exit
767 |
768 | Commands:
769 | fetch
770 |
771 | $ ./example fetch --help
772 | 1.2.3
773 | Usage: example fetch [--count COUNT]
774 |
775 | Options:
776 | --count COUNT
777 |
778 | Global options:
779 | --something SOMETHING
780 | --help, -h display this help and exit
781 | --version display version and exit
782 | ```
783 |
784 | ### API Documentation
785 |
786 | https://pkg.go.dev/github.com/alexflint/go-arg
787 |
788 | ### Rationale
789 |
790 | There are many command line argument parsing libraries for Go, including one in the standard library, so why build another?
791 |
792 | 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.
793 |
794 | 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.
795 |
796 | The idea behind `go-arg` is that Go already has an excellent way to describe data structures using structs, so there is no need to develop additional levels of abstraction. Instead of one API to specify which arguments your program accepts, and then another API to get the values of those arguments, `go-arg` replaces both with a single struct.
797 |
798 | ### Backward compatibility notes
799 |
800 | 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.
801 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | // Package arg parses command line arguments using the fields from a struct.
2 | //
3 | // For example,
4 | //
5 | // var args struct {
6 | // Iter int
7 | // Debug bool
8 | // }
9 | // arg.MustParse(&args)
10 | //
11 | // defines two command line arguments, which can be set using any of
12 | //
13 | // ./example --iter=1 --debug // debug is a boolean flag so its value is set to true
14 | // ./example -iter 1 // debug defaults to its zero value (false)
15 | // ./example --debug=true // iter defaults to its zero value (zero)
16 | //
17 | // The fastest way to see how to use go-arg is to read the examples below.
18 | //
19 | // Fields can be bool, string, any float type, or any signed or unsigned integer type.
20 | // They can also be slices of any of the above, or slices of pointers to any of the above.
21 | //
22 | // Tags can be specified using the `arg` and `help` tag names:
23 | //
24 | // var args struct {
25 | // Input string `arg:"positional"`
26 | // Log string `arg:"positional,required"`
27 | // Debug bool `arg:"-d" help:"turn on debug mode"`
28 | // RealMode bool `arg:"--real"
29 | // Wr io.Writer `arg:"-"`
30 | // }
31 | //
32 | // Any tag string that starts with a single hyphen is the short form for an argument
33 | // (e.g. `./example -d`), and any tag string that starts with two hyphens is the long
34 | // form for the argument (instead of the field name).
35 | //
36 | // Other valid tag strings are `positional` and `required`.
37 | //
38 | // Fields can be excluded from processing with `arg:"-"`.
39 | package arg
40 |
--------------------------------------------------------------------------------
/example_test.go:
--------------------------------------------------------------------------------
1 | package arg
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "net/mail"
7 | "net/url"
8 | "os"
9 | "strings"
10 | "time"
11 | )
12 |
13 | func split(s string) []string {
14 | return strings.Split(s, " ")
15 | }
16 |
17 | // This example demonstrates basic usage
18 | func Example() {
19 | // These are the args you would pass in on the command line
20 | os.Args = split("./example --foo=hello --bar")
21 |
22 | var args struct {
23 | Foo string
24 | Bar bool
25 | }
26 | MustParse(&args)
27 | fmt.Println(args.Foo, args.Bar)
28 | // output: hello true
29 | }
30 |
31 | // This example demonstrates arguments that have default values
32 | func Example_defaultValues() {
33 | // These are the args you would pass in on the command line
34 | os.Args = split("./example")
35 |
36 | var args struct {
37 | Foo string `default:"abc"`
38 | }
39 | MustParse(&args)
40 | fmt.Println(args.Foo)
41 | // output: abc
42 | }
43 |
44 | // This example demonstrates arguments that are required
45 | func Example_requiredArguments() {
46 | // These are the args you would pass in on the command line
47 | os.Args = split("./example --foo=abc --bar")
48 |
49 | var args struct {
50 | Foo string `arg:"required"`
51 | Bar bool
52 | }
53 | MustParse(&args)
54 | fmt.Println(args.Foo, args.Bar)
55 | // output: abc true
56 | }
57 |
58 | // This example demonstrates positional arguments
59 | func Example_positionalArguments() {
60 | // These are the args you would pass in on the command line
61 | os.Args = split("./example in out1 out2 out3")
62 |
63 | var args struct {
64 | Input string `arg:"positional"`
65 | Output []string `arg:"positional"`
66 | }
67 | MustParse(&args)
68 | fmt.Println("In:", args.Input)
69 | fmt.Println("Out:", args.Output)
70 | // output:
71 | // In: in
72 | // Out: [out1 out2 out3]
73 | }
74 |
75 | // This example demonstrates arguments that have multiple values
76 | func Example_multipleValues() {
77 | // The args you would pass in on the command line
78 | os.Args = split("./example --database localhost --ids 1 2 3")
79 |
80 | var args struct {
81 | Database string
82 | IDs []int64
83 | }
84 | MustParse(&args)
85 | fmt.Printf("Fetching the following IDs from %s: %v", args.Database, args.IDs)
86 | // output: Fetching the following IDs from localhost: [1 2 3]
87 | }
88 |
89 | // This example demonstrates arguments with keys and values
90 | func Example_mappings() {
91 | // The args you would pass in on the command line
92 | os.Args = split("./example --userids john=123 mary=456")
93 |
94 | var args struct {
95 | UserIDs map[string]int
96 | }
97 | MustParse(&args)
98 | fmt.Println(args.UserIDs)
99 | // output: map[john:123 mary:456]
100 | }
101 |
102 | type commaSeparated struct {
103 | M map[string]string
104 | }
105 |
106 | func (c *commaSeparated) UnmarshalText(b []byte) error {
107 | c.M = make(map[string]string)
108 | for _, part := range strings.Split(string(b), ",") {
109 | pos := strings.Index(part, "=")
110 | if pos == -1 {
111 | return fmt.Errorf("error parsing %q, expected format key=value", part)
112 | }
113 | c.M[part[:pos]] = part[pos+1:]
114 | }
115 | return nil
116 | }
117 |
118 | // This example demonstrates arguments with keys and values separated by commas
119 | func Example_mappingWithCommas() {
120 | // The args you would pass in on the command line
121 | os.Args = split("./example --values one=two,three=four")
122 |
123 | var args struct {
124 | Values commaSeparated
125 | }
126 | MustParse(&args)
127 | fmt.Println(args.Values.M)
128 | // output: map[one:two three:four]
129 | }
130 |
131 | // This eample demonstrates multiple value arguments that can be mixed with
132 | // other arguments.
133 | func Example_multipleMixed() {
134 | os.Args = split("./example -c cmd1 db1 -f file1 db2 -c cmd2 -f file2 -f file3 db3 -c cmd3")
135 | var args struct {
136 | Commands []string `arg:"-c,separate"`
137 | Files []string `arg:"-f,separate"`
138 | Databases []string `arg:"positional"`
139 | }
140 | MustParse(&args)
141 | fmt.Println("Commands:", args.Commands)
142 | fmt.Println("Files:", args.Files)
143 | fmt.Println("Databases:", args.Databases)
144 |
145 | // output:
146 | // Commands: [cmd1 cmd2 cmd3]
147 | // Files: [file1 file2 file3]
148 | // Databases: [db1 db2 db3]
149 | }
150 |
151 | // This example shows the usage string generated by go-arg
152 | func Example_helpText() {
153 | // These are the args you would pass in on the command line
154 | os.Args = split("./example --help")
155 |
156 | var args struct {
157 | Input string `arg:"positional,required"`
158 | Output []string `arg:"positional"`
159 | Verbose bool `arg:"-v" help:"verbosity level"`
160 | Dataset string `help:"dataset to use"`
161 | Optimize int `arg:"-O,--optim" help:"optimization level"`
162 | }
163 |
164 | // This is only necessary when running inside golang's runnable example harness
165 | mustParseExit = func(int) {}
166 | mustParseOut = os.Stdout
167 |
168 | MustParse(&args)
169 |
170 | // output:
171 | // Usage: example [--verbose] [--dataset DATASET] [--optim OPTIM] INPUT [OUTPUT [OUTPUT ...]]
172 | //
173 | // Positional arguments:
174 | // INPUT
175 | // OUTPUT
176 | //
177 | // Options:
178 | // --verbose, -v verbosity level
179 | // --dataset DATASET dataset to use
180 | // --optim OPTIM, -O OPTIM
181 | // optimization level
182 | // --help, -h display this help and exit
183 | }
184 |
185 | // This example shows the usage string generated by go-arg with customized placeholders
186 | func Example_helpPlaceholder() {
187 | // These are the args you would pass in on the command line
188 | os.Args = split("./example --help")
189 |
190 | var args struct {
191 | Input string `arg:"positional,required" placeholder:"SRC"`
192 | Output []string `arg:"positional" placeholder:"DST"`
193 | Optimize int `arg:"-O" help:"optimization level" placeholder:"LEVEL"`
194 | MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" placeholder:"N"`
195 | }
196 |
197 | // This is only necessary when running inside golang's runnable example harness
198 | mustParseExit = func(int) {}
199 | mustParseOut = os.Stdout
200 |
201 | MustParse(&args)
202 |
203 | // output:
204 | // Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
205 | //
206 | // Positional arguments:
207 | // SRC
208 | // DST
209 | //
210 | // Options:
211 | // --optimize LEVEL, -O LEVEL
212 | // optimization level
213 | // --maxjobs N, -j N maximum number of simultaneous jobs
214 | // --help, -h display this help and exit
215 | }
216 |
217 | // This example shows the usage string generated by go-arg when using subcommands
218 | func Example_helpTextWithSubcommand() {
219 | // These are the args you would pass in on the command line
220 | os.Args = split("./example --help")
221 |
222 | type getCmd struct {
223 | Item string `arg:"positional" help:"item to fetch"`
224 | }
225 |
226 | type listCmd struct {
227 | Format string `help:"output format"`
228 | Limit int
229 | }
230 |
231 | var args struct {
232 | Verbose bool
233 | Get *getCmd `arg:"subcommand" help:"fetch an item and print it"`
234 | List *listCmd `arg:"subcommand" help:"list available items"`
235 | }
236 |
237 | // This is only necessary when running inside golang's runnable example harness
238 | mustParseExit = func(int) {}
239 | mustParseOut = os.Stdout
240 |
241 | MustParse(&args)
242 |
243 | // output:
244 | // Usage: example [--verbose] []
245 | //
246 | // Options:
247 | // --verbose
248 | // --help, -h display this help and exit
249 | //
250 | // Commands:
251 | // get fetch an item and print it
252 | // list list available items
253 | }
254 |
255 | // This example shows the usage string generated by go-arg when using subcommands
256 | func Example_helpTextWhenUsingSubcommand() {
257 | // These are the args you would pass in on the command line
258 | os.Args = split("./example get --help")
259 |
260 | type getCmd struct {
261 | Item string `arg:"positional,required" help:"item to fetch"`
262 | }
263 |
264 | type listCmd struct {
265 | Format string `help:"output format"`
266 | Limit int
267 | }
268 |
269 | var args struct {
270 | Verbose bool
271 | Get *getCmd `arg:"subcommand" help:"fetch an item and print it"`
272 | List *listCmd `arg:"subcommand" help:"list available items"`
273 | }
274 |
275 | // This is only necessary when running inside golang's runnable example harness
276 | mustParseExit = func(int) {}
277 | mustParseOut = os.Stdout
278 |
279 | MustParse(&args)
280 |
281 | // output:
282 | // Usage: example get ITEM
283 | //
284 | // Positional arguments:
285 | // ITEM item to fetch
286 | //
287 | // Global options:
288 | // --verbose
289 | // --help, -h display this help and exit
290 | }
291 |
292 | // This example shows how to print help for an explicit subcommand
293 | func Example_writeHelpForSubcommand() {
294 | // These are the args you would pass in on the command line
295 | os.Args = split("./example get --help")
296 |
297 | type getCmd struct {
298 | Item string `arg:"positional" help:"item to fetch"`
299 | }
300 |
301 | type listCmd struct {
302 | Format string `help:"output format"`
303 | Limit int
304 | }
305 |
306 | var args struct {
307 | Verbose bool
308 | Get *getCmd `arg:"subcommand" help:"fetch an item and print it"`
309 | List *listCmd `arg:"subcommand" help:"list available items"`
310 | }
311 |
312 | // This is only necessary when running inside golang's runnable example harness
313 | exit := func(int) {}
314 |
315 | p, err := NewParser(Config{Exit: exit}, &args)
316 | if err != nil {
317 | fmt.Println(err)
318 | os.Exit(1)
319 | }
320 |
321 | err = p.WriteHelpForSubcommand(os.Stdout, "list")
322 | if err != nil {
323 | fmt.Println(err)
324 | os.Exit(1)
325 | }
326 |
327 | // output:
328 | // Usage: example list [--format FORMAT] [--limit LIMIT]
329 | //
330 | // Options:
331 | // --format FORMAT output format
332 | // --limit LIMIT
333 | //
334 | // Global options:
335 | // --verbose
336 | // --help, -h display this help and exit
337 | }
338 |
339 | // This example shows how to print help for a subcommand that is nested several levels deep
340 | func Example_writeHelpForSubcommandNested() {
341 | // These are the args you would pass in on the command line
342 | os.Args = split("./example get --help")
343 |
344 | type mostNestedCmd struct {
345 | Item string
346 | }
347 |
348 | type nestedCmd struct {
349 | MostNested *mostNestedCmd `arg:"subcommand"`
350 | }
351 |
352 | type topLevelCmd struct {
353 | Nested *nestedCmd `arg:"subcommand"`
354 | }
355 |
356 | var args struct {
357 | TopLevel *topLevelCmd `arg:"subcommand"`
358 | }
359 |
360 | // This is only necessary when running inside golang's runnable example harness
361 | exit := func(int) {}
362 |
363 | p, err := NewParser(Config{Exit: exit}, &args)
364 | if err != nil {
365 | fmt.Println(err)
366 | os.Exit(1)
367 | }
368 |
369 | err = p.WriteHelpForSubcommand(os.Stdout, "toplevel", "nested", "mostnested")
370 | if err != nil {
371 | fmt.Println(err)
372 | os.Exit(1)
373 | }
374 |
375 | // output:
376 | // Usage: example toplevel nested mostnested [--item ITEM]
377 | //
378 | // Options:
379 | // --item ITEM
380 | // --help, -h display this help and exit
381 | }
382 |
383 | // This example shows the error string generated by go-arg when an invalid option is provided
384 | func Example_errorText() {
385 | // These are the args you would pass in on the command line
386 | os.Args = split("./example --optimize INVALID")
387 |
388 | var args struct {
389 | Input string `arg:"positional,required"`
390 | Output []string `arg:"positional"`
391 | Verbose bool `arg:"-v" help:"verbosity level"`
392 | Dataset string `help:"dataset to use"`
393 | Optimize int `arg:"-O,help:optimization level"`
394 | }
395 |
396 | // This is only necessary when running inside golang's runnable example harness
397 | mustParseExit = func(int) {}
398 | mustParseOut = os.Stdout
399 |
400 | MustParse(&args)
401 |
402 | // output:
403 | // Usage: example [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]]
404 | // error: error processing --optimize: strconv.ParseInt: parsing "INVALID": invalid syntax
405 | }
406 |
407 | // This example shows the error string generated by go-arg when an invalid option is provided
408 | func Example_errorTextForSubcommand() {
409 | // These are the args you would pass in on the command line
410 | os.Args = split("./example get --count INVALID")
411 |
412 | type getCmd struct {
413 | Count int
414 | }
415 |
416 | var args struct {
417 | Get *getCmd `arg:"subcommand"`
418 | }
419 |
420 | // This is only necessary when running inside golang's runnable example harness
421 | mustParseExit = func(int) {}
422 | mustParseOut = os.Stdout
423 |
424 | MustParse(&args)
425 |
426 | // output:
427 | // Usage: example get [--count COUNT]
428 | // error: error processing --count: strconv.ParseInt: parsing "INVALID": invalid syntax
429 | }
430 |
431 | // This example demonstrates use of subcommands
432 | func Example_subcommand() {
433 | // These are the args you would pass in on the command line
434 | os.Args = split("./example commit -a -m what-this-commit-is-about")
435 |
436 | type CheckoutCmd struct {
437 | Branch string `arg:"positional"`
438 | Track bool `arg:"-t"`
439 | }
440 | type CommitCmd struct {
441 | All bool `arg:"-a"`
442 | Message string `arg:"-m"`
443 | }
444 | type PushCmd struct {
445 | Remote string `arg:"positional"`
446 | Branch string `arg:"positional"`
447 | SetUpstream bool `arg:"-u"`
448 | }
449 | var args struct {
450 | Checkout *CheckoutCmd `arg:"subcommand:checkout"`
451 | Commit *CommitCmd `arg:"subcommand:commit"`
452 | Push *PushCmd `arg:"subcommand:push"`
453 | Quiet bool `arg:"-q"` // this flag is global to all subcommands
454 | }
455 |
456 | // This is only necessary when running inside golang's runnable example harness
457 | mustParseExit = func(int) {}
458 | mustParseOut = os.Stdout
459 |
460 | MustParse(&args)
461 |
462 | switch {
463 | case args.Checkout != nil:
464 | fmt.Printf("checkout requested for branch %s\n", args.Checkout.Branch)
465 | case args.Commit != nil:
466 | fmt.Printf("commit requested with message \"%s\"\n", args.Commit.Message)
467 | case args.Push != nil:
468 | fmt.Printf("push requested from %s to %s\n", args.Push.Branch, args.Push.Remote)
469 | }
470 |
471 | // output:
472 | // commit requested with message "what-this-commit-is-about"
473 | }
474 |
475 | func Example_allSupportedTypes() {
476 | // These are the args you would pass in on the command line
477 | os.Args = []string{}
478 |
479 | var args struct {
480 | Bool bool
481 | Byte byte
482 | Rune rune
483 | Int int
484 | Int8 int8
485 | Int16 int16
486 | Int32 int32
487 | Int64 int64
488 | Float32 float32
489 | Float64 float64
490 | String string
491 | Duration time.Duration
492 | URL url.URL
493 | Email mail.Address
494 | MAC net.HardwareAddr
495 | }
496 |
497 | // go-arg supports each of the types above, as well as pointers to any of
498 | // the above and slices of any of the above. It also supports any types that
499 | // implements encoding.TextUnmarshaler.
500 |
501 | MustParse(&args)
502 |
503 | // output:
504 | }
505 |
506 | func Example_envVarOnly() {
507 | os.Args = split("./example")
508 | _ = os.Setenv("AUTH_KEY", "my_key")
509 |
510 | defer os.Unsetenv("AUTH_KEY")
511 |
512 | var args struct {
513 | AuthKey string `arg:"--,env:AUTH_KEY"`
514 | }
515 |
516 | MustParse(&args)
517 |
518 | fmt.Println(args.AuthKey)
519 | // output: my_key
520 | }
521 |
522 | func Example_envVarOnlyShouldIgnoreFlag() {
523 | os.Args = split("./example --=my_key")
524 |
525 | var args struct {
526 | AuthKey string `arg:"--,env:AUTH_KEY"`
527 | }
528 |
529 | err := Parse(&args)
530 |
531 | fmt.Println(err)
532 | // output: unknown argument --=my_key
533 | }
534 |
535 | func Example_envVarOnlyShouldIgnoreShortFlag() {
536 | os.Args = split("./example -=my_key")
537 |
538 | var args struct {
539 | AuthKey string `arg:"--,env:AUTH_KEY"`
540 | }
541 |
542 | err := Parse(&args)
543 |
544 | fmt.Println(err)
545 | // output: unknown argument -=my_key
546 | }
547 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/alexflint/go-arg
2 |
3 | require (
4 | github.com/alexflint/go-scalar v1.2.0
5 | github.com/stretchr/testify v1.7.0
6 | )
7 |
8 | require (
9 | github.com/davecgh/go-spew v1.1.1 // indirect
10 | github.com/pmezard/go-difflib v1.0.0 // indirect
11 | gopkg.in/yaml.v3 v3.0.0 // indirect
12 | )
13 |
14 | go 1.18
15 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
2 | github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
9 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
10 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
11 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
14 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
15 | gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
16 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
17 |
--------------------------------------------------------------------------------
/parse.go:
--------------------------------------------------------------------------------
1 | package arg
2 |
3 | import (
4 | "encoding"
5 | "encoding/csv"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "os"
10 | "path/filepath"
11 | "reflect"
12 | "strings"
13 |
14 | scalar "github.com/alexflint/go-scalar"
15 | )
16 |
17 | // path represents a sequence of steps to find the output location for an
18 | // argument or subcommand in the final destination struct
19 | type path struct {
20 | root int // index of the destination struct
21 | fields []reflect.StructField // sequence of struct fields to traverse
22 | }
23 |
24 | // String gets a string representation of the given path
25 | func (p path) String() string {
26 | s := "args"
27 | for _, f := range p.fields {
28 | s += "." + f.Name
29 | }
30 | return s
31 | }
32 |
33 | // Child gets a new path representing a child of this path.
34 | func (p path) Child(f reflect.StructField) path {
35 | // copy the entire slice of fields to avoid possible slice overwrite
36 | subfields := make([]reflect.StructField, len(p.fields)+1)
37 | copy(subfields, p.fields)
38 | subfields[len(subfields)-1] = f
39 | return path{
40 | root: p.root,
41 | fields: subfields,
42 | }
43 | }
44 |
45 | // spec represents a command line option
46 | type spec struct {
47 | dest path
48 | field reflect.StructField // the struct field from which this option was created
49 | long string // the --long form for this option, or empty if none
50 | short string // the -s short form for this option, or empty if none
51 | cardinality cardinality // determines how many tokens will be present (possible values: zero, one, multiple)
52 | required bool // if true, this option must be present on the command line
53 | positional bool // if true, this option will be looked for in the positional flags
54 | separate bool // if true, each slice and map entry will have its own --flag
55 | help string // the help text for this option
56 | env string // the name of the environment variable for this option, or empty for none
57 | defaultValue reflect.Value // default value for this option
58 | defaultString string // default value for this option, in string form to be displayed in help text
59 | placeholder string // placeholder string in help
60 | }
61 |
62 | // command represents a named subcommand, or the top-level command
63 | type command struct {
64 | name string
65 | aliases []string
66 | help string
67 | dest path
68 | specs []*spec
69 | subcommands []*command
70 | parent *command
71 | }
72 |
73 | // ErrHelp indicates that the builtin -h or --help were provided
74 | var ErrHelp = errors.New("help requested by user")
75 |
76 | // ErrVersion indicates that the builtin --version was provided
77 | var ErrVersion = errors.New("version requested by user")
78 |
79 | // for monkey patching in example and test code
80 | var mustParseExit = os.Exit
81 | var mustParseOut io.Writer = os.Stdout
82 |
83 | // MustParse processes command line arguments and exits upon failure
84 | func MustParse(dest ...interface{}) *Parser {
85 | return mustParse(Config{Exit: mustParseExit, Out: mustParseOut}, dest...)
86 | }
87 |
88 | // mustParse is a helper that facilitates testing
89 | func mustParse(config Config, dest ...interface{}) *Parser {
90 | p, err := NewParser(config, dest...)
91 | if err != nil {
92 | fmt.Fprintln(config.Out, err)
93 | config.Exit(2)
94 | return nil
95 | }
96 |
97 | p.MustParse(flags())
98 | return p
99 | }
100 |
101 | // Parse processes command line arguments and stores them in dest
102 | func Parse(dest ...interface{}) error {
103 | p, err := NewParser(Config{}, dest...)
104 | if err != nil {
105 | return err
106 | }
107 | return p.Parse(flags())
108 | }
109 |
110 | // flags gets all command line arguments other than the first (program name)
111 | func flags() []string {
112 | if len(os.Args) == 0 { // os.Args could be empty
113 | return nil
114 | }
115 | return os.Args[1:]
116 | }
117 |
118 | // Config represents configuration options for an argument parser
119 | type Config struct {
120 | // Program is the name of the program used in the help text
121 | Program string
122 |
123 | // IgnoreEnv instructs the library not to read environment variables
124 | IgnoreEnv bool
125 |
126 | // IgnoreDefault instructs the library not to reset the variables to the
127 | // default values, including pointers to sub commands
128 | IgnoreDefault bool
129 |
130 | // StrictSubcommands intructs the library not to allow global commands after
131 | // subcommand
132 | StrictSubcommands bool
133 |
134 | // EnvPrefix instructs the library to use a name prefix when reading environment variables.
135 | EnvPrefix string
136 |
137 | // Exit is called to terminate the process with an error code (defaults to os.Exit)
138 | Exit func(int)
139 |
140 | // Out is where help text, usage text, and failure messages are printed (defaults to os.Stdout)
141 | Out io.Writer
142 | }
143 |
144 | // Parser represents a set of command line options with destination values
145 | type Parser struct {
146 | cmd *command
147 | roots []reflect.Value
148 | config Config
149 | version string
150 | description string
151 | epilogue string
152 |
153 | // the following field changes during processing of command line arguments
154 | subcommand []string
155 | }
156 |
157 | // Versioned is the interface that the destination struct should implement to
158 | // make a version string appear at the top of the help message.
159 | type Versioned interface {
160 | // Version returns the version string that will be printed on a line by itself
161 | // at the top of the help message.
162 | Version() string
163 | }
164 |
165 | // Described is the interface that the destination struct should implement to
166 | // make a description string appear at the top of the help message.
167 | type Described interface {
168 | // Description returns the string that will be printed on a line by itself
169 | // at the top of the help message.
170 | Description() string
171 | }
172 |
173 | // Epilogued is the interface that the destination struct should implement to
174 | // add an epilogue string at the bottom of the help message.
175 | type Epilogued interface {
176 | // Epilogue returns the string that will be printed on a line by itself
177 | // at the end of the help message.
178 | Epilogue() string
179 | }
180 |
181 | // walkFields calls a function for each field of a struct, recursively expanding struct fields.
182 | func walkFields(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool) {
183 | walkFieldsImpl(t, visit, nil)
184 | }
185 |
186 | func walkFieldsImpl(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool, path []int) {
187 | for i := 0; i < t.NumField(); i++ {
188 | field := t.Field(i)
189 | field.Index = make([]int, len(path)+1)
190 | copy(field.Index, append(path, i))
191 | expand := visit(field, t)
192 | if expand && field.Type.Kind() == reflect.Struct {
193 | var subpath []int
194 | if field.Anonymous {
195 | subpath = append(path, i)
196 | }
197 | walkFieldsImpl(field.Type, visit, subpath)
198 | }
199 | }
200 | }
201 |
202 | // NewParser constructs a parser from a list of destination structs
203 | func NewParser(config Config, dests ...interface{}) (*Parser, error) {
204 | // fill in defaults
205 | if config.Exit == nil {
206 | config.Exit = os.Exit
207 | }
208 | if config.Out == nil {
209 | config.Out = os.Stdout
210 | }
211 |
212 | // first pick a name for the command for use in the usage text
213 | var name string
214 | switch {
215 | case config.Program != "":
216 | name = config.Program
217 | case len(os.Args) > 0:
218 | name = filepath.Base(os.Args[0])
219 | default:
220 | name = "program"
221 | }
222 |
223 | // construct a parser
224 | p := Parser{
225 | cmd: &command{name: name},
226 | config: config,
227 | }
228 |
229 | // make a list of roots
230 | for _, dest := range dests {
231 | p.roots = append(p.roots, reflect.ValueOf(dest))
232 | }
233 |
234 | // process each of the destination values
235 | for i, dest := range dests {
236 | t := reflect.TypeOf(dest)
237 | if t.Kind() != reflect.Ptr {
238 | panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", t))
239 | }
240 |
241 | cmd, err := cmdFromStruct(name, path{root: i}, t, config.EnvPrefix)
242 | if err != nil {
243 | return nil, err
244 | }
245 |
246 | // for backwards compatibility, add nonzero field values as defaults
247 | // this applies only to the top-level command, not to subcommands (this inconsistency
248 | // is the reason that this method for setting default values was deprecated)
249 | for _, spec := range cmd.specs {
250 | // get the value
251 | v := p.val(spec.dest)
252 |
253 | // if the value is the "zero value" (e.g. nil pointer, empty struct) then ignore
254 | if isZero(v) {
255 | continue
256 | }
257 |
258 | // store as a default
259 | spec.defaultValue = v
260 |
261 | // we need a string to display in help text
262 | // if MarshalText is implemented then use that
263 | if m, ok := v.Interface().(encoding.TextMarshaler); ok {
264 | s, err := m.MarshalText()
265 | if err != nil {
266 | return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
267 | }
268 | spec.defaultString = string(s)
269 | } else {
270 | spec.defaultString = fmt.Sprintf("%v", v)
271 | }
272 | }
273 |
274 | p.cmd.specs = append(p.cmd.specs, cmd.specs...)
275 | p.cmd.subcommands = append(p.cmd.subcommands, cmd.subcommands...)
276 |
277 | if dest, ok := dest.(Versioned); ok {
278 | p.version = dest.Version()
279 | }
280 | if dest, ok := dest.(Described); ok {
281 | p.description = dest.Description()
282 | }
283 | if dest, ok := dest.(Epilogued); ok {
284 | p.epilogue = dest.Epilogue()
285 | }
286 | }
287 |
288 | // Set the parent of the subcommands to be the top-level command
289 | // to make sure that global options work when there is more than one
290 | // dest supplied.
291 | for _, subcommand := range p.cmd.subcommands {
292 | subcommand.parent = p.cmd
293 | }
294 |
295 | return &p, nil
296 | }
297 |
298 | func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*command, error) {
299 | // commands can only be created from pointers to structs
300 | if t.Kind() != reflect.Ptr {
301 | return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a %s",
302 | dest, t.Kind())
303 | }
304 |
305 | t = t.Elem()
306 | if t.Kind() != reflect.Struct {
307 | return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a pointer to %s",
308 | dest, t.Kind())
309 | }
310 |
311 | cmd := command{
312 | name: name,
313 | dest: dest,
314 | }
315 |
316 | var errs []string
317 | walkFields(t, func(field reflect.StructField, t reflect.Type) bool {
318 | // check for the ignore switch in the tag
319 | tag := field.Tag.Get("arg")
320 | if tag == "-" {
321 | return false
322 | }
323 |
324 | // if this is an embedded struct then recurse into its fields, even if
325 | // it is unexported, because exported fields on unexported embedded
326 | // structs are still writable
327 | if field.Anonymous && field.Type.Kind() == reflect.Struct {
328 | return true
329 | }
330 |
331 | // ignore any other unexported field
332 | if !isExported(field.Name) {
333 | return false
334 | }
335 |
336 | // duplicate the entire path to avoid slice overwrites
337 | subdest := dest.Child(field)
338 | spec := spec{
339 | dest: subdest,
340 | field: field,
341 | long: strings.ToLower(field.Name),
342 | }
343 |
344 | help, exists := field.Tag.Lookup("help")
345 | if exists {
346 | spec.help = help
347 | }
348 |
349 | // process each comma-separated part of the tag
350 | var isSubcommand bool
351 | for _, key := range strings.Split(tag, ",") {
352 | if key == "" {
353 | continue
354 | }
355 | key = strings.TrimLeft(key, " ")
356 | var value string
357 | if pos := strings.Index(key, ":"); pos != -1 {
358 | value = key[pos+1:]
359 | key = key[:pos]
360 | }
361 |
362 | switch {
363 | case strings.HasPrefix(key, "---"):
364 | errs = append(errs, fmt.Sprintf("%s.%s: too many hyphens", t.Name(), field.Name))
365 | case strings.HasPrefix(key, "--"):
366 | spec.long = key[2:]
367 | case strings.HasPrefix(key, "-"):
368 | if len(key) > 2 {
369 | errs = append(errs, fmt.Sprintf("%s.%s: short arguments must be one character only",
370 | t.Name(), field.Name))
371 | return false
372 | }
373 | spec.short = key[1:]
374 | case key == "required":
375 | spec.required = true
376 | case key == "positional":
377 | spec.positional = true
378 | case key == "separate":
379 | spec.separate = true
380 | case key == "help": // deprecated
381 | spec.help = value
382 | case key == "env":
383 | // Use override name if provided
384 | if value != "" {
385 | spec.env = envPrefix + value
386 | } else {
387 | spec.env = envPrefix + strings.ToUpper(field.Name)
388 | }
389 | case key == "subcommand":
390 | // decide on a name for the subcommand
391 | var cmdnames []string
392 | if value == "" {
393 | cmdnames = []string{strings.ToLower(field.Name)}
394 | } else {
395 | cmdnames = strings.Split(value, "|")
396 | }
397 | for i := range cmdnames {
398 | cmdnames[i] = strings.TrimSpace(cmdnames[i])
399 | }
400 |
401 | // parse the subcommand recursively
402 | subcmd, err := cmdFromStruct(cmdnames[0], subdest, field.Type, envPrefix)
403 | if err != nil {
404 | errs = append(errs, err.Error())
405 | return false
406 | }
407 |
408 | subcmd.aliases = cmdnames[1:]
409 | subcmd.parent = &cmd
410 | subcmd.help = field.Tag.Get("help")
411 |
412 | cmd.subcommands = append(cmd.subcommands, subcmd)
413 | isSubcommand = true
414 | default:
415 | errs = append(errs, fmt.Sprintf("unrecognized tag '%s' on field %s", key, tag))
416 | return false
417 | }
418 | }
419 |
420 | // placeholder is the string used in the help text like this: "--somearg PLACEHOLDER"
421 | placeholder, hasPlaceholder := field.Tag.Lookup("placeholder")
422 | if hasPlaceholder {
423 | spec.placeholder = placeholder
424 | } else if spec.long != "" {
425 | spec.placeholder = strings.ToUpper(spec.long)
426 | } else {
427 | spec.placeholder = strings.ToUpper(spec.field.Name)
428 | }
429 |
430 | // if this is a subcommand then we've done everything we need to do
431 | if isSubcommand {
432 | return false
433 | }
434 |
435 | // check whether this field is supported. It's good to do this here rather than
436 | // wait until ParseValue because it means that a program with invalid argument
437 | // fields will always fail regardless of whether the arguments it received
438 | // exercised those fields.
439 | var err error
440 | spec.cardinality, err = cardinalityOf(field.Type)
441 | if err != nil {
442 | errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported",
443 | t.Name(), field.Name, field.Type.String()))
444 | return false
445 | }
446 |
447 | defaultString, hasDefault := field.Tag.Lookup("default")
448 | if hasDefault {
449 | // we do not support default values for maps and slices
450 | if spec.cardinality == multiple {
451 | errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields",
452 | t.Name(), field.Name))
453 | return false
454 | }
455 |
456 | // a required field cannot also have a default value
457 | if spec.required {
458 | errs = append(errs, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified",
459 | t.Name(), field.Name))
460 | return false
461 | }
462 |
463 | // parse the default value
464 | spec.defaultString = defaultString
465 | if field.Type.Kind() == reflect.Ptr {
466 | // here we have a field of type *T and we create a new T, no need to dereference
467 | // in order for the value to be settable
468 | spec.defaultValue = reflect.New(field.Type.Elem())
469 | } else {
470 | // here we have a field of type T and we create a new T and then dereference it
471 | // so that the resulting value is settable
472 | spec.defaultValue = reflect.New(field.Type).Elem()
473 | }
474 | err := scalar.ParseValue(spec.defaultValue, defaultString)
475 | if err != nil {
476 | errs = append(errs, fmt.Sprintf("%s.%s: error processing default value: %v", t.Name(), field.Name, err))
477 | return false
478 | }
479 | }
480 |
481 | // add the spec to the list of specs
482 | cmd.specs = append(cmd.specs, &spec)
483 |
484 | // if this was an embedded field then we already returned true up above
485 | return false
486 | })
487 |
488 | if len(errs) > 0 {
489 | return nil, errors.New(strings.Join(errs, "\n"))
490 | }
491 |
492 | // check that we don't have both positionals and subcommands
493 | var hasPositional bool
494 | for _, spec := range cmd.specs {
495 | if spec.positional {
496 | hasPositional = true
497 | }
498 | }
499 | if hasPositional && len(cmd.subcommands) > 0 {
500 | return nil, fmt.Errorf("%s cannot have both subcommands and positional arguments", dest)
501 | }
502 |
503 | return &cmd, nil
504 | }
505 |
506 | // Parse processes the given command line option, storing the results in the fields
507 | // of the structs from which NewParser was constructed.
508 | //
509 | // It returns ErrHelp if "--help" is one of the command line args and ErrVersion if
510 | // "--version" is one of the command line args (the latter only applies if the
511 | // destination struct passed to NewParser implements Versioned.)
512 | //
513 | // To respond to --help and --version in the way that MustParse does, see examples
514 | // in the README under "Custom handling of --help and --version".
515 | func (p *Parser) Parse(args []string) error {
516 | err := p.process(args)
517 | if err != nil {
518 | // If -h or --help were specified then make sure help text supercedes other errors
519 | for _, arg := range args {
520 | if arg == "-h" || arg == "--help" {
521 | return ErrHelp
522 | }
523 | if arg == "--" {
524 | break
525 | }
526 | }
527 | }
528 | return err
529 | }
530 |
531 | func (p *Parser) MustParse(args []string) {
532 | err := p.Parse(args)
533 | switch {
534 | case err == ErrHelp:
535 | p.WriteHelpForSubcommand(p.config.Out, p.subcommand...)
536 | p.config.Exit(0)
537 | case err == ErrVersion:
538 | fmt.Fprintln(p.config.Out, p.version)
539 | p.config.Exit(0)
540 | case err != nil:
541 | p.FailSubcommand(err.Error(), p.subcommand...)
542 | }
543 | }
544 |
545 | // process environment vars for the given arguments
546 | func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error {
547 | for _, spec := range specs {
548 | if spec.env == "" {
549 | continue
550 | }
551 |
552 | value, found := os.LookupEnv(spec.env)
553 | if !found {
554 | continue
555 | }
556 |
557 | if spec.cardinality == multiple {
558 | // expect a CSV string in an environment
559 | // variable in the case of multiple values
560 | var values []string
561 | var err error
562 | if len(strings.TrimSpace(value)) > 0 {
563 | values, err = csv.NewReader(strings.NewReader(value)).Read()
564 | if err != nil {
565 | return fmt.Errorf(
566 | "error reading a CSV string from environment variable %s with multiple values: %v",
567 | spec.env,
568 | err,
569 | )
570 | }
571 | }
572 | if err = setSliceOrMap(p.val(spec.dest), values, !spec.separate); err != nil {
573 | return fmt.Errorf(
574 | "error processing environment variable %s with multiple values: %v",
575 | spec.env,
576 | err,
577 | )
578 | }
579 | } else {
580 | if err := scalar.ParseValue(p.val(spec.dest), value); err != nil {
581 | return fmt.Errorf("error processing environment variable %s: %v", spec.env, err)
582 | }
583 | }
584 | wasPresent[spec] = true
585 | }
586 |
587 | return nil
588 | }
589 |
590 | // process goes through arguments one-by-one, parses them, and assigns the result to
591 | // the underlying struct field
592 | func (p *Parser) process(args []string) error {
593 | // track the options we have seen
594 | wasPresent := make(map[*spec]bool)
595 |
596 | // union of specs for the chain of subcommands encountered so far
597 | curCmd := p.cmd
598 | p.subcommand = nil
599 |
600 | // make a copy of the specs because we will add to this list each time we expand a subcommand
601 | specs := make([]*spec, len(curCmd.specs))
602 | copy(specs, curCmd.specs)
603 |
604 | // deal with environment vars
605 | if !p.config.IgnoreEnv {
606 | err := p.captureEnvVars(specs, wasPresent)
607 | if err != nil {
608 | return err
609 | }
610 | }
611 |
612 | // determine if the current command has a version option spec
613 | var hasVersionOption bool
614 | for _, spec := range curCmd.specs {
615 | if spec.long == "version" {
616 | hasVersionOption = true
617 | break
618 | }
619 | }
620 |
621 | // process each string from the command line
622 | var allpositional bool
623 | var positionals []string
624 |
625 | // must use explicit for loop, not range, because we manipulate i inside the loop
626 | for i := 0; i < len(args); i++ {
627 | arg := args[i]
628 | if arg == "--" && !allpositional {
629 | allpositional = true
630 | continue
631 | }
632 |
633 | if !isFlag(arg) || allpositional {
634 | // each subcommand can have either subcommands or positionals, but not both
635 | if len(curCmd.subcommands) == 0 {
636 | positionals = append(positionals, arg)
637 | continue
638 | }
639 |
640 | // if we have a subcommand then make sure it is valid for the current context
641 | subcmd := findSubcommand(curCmd.subcommands, arg)
642 | if subcmd == nil {
643 | return fmt.Errorf("invalid subcommand: %s", arg)
644 | }
645 |
646 | // instantiate the field to point to a new struct
647 | v := p.val(subcmd.dest)
648 | if v.IsNil() {
649 | v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers
650 | }
651 |
652 | // add the new options to the set of allowed options
653 | if p.config.StrictSubcommands {
654 | specs = make([]*spec, len(subcmd.specs))
655 | copy(specs, subcmd.specs)
656 | } else {
657 | specs = append(specs, subcmd.specs...)
658 | }
659 |
660 | // capture environment vars for these new options
661 | if !p.config.IgnoreEnv {
662 | err := p.captureEnvVars(subcmd.specs, wasPresent)
663 | if err != nil {
664 | return err
665 | }
666 | }
667 |
668 | curCmd = subcmd
669 | p.subcommand = append(p.subcommand, arg)
670 | continue
671 | }
672 |
673 | // check for special --help and --version flags
674 | switch arg {
675 | case "-h", "--help":
676 | return ErrHelp
677 | case "--version":
678 | if !hasVersionOption && p.version != "" {
679 | return ErrVersion
680 | }
681 | }
682 |
683 | // check for an equals sign, as in "--foo=bar"
684 | var value string
685 | opt := strings.TrimLeft(arg, "-")
686 | if pos := strings.Index(opt, "="); pos != -1 {
687 | value = opt[pos+1:]
688 | opt = opt[:pos]
689 | }
690 |
691 | // lookup the spec for this option (note that the "specs" slice changes as
692 | // we expand subcommands so it is better not to use a map)
693 | spec := findOption(specs, opt)
694 | if spec == nil || opt == "" {
695 | return fmt.Errorf("unknown argument %s", arg)
696 | }
697 | wasPresent[spec] = true
698 |
699 | // deal with the case of multiple values
700 | if spec.cardinality == multiple {
701 | var values []string
702 | if value == "" {
703 | for i+1 < len(args) && isValue(args[i+1], spec.field.Type, specs) && args[i+1] != "--" {
704 | values = append(values, args[i+1])
705 | i++
706 | if spec.separate {
707 | break
708 | }
709 | }
710 | } else {
711 | values = append(values, value)
712 | }
713 | err := setSliceOrMap(p.val(spec.dest), values, !spec.separate)
714 | if err != nil {
715 | return fmt.Errorf("error processing %s: %v", arg, err)
716 | }
717 | continue
718 | }
719 |
720 | // if it's a flag and it has no value then set the value to true
721 | // use boolean because this takes account of TextUnmarshaler
722 | if spec.cardinality == zero && value == "" {
723 | value = "true"
724 | }
725 |
726 | // if we have something like "--foo" then the value is the next argument
727 | if value == "" {
728 | if i+1 == len(args) {
729 | return fmt.Errorf("missing value for %s", arg)
730 | }
731 | if !isValue(args[i+1], spec.field.Type, specs) {
732 | return fmt.Errorf("missing value for %s", arg)
733 | }
734 | value = args[i+1]
735 | i++
736 | }
737 |
738 | err := scalar.ParseValue(p.val(spec.dest), value)
739 | if err != nil {
740 | return fmt.Errorf("error processing %s: %v", arg, err)
741 | }
742 | }
743 |
744 | // process positionals
745 | for _, spec := range specs {
746 | if !spec.positional {
747 | continue
748 | }
749 | if len(positionals) == 0 {
750 | break
751 | }
752 | wasPresent[spec] = true
753 | if spec.cardinality == multiple {
754 | err := setSliceOrMap(p.val(spec.dest), positionals, true)
755 | if err != nil {
756 | return fmt.Errorf("error processing %s: %v", spec.placeholder, err)
757 | }
758 | positionals = nil
759 | } else {
760 | err := scalar.ParseValue(p.val(spec.dest), positionals[0])
761 | if err != nil {
762 | return fmt.Errorf("error processing %s: %v", spec.placeholder, err)
763 | }
764 | positionals = positionals[1:]
765 | }
766 | }
767 | if len(positionals) > 0 {
768 | return fmt.Errorf("too many positional arguments at '%s'", positionals[0])
769 | }
770 |
771 | // fill in defaults and check that all the required args were provided
772 | for _, spec := range specs {
773 | if wasPresent[spec] {
774 | continue
775 | }
776 |
777 | if spec.required {
778 | if spec.short == "" && spec.long == "" {
779 | msg := fmt.Sprintf("environment variable %s is required", spec.env)
780 | return errors.New(msg)
781 | }
782 |
783 | msg := fmt.Sprintf("%s is required", spec.placeholder)
784 | if spec.env != "" {
785 | msg += " (or environment variable " + spec.env + ")"
786 | }
787 |
788 | return errors.New(msg)
789 | }
790 |
791 | if spec.defaultValue.IsValid() && !p.config.IgnoreDefault {
792 | // One issue here is that if the user now modifies the value then
793 | // the default value stored in the spec will be corrupted. There
794 | // is no general way to "deep-copy" values in Go, and we still
795 | // support the old-style method for specifying defaults as
796 | // Go values assigned directly to the struct field, so we are stuck.
797 | p.val(spec.dest).Set(spec.defaultValue)
798 | }
799 | }
800 |
801 | return nil
802 | }
803 |
804 | // isFlag returns true if a token is a flag such as "-v" or "--user" but not "-" or "--"
805 | func isFlag(s string) bool {
806 | return strings.HasPrefix(s, "-") && strings.TrimLeft(s, "-") != ""
807 | }
808 |
809 | // isValue returns true if a token should be consumed as a value for a flag of type t. This
810 | // is almost always the inverse of isFlag. The one exception is for negative numbers, in which
811 | // case we check the list of active options and return true if its not present there.
812 | func isValue(s string, t reflect.Type, specs []*spec) bool {
813 | switch t.Kind() {
814 | case reflect.Ptr, reflect.Slice:
815 | return isValue(s, t.Elem(), specs)
816 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
817 | v := reflect.New(t)
818 | err := scalar.ParseValue(v, s)
819 | // if value can be parsed and is not an explicit option declared elsewhere, then use it as a value
820 | if err == nil && (!strings.HasPrefix(s, "-") || findOption(specs, strings.TrimPrefix(s, "-")) == nil) {
821 | return true
822 | }
823 | }
824 |
825 | // default case that is used in all cases other than negative numbers: inverse of isFlag
826 | return !isFlag(s)
827 | }
828 |
829 | // val returns a reflect.Value corresponding to the current value for the
830 | // given path
831 | func (p *Parser) val(dest path) reflect.Value {
832 | v := p.roots[dest.root]
833 | for _, field := range dest.fields {
834 | if v.Kind() == reflect.Ptr {
835 | if v.IsNil() {
836 | return reflect.Value{}
837 | }
838 | v = v.Elem()
839 | }
840 |
841 | v = v.FieldByIndex(field.Index)
842 | }
843 | return v
844 | }
845 |
846 | // findOption finds an option from its name, or returns null if no spec is found
847 | func findOption(specs []*spec, name string) *spec {
848 | for _, spec := range specs {
849 | if spec.positional {
850 | continue
851 | }
852 | if spec.long == name || spec.short == name {
853 | return spec
854 | }
855 | }
856 | return nil
857 | }
858 |
859 | // findSubcommand finds a subcommand using its name, or returns null if no subcommand is found
860 | func findSubcommand(cmds []*command, name string) *command {
861 | for _, cmd := range cmds {
862 | if cmd.name == name {
863 | return cmd
864 | }
865 | for _, alias := range cmd.aliases {
866 | if alias == name {
867 | return cmd
868 | }
869 | }
870 | }
871 | return nil
872 | }
873 |
--------------------------------------------------------------------------------
/parse_test.go:
--------------------------------------------------------------------------------
1 | package arg
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "net"
8 | "net/mail"
9 | "net/url"
10 | "os"
11 | "strings"
12 | "testing"
13 | "time"
14 |
15 | "github.com/stretchr/testify/assert"
16 | "github.com/stretchr/testify/require"
17 | )
18 |
19 | func setenv(t *testing.T, name, val string) {
20 | if err := os.Setenv(name, val); err != nil {
21 | t.Error(err)
22 | }
23 | }
24 |
25 | func parse(cmdline string, dest interface{}) error {
26 | _, err := pparse(cmdline, dest)
27 | return err
28 | }
29 |
30 | func pparse(cmdline string, dest interface{}) (*Parser, error) {
31 | return parseWithEnv(Config{}, cmdline, nil, dest)
32 | }
33 |
34 | func parseWithEnv(config Config, cmdline string, env []string, dest interface{}) (*Parser, error) {
35 | p, err := NewParser(config, dest)
36 | if err != nil {
37 | return nil, err
38 | }
39 |
40 | // split the command line
41 | var parts []string
42 | if len(cmdline) > 0 {
43 | parts = strings.Split(cmdline, " ")
44 | }
45 |
46 | // split the environment vars
47 | for _, s := range env {
48 | pos := strings.Index(s, "=")
49 | if pos == -1 {
50 | return nil, fmt.Errorf("missing equals sign in %q", s)
51 | }
52 | err := os.Setenv(s[:pos], s[pos+1:])
53 | if err != nil {
54 | return nil, err
55 | }
56 | }
57 |
58 | // execute the parser
59 | return p, p.Parse(parts)
60 | }
61 |
62 | func TestString(t *testing.T) {
63 | var args struct {
64 | Foo string
65 | Ptr *string
66 | }
67 | err := parse("--foo bar --ptr baz", &args)
68 | require.NoError(t, err)
69 | assert.Equal(t, "bar", args.Foo)
70 | assert.Equal(t, "baz", *args.Ptr)
71 | }
72 |
73 | func TestBool(t *testing.T) {
74 | var args struct {
75 | A bool
76 | B bool
77 | C *bool
78 | D *bool
79 | }
80 | err := parse("--a --c", &args)
81 | require.NoError(t, err)
82 | assert.True(t, args.A)
83 | assert.False(t, args.B)
84 | assert.True(t, *args.C)
85 | assert.Nil(t, args.D)
86 | }
87 |
88 | func TestInt(t *testing.T) {
89 | var args struct {
90 | Foo int
91 | Ptr *int
92 | }
93 | err := parse("--foo 7 --ptr 8", &args)
94 | require.NoError(t, err)
95 | assert.EqualValues(t, 7, args.Foo)
96 | assert.EqualValues(t, 8, *args.Ptr)
97 | }
98 |
99 | func TestHexOctBin(t *testing.T) {
100 | var args struct {
101 | Hex int
102 | Oct int
103 | Bin int
104 | Underscored int
105 | }
106 | err := parse("--hex 0xA --oct 0o10 --bin 0b101 --underscored 123_456", &args)
107 | require.NoError(t, err)
108 | assert.EqualValues(t, 10, args.Hex)
109 | assert.EqualValues(t, 8, args.Oct)
110 | assert.EqualValues(t, 5, args.Bin)
111 | assert.EqualValues(t, 123456, args.Underscored)
112 | }
113 |
114 | func TestNegativeInt(t *testing.T) {
115 | var args struct {
116 | Foo int
117 | }
118 | err := parse("-foo -100", &args)
119 | require.NoError(t, err)
120 | assert.EqualValues(t, args.Foo, -100)
121 | }
122 |
123 | func TestNegativeFloat(t *testing.T) {
124 | var args struct {
125 | Foo float64
126 | }
127 | err := parse("-foo -99", &args)
128 | require.NoError(t, err)
129 | assert.EqualValues(t, args.Foo, -99)
130 | }
131 |
132 | func TestNumericFlag(t *testing.T) {
133 | var args struct {
134 | UseIPv6 bool `arg:"-6"`
135 | Foo int
136 | }
137 | err := parse("-6", &args)
138 | require.NoError(t, err)
139 | assert.EqualValues(t, args.UseIPv6, true)
140 | }
141 |
142 | func TestNumericFlagTakesPrecedence(t *testing.T) {
143 | var args struct {
144 | UseIPv6 bool `arg:"-6"`
145 | Foo int
146 | }
147 | err := parse("-foo -6", &args)
148 | require.Error(t, err)
149 | }
150 |
151 | func TestRepeatedNegativeInts(t *testing.T) {
152 | var args struct {
153 | Ints []int `arg:"--numbers"`
154 | }
155 | err := parse("--numbers -1 -2 -6", &args)
156 | require.NoError(t, err)
157 | assert.EqualValues(t, args.Ints, []int{-1, -2, -6})
158 | }
159 |
160 | func TestRepeatedNegativeFloats(t *testing.T) {
161 | var args struct {
162 | Floats []float32 `arg:"--numbers"`
163 | }
164 | err := parse("--numbers -1 -2 -6", &args)
165 | require.NoError(t, err)
166 | assert.EqualValues(t, args.Floats, []float32{-1, -2, -6})
167 | }
168 |
169 | func TestRepeatedNegativeFloatsThenNumericFlag(t *testing.T) {
170 | var args struct {
171 | Floats []float32 `arg:"--numbers"`
172 | UseIPv6 bool `arg:"-6"`
173 | }
174 | err := parse("--numbers -1 -2 -6", &args)
175 | require.NoError(t, err)
176 | assert.EqualValues(t, args.Floats, []float32{-1, -2})
177 | assert.True(t, args.UseIPv6)
178 | }
179 |
180 | func TestRepeatedNegativeFloatsThenNonexistentFlag(t *testing.T) {
181 | var args struct {
182 | Floats []float32 `arg:"--numbers"`
183 | UseIPv6 bool `arg:"-6"`
184 | }
185 | err := parse("--numbers -1 -2 -n", &args)
186 | require.Error(t, err, "unknown argument -n")
187 | }
188 |
189 | func TestRepeatedNegativeIntsThenFloat(t *testing.T) {
190 | var args struct {
191 | Ints []int `arg:"--numbers"`
192 | }
193 | err := parse("--numbers -1 -2 -0.1", &args)
194 | require.Error(t, err, "unknown argument -0.1")
195 | }
196 |
197 | func TestNegativeIntAndFloatAndTricks(t *testing.T) {
198 | var args struct {
199 | Foo int
200 | Bar float64
201 | N int `arg:"--100"`
202 | }
203 | err := parse("-foo -99 -bar -60.14 -100 -101", &args)
204 | require.NoError(t, err)
205 | assert.EqualValues(t, args.Foo, -99)
206 | assert.EqualValues(t, args.Bar, -60.14)
207 | assert.EqualValues(t, args.N, -101)
208 | }
209 |
210 | func TestUint(t *testing.T) {
211 | var args struct {
212 | Foo uint
213 | Ptr *uint
214 | }
215 | err := parse("--foo 7 --ptr 8", &args)
216 | require.NoError(t, err)
217 | assert.EqualValues(t, 7, args.Foo)
218 | assert.EqualValues(t, 8, *args.Ptr)
219 | }
220 |
221 | func TestFloat(t *testing.T) {
222 | var args struct {
223 | Foo float32
224 | Ptr *float32
225 | }
226 | err := parse("--foo 3.4 --ptr 3.5", &args)
227 | require.NoError(t, err)
228 | assert.EqualValues(t, 3.4, args.Foo)
229 | assert.EqualValues(t, 3.5, *args.Ptr)
230 | }
231 |
232 | func TestDuration(t *testing.T) {
233 | var args struct {
234 | Foo time.Duration
235 | Ptr *time.Duration
236 | }
237 | err := parse("--foo 3ms --ptr 4ms", &args)
238 | require.NoError(t, err)
239 | assert.Equal(t, 3*time.Millisecond, args.Foo)
240 | assert.Equal(t, 4*time.Millisecond, *args.Ptr)
241 | }
242 |
243 | func TestInvalidDuration(t *testing.T) {
244 | var args struct {
245 | Foo time.Duration
246 | }
247 | err := parse("--foo xxx", &args)
248 | require.Error(t, err)
249 | }
250 |
251 | func TestIntPtr(t *testing.T) {
252 | var args struct {
253 | Foo *int
254 | }
255 | err := parse("--foo 123", &args)
256 | require.NoError(t, err)
257 | require.NotNil(t, args.Foo)
258 | assert.Equal(t, 123, *args.Foo)
259 | }
260 |
261 | func TestIntPtrNotPresent(t *testing.T) {
262 | var args struct {
263 | Foo *int
264 | }
265 | err := parse("", &args)
266 | require.NoError(t, err)
267 | assert.Nil(t, args.Foo)
268 | }
269 |
270 | func TestMixed(t *testing.T) {
271 | var args struct {
272 | Foo string `arg:"-f"`
273 | Bar int
274 | Baz uint `arg:"positional"`
275 | Ham bool
276 | Spam float32
277 | }
278 | args.Bar = 3
279 | err := parse("123 -spam=1.2 -ham -f xyz", &args)
280 | require.NoError(t, err)
281 | assert.Equal(t, "xyz", args.Foo)
282 | assert.Equal(t, 3, args.Bar)
283 | assert.Equal(t, uint(123), args.Baz)
284 | assert.Equal(t, true, args.Ham)
285 | assert.EqualValues(t, 1.2, args.Spam)
286 | }
287 |
288 | func TestRequired(t *testing.T) {
289 | var args struct {
290 | Foo string `arg:"required"`
291 | }
292 | err := parse("", &args)
293 | require.Error(t, err, "--foo is required")
294 | }
295 |
296 | func TestRequiredWithEnv(t *testing.T) {
297 | var args struct {
298 | Foo string `arg:"required,env:FOO"`
299 | }
300 | err := parse("", &args)
301 | require.Error(t, err, "--foo is required (or environment variable FOO)")
302 | }
303 |
304 | func TestRequiredWithEnvOnly(t *testing.T) {
305 | var args struct {
306 | Foo string `arg:"required,--,-,env:FOO"`
307 | }
308 | _, err := parseWithEnv(Config{}, "", []string{}, &args)
309 | require.Error(t, err, "environment variable FOO is required")
310 | }
311 |
312 | func TestShortFlag(t *testing.T) {
313 | var args struct {
314 | Foo string `arg:"-f"`
315 | }
316 |
317 | err := parse("-f xyz", &args)
318 | require.NoError(t, err)
319 | assert.Equal(t, "xyz", args.Foo)
320 |
321 | err = parse("-foo xyz", &args)
322 | require.NoError(t, err)
323 | assert.Equal(t, "xyz", args.Foo)
324 |
325 | err = parse("--foo xyz", &args)
326 | require.NoError(t, err)
327 | assert.Equal(t, "xyz", args.Foo)
328 | }
329 |
330 | func TestInvalidShortFlag(t *testing.T) {
331 | var args struct {
332 | Foo string `arg:"-foo"`
333 | }
334 | err := parse("", &args)
335 | assert.Error(t, err)
336 | }
337 |
338 | func TestLongFlag(t *testing.T) {
339 | var args struct {
340 | Foo string `arg:"--abc"`
341 | }
342 |
343 | err := parse("-abc xyz", &args)
344 | require.NoError(t, err)
345 | assert.Equal(t, "xyz", args.Foo)
346 |
347 | err = parse("--abc xyz", &args)
348 | require.NoError(t, err)
349 | assert.Equal(t, "xyz", args.Foo)
350 | }
351 |
352 | func TestSlice(t *testing.T) {
353 | var args struct {
354 | Strings []string
355 | }
356 | err := parse("--strings a b c", &args)
357 | require.NoError(t, err)
358 | assert.Equal(t, []string{"a", "b", "c"}, args.Strings)
359 | }
360 | func TestSliceOfBools(t *testing.T) {
361 | var args struct {
362 | B []bool
363 | }
364 |
365 | err := parse("--b true false true", &args)
366 | require.NoError(t, err)
367 | assert.Equal(t, []bool{true, false, true}, args.B)
368 | }
369 |
370 | func TestMap(t *testing.T) {
371 | var args struct {
372 | Values map[string]int
373 | }
374 | err := parse("--values a=1 b=2 c=3", &args)
375 | require.NoError(t, err)
376 | assert.Len(t, args.Values, 3)
377 | assert.Equal(t, 1, args.Values["a"])
378 | assert.Equal(t, 2, args.Values["b"])
379 | assert.Equal(t, 3, args.Values["c"])
380 | }
381 |
382 | func TestMapPositional(t *testing.T) {
383 | var args struct {
384 | Values map[string]int `arg:"positional"`
385 | }
386 | err := parse("a=1 b=2 c=3", &args)
387 | require.NoError(t, err)
388 | assert.Len(t, args.Values, 3)
389 | assert.Equal(t, 1, args.Values["a"])
390 | assert.Equal(t, 2, args.Values["b"])
391 | assert.Equal(t, 3, args.Values["c"])
392 | }
393 |
394 | func TestMapWithSeparate(t *testing.T) {
395 | var args struct {
396 | Values map[string]int `arg:"separate"`
397 | }
398 | err := parse("--values a=1 --values b=2 --values c=3", &args)
399 | require.NoError(t, err)
400 | assert.Len(t, args.Values, 3)
401 | assert.Equal(t, 1, args.Values["a"])
402 | assert.Equal(t, 2, args.Values["b"])
403 | assert.Equal(t, 3, args.Values["c"])
404 | }
405 |
406 | func TestPlaceholder(t *testing.T) {
407 | var args struct {
408 | Input string `arg:"positional" placeholder:"SRC"`
409 | Output []string `arg:"positional" placeholder:"DST"`
410 | Optimize int `arg:"-O" placeholder:"LEVEL"`
411 | MaxJobs int `arg:"-j" placeholder:"N"`
412 | }
413 | err := parse("-O 5 --maxjobs 2 src dest1 dest2", &args)
414 | assert.NoError(t, err)
415 | }
416 |
417 | func TestNoLongName(t *testing.T) {
418 | var args struct {
419 | ShortOnly string `arg:"-s,--"`
420 | EnvOnly string `arg:"--,env"`
421 | }
422 | setenv(t, "ENVONLY", "TestVal")
423 | err := parse("-s TestVal2", &args)
424 | assert.NoError(t, err)
425 | assert.Equal(t, "TestVal", args.EnvOnly)
426 | assert.Equal(t, "TestVal2", args.ShortOnly)
427 | }
428 |
429 | func TestCaseSensitive(t *testing.T) {
430 | var args struct {
431 | Lower bool `arg:"-v"`
432 | Upper bool `arg:"-V"`
433 | }
434 |
435 | err := parse("-v", &args)
436 | require.NoError(t, err)
437 | assert.True(t, args.Lower)
438 | assert.False(t, args.Upper)
439 | }
440 |
441 | func TestCaseSensitive2(t *testing.T) {
442 | var args struct {
443 | Lower bool `arg:"-v"`
444 | Upper bool `arg:"-V"`
445 | }
446 |
447 | err := parse("-V", &args)
448 | require.NoError(t, err)
449 | assert.False(t, args.Lower)
450 | assert.True(t, args.Upper)
451 | }
452 |
453 | func TestPositional(t *testing.T) {
454 | var args struct {
455 | Input string `arg:"positional"`
456 | Output string `arg:"positional"`
457 | }
458 | err := parse("foo", &args)
459 | require.NoError(t, err)
460 | assert.Equal(t, "foo", args.Input)
461 | assert.Equal(t, "", args.Output)
462 | }
463 |
464 | func TestPositionalPointer(t *testing.T) {
465 | var args struct {
466 | Input string `arg:"positional"`
467 | Output []*string `arg:"positional"`
468 | }
469 | err := parse("foo bar baz", &args)
470 | require.NoError(t, err)
471 | assert.Equal(t, "foo", args.Input)
472 | bar := "bar"
473 | baz := "baz"
474 | assert.Equal(t, []*string{&bar, &baz}, args.Output)
475 | }
476 |
477 | func TestRequiredPositional(t *testing.T) {
478 | var args struct {
479 | Input string `arg:"positional"`
480 | Output string `arg:"positional,required"`
481 | }
482 | err := parse("foo", &args)
483 | assert.Error(t, err)
484 | }
485 |
486 | func TestRequiredPositionalMultiple(t *testing.T) {
487 | var args struct {
488 | Input string `arg:"positional"`
489 | Multiple []string `arg:"positional,required"`
490 | }
491 | err := parse("foo", &args)
492 | assert.Error(t, err)
493 | }
494 |
495 | func TestTooManyPositional(t *testing.T) {
496 | var args struct {
497 | Input string `arg:"positional"`
498 | Output string `arg:"positional"`
499 | }
500 | err := parse("foo bar baz", &args)
501 | assert.Error(t, err)
502 | }
503 |
504 | func TestMultiple(t *testing.T) {
505 | var args struct {
506 | Foo []int
507 | Bar []string
508 | }
509 | err := parse("--foo 1 2 3 --bar x y z", &args)
510 | require.NoError(t, err)
511 | assert.Equal(t, []int{1, 2, 3}, args.Foo)
512 | assert.Equal(t, []string{"x", "y", "z"}, args.Bar)
513 | }
514 |
515 | func TestMultiplePositionals(t *testing.T) {
516 | var args struct {
517 | Input string `arg:"positional"`
518 | Multiple []string `arg:"positional,required"`
519 | }
520 | err := parse("foo a b c", &args)
521 | assert.NoError(t, err)
522 | assert.Equal(t, "foo", args.Input)
523 | assert.Equal(t, []string{"a", "b", "c"}, args.Multiple)
524 | }
525 |
526 | func TestMultipleWithEq(t *testing.T) {
527 | var args struct {
528 | Foo []int
529 | Bar []string
530 | }
531 | err := parse("--foo 1 2 3 --bar=x", &args)
532 | require.NoError(t, err)
533 | assert.Equal(t, []int{1, 2, 3}, args.Foo)
534 | assert.Equal(t, []string{"x"}, args.Bar)
535 | }
536 |
537 | func TestMultipleWithDefault(t *testing.T) {
538 | var args struct {
539 | Foo []int
540 | Bar []string
541 | }
542 | args.Foo = []int{42}
543 | args.Bar = []string{"foo"}
544 | err := parse("--foo 1 2 3 --bar x y z", &args)
545 | require.NoError(t, err)
546 | assert.Equal(t, []int{1, 2, 3}, args.Foo)
547 | assert.Equal(t, []string{"x", "y", "z"}, args.Bar)
548 | }
549 |
550 | func TestExemptField(t *testing.T) {
551 | var args struct {
552 | Foo string
553 | Bar interface{} `arg:"-"`
554 | }
555 | err := parse("--foo xyz", &args)
556 | require.NoError(t, err)
557 | assert.Equal(t, "xyz", args.Foo)
558 | }
559 |
560 | func TestUnknownField(t *testing.T) {
561 | var args struct {
562 | Foo string
563 | }
564 | err := parse("--bar xyz", &args)
565 | assert.Error(t, err)
566 | }
567 |
568 | func TestMissingRequired(t *testing.T) {
569 | var args struct {
570 | Foo string `arg:"required"`
571 | X []string `arg:"positional"`
572 | }
573 | err := parse("x", &args)
574 | assert.Error(t, err)
575 | }
576 |
577 | func TestNonsenseKey(t *testing.T) {
578 | var args struct {
579 | X []string `arg:"positional, nonsense"`
580 | }
581 | err := parse("x", &args)
582 | assert.Error(t, err)
583 | }
584 |
585 | func TestMissingValueAtEnd(t *testing.T) {
586 | var args struct {
587 | Foo string
588 | }
589 | err := parse("--foo", &args)
590 | assert.Error(t, err)
591 | }
592 |
593 | func TestMissingValueInMiddle(t *testing.T) {
594 | var args struct {
595 | Foo string
596 | Bar string
597 | }
598 | err := parse("--foo --bar=abc", &args)
599 | assert.Error(t, err)
600 | }
601 |
602 | func TestInvalidInt(t *testing.T) {
603 | var args struct {
604 | Foo int
605 | }
606 | err := parse("--foo=xyz", &args)
607 | assert.Error(t, err)
608 | }
609 |
610 | func TestInvalidUint(t *testing.T) {
611 | var args struct {
612 | Foo uint
613 | }
614 | err := parse("--foo=xyz", &args)
615 | assert.Error(t, err)
616 | }
617 |
618 | func TestInvalidFloat(t *testing.T) {
619 | var args struct {
620 | Foo float64
621 | }
622 | err := parse("--foo xyz", &args)
623 | require.Error(t, err)
624 | }
625 |
626 | func TestInvalidBool(t *testing.T) {
627 | var args struct {
628 | Foo bool
629 | }
630 | err := parse("--foo=xyz", &args)
631 | require.Error(t, err)
632 | }
633 |
634 | func TestInvalidIntSlice(t *testing.T) {
635 | var args struct {
636 | Foo []int
637 | }
638 | err := parse("--foo 1 2 xyz", &args)
639 | require.Error(t, err)
640 | }
641 |
642 | func TestInvalidPositional(t *testing.T) {
643 | var args struct {
644 | Foo int `arg:"positional"`
645 | }
646 | err := parse("xyz", &args)
647 | require.Error(t, err)
648 | }
649 |
650 | func TestInvalidPositionalSlice(t *testing.T) {
651 | var args struct {
652 | Foo []int `arg:"positional"`
653 | }
654 | err := parse("1 2 xyz", &args)
655 | require.Error(t, err)
656 | }
657 |
658 | func TestNoMoreOptions(t *testing.T) {
659 | var args struct {
660 | Foo string
661 | Bar []string `arg:"positional"`
662 | }
663 | err := parse("abc -- --foo xyz", &args)
664 | require.NoError(t, err)
665 | assert.Equal(t, "", args.Foo)
666 | assert.Equal(t, []string{"abc", "--foo", "xyz"}, args.Bar)
667 | }
668 |
669 | func TestNoMoreOptionsBeforeHelp(t *testing.T) {
670 | var args struct {
671 | Foo int
672 | }
673 | err := parse("not_an_integer -- --help", &args)
674 | assert.NotEqual(t, ErrHelp, err)
675 | }
676 |
677 | func TestNoMoreOptionsTwice(t *testing.T) {
678 | var args struct {
679 | X []string `arg:"positional"`
680 | }
681 | err := parse("-- --", &args)
682 | require.NoError(t, err)
683 | assert.Equal(t, []string{"--"}, args.X)
684 | }
685 |
686 | func TestHelpFlag(t *testing.T) {
687 | var args struct {
688 | Foo string
689 | Bar interface{} `arg:"-"`
690 | }
691 | err := parse("--help", &args)
692 | assert.Equal(t, ErrHelp, err)
693 | }
694 |
695 | func TestPanicOnNonPointer(t *testing.T) {
696 | var args struct{}
697 | assert.Panics(t, func() {
698 | _ = parse("", args)
699 | })
700 | }
701 |
702 | func TestErrorOnNonStruct(t *testing.T) {
703 | var args string
704 | err := parse("", &args)
705 | assert.Error(t, err)
706 | }
707 |
708 | func TestUnsupportedType(t *testing.T) {
709 | var args struct {
710 | Foo interface{}
711 | }
712 | err := parse("--foo", &args)
713 | assert.Error(t, err)
714 | }
715 |
716 | func TestUnsupportedSliceElement(t *testing.T) {
717 | var args struct {
718 | Foo []interface{}
719 | }
720 | err := parse("--foo 3", &args)
721 | assert.Error(t, err)
722 | }
723 |
724 | func TestUnsupportedSliceElementMissingValue(t *testing.T) {
725 | var args struct {
726 | Foo []interface{}
727 | }
728 | err := parse("--foo", &args)
729 | assert.Error(t, err)
730 | }
731 |
732 | func TestUnknownTag(t *testing.T) {
733 | var args struct {
734 | Foo string `arg:"this_is_not_valid"`
735 | }
736 | err := parse("--foo xyz", &args)
737 | assert.Error(t, err)
738 | }
739 |
740 | func TestParse(t *testing.T) {
741 | var args struct {
742 | Foo string
743 | }
744 | os.Args = []string{"example", "--foo", "bar"}
745 | err := Parse(&args)
746 | require.NoError(t, err)
747 | assert.Equal(t, "bar", args.Foo)
748 | }
749 |
750 | func TestParseError(t *testing.T) {
751 | var args struct {
752 | Foo string `arg:"this_is_not_valid"`
753 | }
754 | os.Args = []string{"example", "--bar"}
755 | err := Parse(&args)
756 | assert.Error(t, err)
757 | }
758 |
759 | func TestMustParse(t *testing.T) {
760 | var args struct {
761 | Foo string
762 | }
763 | os.Args = []string{"example", "--foo", "bar"}
764 | parser := MustParse(&args)
765 | assert.Equal(t, "bar", args.Foo)
766 | assert.NotNil(t, parser)
767 | }
768 |
769 | func TestMustParseError(t *testing.T) {
770 | var args struct {
771 | Foo []string `default:""`
772 | }
773 | var exitCode int
774 | var stdout bytes.Buffer
775 | mustParseExit = func(code int) { exitCode = code }
776 | mustParseOut = &stdout
777 | os.Args = []string{"example"}
778 | parser := MustParse(&args)
779 | assert.Nil(t, parser)
780 | assert.Equal(t, 2, exitCode)
781 | assert.Contains(t, stdout.String(), "default values are not supported for slice or map fields")
782 | }
783 |
784 | func TestEnvironmentVariable(t *testing.T) {
785 | var args struct {
786 | Foo string `arg:"env"`
787 | }
788 | _, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
789 | require.NoError(t, err)
790 | assert.Equal(t, "bar", args.Foo)
791 | }
792 |
793 | func TestEnvironmentVariableNotPresent(t *testing.T) {
794 | var args struct {
795 | NotPresent string `arg:"env"`
796 | }
797 | _, err := parseWithEnv(Config{}, "", nil, &args)
798 | require.NoError(t, err)
799 | assert.Equal(t, "", args.NotPresent)
800 | }
801 |
802 | func TestEnvironmentVariableOverrideName(t *testing.T) {
803 | var args struct {
804 | Foo string `arg:"env:BAZ"`
805 | }
806 | _, err := parseWithEnv(Config{}, "", []string{"BAZ=bar"}, &args)
807 | require.NoError(t, err)
808 | assert.Equal(t, "bar", args.Foo)
809 | }
810 |
811 | func TestEnvironmentVariableOverrideArgument(t *testing.T) {
812 | var args struct {
813 | Foo string `arg:"env"`
814 | }
815 | _, err := parseWithEnv(Config{}, "--foo zzz", []string{"FOO=bar"}, &args)
816 | require.NoError(t, err)
817 | assert.Equal(t, "zzz", args.Foo)
818 | }
819 |
820 | func TestEnvironmentVariableError(t *testing.T) {
821 | var args struct {
822 | Foo int `arg:"env"`
823 | }
824 | _, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
825 | assert.Error(t, err)
826 | }
827 |
828 | func TestEnvironmentVariableRequired(t *testing.T) {
829 | var args struct {
830 | Foo string `arg:"env,required"`
831 | }
832 | _, err := parseWithEnv(Config{}, "", []string{"FOO=bar"}, &args)
833 | require.NoError(t, err)
834 | assert.Equal(t, "bar", args.Foo)
835 | }
836 |
837 | func TestEnvironmentVariableSliceArgumentString(t *testing.T) {
838 | var args struct {
839 | Foo []string `arg:"env"`
840 | }
841 | _, err := parseWithEnv(Config{}, "", []string{`FOO=bar,"baz, qux"`}, &args)
842 | require.NoError(t, err)
843 | assert.Equal(t, []string{"bar", "baz, qux"}, args.Foo)
844 | }
845 |
846 | func TestEnvironmentVariableSliceEmpty(t *testing.T) {
847 | var args struct {
848 | Foo []string `arg:"env"`
849 | }
850 | _, err := parseWithEnv(Config{}, "", []string{`FOO=`}, &args)
851 | require.NoError(t, err)
852 | assert.Len(t, args.Foo, 0)
853 | }
854 |
855 | func TestEnvironmentVariableSliceArgumentInteger(t *testing.T) {
856 | var args struct {
857 | Foo []int `arg:"env"`
858 | }
859 | _, err := parseWithEnv(Config{}, "", []string{`FOO=1,99`}, &args)
860 | require.NoError(t, err)
861 | assert.Equal(t, []int{1, 99}, args.Foo)
862 | }
863 |
864 | func TestEnvironmentVariableSliceArgumentFloat(t *testing.T) {
865 | var args struct {
866 | Foo []float32 `arg:"env"`
867 | }
868 | _, err := parseWithEnv(Config{}, "", []string{`FOO=1.1,99.9`}, &args)
869 | require.NoError(t, err)
870 | assert.Equal(t, []float32{1.1, 99.9}, args.Foo)
871 | }
872 |
873 | func TestEnvironmentVariableSliceArgumentBool(t *testing.T) {
874 | var args struct {
875 | Foo []bool `arg:"env"`
876 | }
877 | _, err := parseWithEnv(Config{}, "", []string{`FOO=true,false,0,1`}, &args)
878 | require.NoError(t, err)
879 | assert.Equal(t, []bool{true, false, false, true}, args.Foo)
880 | }
881 |
882 | func TestEnvironmentVariableSliceArgumentWrongCsv(t *testing.T) {
883 | var args struct {
884 | Foo []int `arg:"env"`
885 | }
886 | _, err := parseWithEnv(Config{}, "", []string{`FOO=1,99\"`}, &args)
887 | assert.Error(t, err)
888 | }
889 |
890 | func TestEnvironmentVariableSliceArgumentWrongType(t *testing.T) {
891 | var args struct {
892 | Foo []bool `arg:"env"`
893 | }
894 | _, err := parseWithEnv(Config{}, "", []string{`FOO=one,two`}, &args)
895 | assert.Error(t, err)
896 | }
897 |
898 | func TestEnvironmentVariableMap(t *testing.T) {
899 | var args struct {
900 | Foo map[int]string `arg:"env"`
901 | }
902 | _, err := parseWithEnv(Config{}, "", []string{`FOO=1=one,99=ninetynine`}, &args)
903 | require.NoError(t, err)
904 | assert.Len(t, args.Foo, 2)
905 | assert.Equal(t, "one", args.Foo[1])
906 | assert.Equal(t, "ninetynine", args.Foo[99])
907 | }
908 |
909 | func TestEnvironmentVariableEmptyMap(t *testing.T) {
910 | var args struct {
911 | Foo map[int]string `arg:"env"`
912 | }
913 | _, err := parseWithEnv(Config{}, "", []string{`FOO=`}, &args)
914 | require.NoError(t, err)
915 | assert.Len(t, args.Foo, 0)
916 | }
917 |
918 | func TestEnvironmentVariableWithPrefix(t *testing.T) {
919 | var args struct {
920 | Foo string `arg:"env"`
921 | }
922 |
923 | _, err := parseWithEnv(Config{EnvPrefix: "MYAPP_"}, "", []string{"MYAPP_FOO=bar"}, &args)
924 | require.NoError(t, err)
925 | assert.Equal(t, "bar", args.Foo)
926 | }
927 |
928 | func TestEnvironmentVariableIgnored(t *testing.T) {
929 | var args struct {
930 | Foo string `arg:"env"`
931 | }
932 | setenv(t, "FOO", "abc")
933 |
934 | p, err := NewParser(Config{IgnoreEnv: true}, &args)
935 | require.NoError(t, err)
936 |
937 | err = p.Parse(nil)
938 | assert.NoError(t, err)
939 | assert.Equal(t, "", args.Foo)
940 | }
941 |
942 | func TestDefaultValuesIgnored(t *testing.T) {
943 | var args struct {
944 | Foo string `default:"bad"`
945 | }
946 |
947 | p, err := NewParser(Config{IgnoreDefault: true}, &args)
948 | require.NoError(t, err)
949 |
950 | err = p.Parse(nil)
951 | assert.NoError(t, err)
952 | assert.Equal(t, "", args.Foo)
953 | }
954 |
955 | func TestRequiredEnvironmentOnlyVariableIsMissing(t *testing.T) {
956 | var args struct {
957 | Foo string `arg:"required,--,env:FOO"`
958 | }
959 |
960 | _, err := parseWithEnv(Config{}, "", []string{""}, &args)
961 | assert.Error(t, err)
962 | }
963 |
964 | func TestOptionalEnvironmentOnlyVariable(t *testing.T) {
965 | var args struct {
966 | Foo string `arg:"env:FOO"`
967 | }
968 |
969 | _, err := parseWithEnv(Config{}, "", []string{}, &args)
970 | assert.NoError(t, err)
971 | }
972 |
973 | func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
974 | var args struct {
975 | Sub *struct {
976 | Foo string `arg:"env"`
977 | } `arg:"subcommand"`
978 | }
979 | setenv(t, "FOO", "abc")
980 |
981 | p, err := NewParser(Config{IgnoreEnv: true}, &args)
982 | require.NoError(t, err)
983 |
984 | err = p.Parse([]string{"sub"})
985 | require.NoError(t, err)
986 | require.NotNil(t, args.Sub)
987 | assert.Equal(t, "", args.Sub.Foo)
988 | }
989 |
990 | func TestParserMustParseEmptyArgs(t *testing.T) {
991 | // this mirrors TestEmptyArgs
992 | p, err := NewParser(Config{}, &struct{}{})
993 | require.NoError(t, err)
994 | assert.NotNil(t, p)
995 | p.MustParse(nil)
996 | }
997 |
998 | func TestParserMustParse(t *testing.T) {
999 | tests := []struct {
1000 | name string
1001 | args versioned
1002 | cmdLine []string
1003 | code int
1004 | output string
1005 | }{
1006 | {name: "help", args: struct{}{}, cmdLine: []string{"--help"}, code: 0, output: "display this help and exit"},
1007 | {name: "version", args: versioned{}, cmdLine: []string{"--version"}, code: 0, output: "example 3.2.1"},
1008 | {name: "invalid", args: struct{}{}, cmdLine: []string{"invalid"}, code: 2, output: ""},
1009 | }
1010 |
1011 | for _, tt := range tests {
1012 | tt := tt
1013 | t.Run(tt.name, func(t *testing.T) {
1014 | var exitCode int
1015 | var stdout bytes.Buffer
1016 | exit := func(code int) { exitCode = code }
1017 |
1018 | p, err := NewParser(Config{Exit: exit, Out: &stdout}, &tt.args)
1019 | require.NoError(t, err)
1020 | assert.NotNil(t, p)
1021 |
1022 | p.MustParse(tt.cmdLine)
1023 | assert.NotNil(t, exitCode)
1024 | assert.Equal(t, tt.code, exitCode)
1025 | assert.Contains(t, stdout.String(), tt.output)
1026 | })
1027 | }
1028 | }
1029 |
1030 | type textUnmarshaler struct {
1031 | val int
1032 | }
1033 |
1034 | func (f *textUnmarshaler) UnmarshalText(b []byte) error {
1035 | f.val = len(b)
1036 | return nil
1037 | }
1038 |
1039 | func TestTextUnmarshaler(t *testing.T) {
1040 | // fields that implement TextUnmarshaler should be parsed using that interface
1041 | var args struct {
1042 | Foo textUnmarshaler
1043 | }
1044 | err := parse("--foo abc", &args)
1045 | require.NoError(t, err)
1046 | assert.Equal(t, 3, args.Foo.val)
1047 | }
1048 |
1049 | func TestPtrToTextUnmarshaler(t *testing.T) {
1050 | // fields that implement TextUnmarshaler should be parsed using that interface
1051 | var args struct {
1052 | Foo *textUnmarshaler
1053 | }
1054 | err := parse("--foo abc", &args)
1055 | require.NoError(t, err)
1056 | assert.Equal(t, 3, args.Foo.val)
1057 | }
1058 |
1059 | func TestRepeatedTextUnmarshaler(t *testing.T) {
1060 | // fields that implement TextUnmarshaler should be parsed using that interface
1061 | var args struct {
1062 | Foo []textUnmarshaler
1063 | }
1064 | err := parse("--foo abc d ef", &args)
1065 | require.NoError(t, err)
1066 | require.Len(t, args.Foo, 3)
1067 | assert.Equal(t, 3, args.Foo[0].val)
1068 | assert.Equal(t, 1, args.Foo[1].val)
1069 | assert.Equal(t, 2, args.Foo[2].val)
1070 | }
1071 |
1072 | func TestRepeatedPtrToTextUnmarshaler(t *testing.T) {
1073 | // fields that implement TextUnmarshaler should be parsed using that interface
1074 | var args struct {
1075 | Foo []*textUnmarshaler
1076 | }
1077 | err := parse("--foo abc d ef", &args)
1078 | require.NoError(t, err)
1079 | require.Len(t, args.Foo, 3)
1080 | assert.Equal(t, 3, args.Foo[0].val)
1081 | assert.Equal(t, 1, args.Foo[1].val)
1082 | assert.Equal(t, 2, args.Foo[2].val)
1083 | }
1084 |
1085 | func TestPositionalTextUnmarshaler(t *testing.T) {
1086 | // fields that implement TextUnmarshaler should be parsed using that interface
1087 | var args struct {
1088 | Foo []textUnmarshaler `arg:"positional"`
1089 | }
1090 | err := parse("abc d ef", &args)
1091 | require.NoError(t, err)
1092 | require.Len(t, args.Foo, 3)
1093 | assert.Equal(t, 3, args.Foo[0].val)
1094 | assert.Equal(t, 1, args.Foo[1].val)
1095 | assert.Equal(t, 2, args.Foo[2].val)
1096 | }
1097 |
1098 | func TestPositionalPtrToTextUnmarshaler(t *testing.T) {
1099 | // fields that implement TextUnmarshaler should be parsed using that interface
1100 | var args struct {
1101 | Foo []*textUnmarshaler `arg:"positional"`
1102 | }
1103 | err := parse("abc d ef", &args)
1104 | require.NoError(t, err)
1105 | require.Len(t, args.Foo, 3)
1106 | assert.Equal(t, 3, args.Foo[0].val)
1107 | assert.Equal(t, 1, args.Foo[1].val)
1108 | assert.Equal(t, 2, args.Foo[2].val)
1109 | }
1110 |
1111 | type boolUnmarshaler bool
1112 |
1113 | func (p *boolUnmarshaler) UnmarshalText(b []byte) error {
1114 | *p = len(b)%2 == 0
1115 | return nil
1116 | }
1117 |
1118 | func TestBoolUnmarhsaler(t *testing.T) {
1119 | // test that a bool type that implements TextUnmarshaler is
1120 | // handled as a TextUnmarshaler not as a bool
1121 | var args struct {
1122 | Foo *boolUnmarshaler
1123 | }
1124 | err := parse("--foo ab", &args)
1125 | require.NoError(t, err)
1126 | assert.EqualValues(t, true, *args.Foo)
1127 | }
1128 |
1129 | type sliceUnmarshaler []int
1130 |
1131 | func (p *sliceUnmarshaler) UnmarshalText(b []byte) error {
1132 | *p = sliceUnmarshaler{len(b)}
1133 | return nil
1134 | }
1135 |
1136 | func TestSliceUnmarhsaler(t *testing.T) {
1137 | // test that a slice type that implements TextUnmarshaler is
1138 | // handled as a TextUnmarshaler not as a slice
1139 | var args struct {
1140 | Foo *sliceUnmarshaler
1141 | Bar string `arg:"positional"`
1142 | }
1143 | err := parse("--foo abcde xyz", &args)
1144 | require.NoError(t, err)
1145 | require.Len(t, *args.Foo, 1)
1146 | assert.EqualValues(t, 5, (*args.Foo)[0])
1147 | assert.Equal(t, "xyz", args.Bar)
1148 | }
1149 |
1150 | func TestIP(t *testing.T) {
1151 | var args struct {
1152 | Host net.IP
1153 | }
1154 | err := parse("--host 192.168.0.1", &args)
1155 | require.NoError(t, err)
1156 | assert.Equal(t, "192.168.0.1", args.Host.String())
1157 | }
1158 |
1159 | func TestPtrToIP(t *testing.T) {
1160 | var args struct {
1161 | Host *net.IP
1162 | }
1163 | err := parse("--host 192.168.0.1", &args)
1164 | require.NoError(t, err)
1165 | assert.Equal(t, "192.168.0.1", args.Host.String())
1166 | }
1167 |
1168 | func TestURL(t *testing.T) {
1169 | var args struct {
1170 | URL url.URL
1171 | }
1172 | err := parse("--url https://example.com/get?item=xyz", &args)
1173 | require.NoError(t, err)
1174 | assert.Equal(t, "https://example.com/get?item=xyz", args.URL.String())
1175 | }
1176 |
1177 | func TestPtrToURL(t *testing.T) {
1178 | var args struct {
1179 | URL *url.URL
1180 | }
1181 | err := parse("--url http://example.com/#xyz", &args)
1182 | require.NoError(t, err)
1183 | assert.Equal(t, "http://example.com/#xyz", args.URL.String())
1184 | }
1185 |
1186 | func TestIPSlice(t *testing.T) {
1187 | var args struct {
1188 | Host []net.IP
1189 | }
1190 | err := parse("--host 192.168.0.1 127.0.0.1", &args)
1191 | require.NoError(t, err)
1192 | require.Len(t, args.Host, 2)
1193 | assert.Equal(t, "192.168.0.1", args.Host[0].String())
1194 | assert.Equal(t, "127.0.0.1", args.Host[1].String())
1195 | }
1196 |
1197 | func TestInvalidIPAddress(t *testing.T) {
1198 | var args struct {
1199 | Host net.IP
1200 | }
1201 | err := parse("--host xxx", &args)
1202 | assert.Error(t, err)
1203 | }
1204 |
1205 | func TestMAC(t *testing.T) {
1206 | var args struct {
1207 | Host net.HardwareAddr
1208 | }
1209 | err := parse("--host 0123.4567.89ab", &args)
1210 | require.NoError(t, err)
1211 | assert.Equal(t, "01:23:45:67:89:ab", args.Host.String())
1212 | }
1213 |
1214 | func TestInvalidMac(t *testing.T) {
1215 | var args struct {
1216 | Host net.HardwareAddr
1217 | }
1218 | err := parse("--host xxx", &args)
1219 | assert.Error(t, err)
1220 | }
1221 |
1222 | func TestMailAddr(t *testing.T) {
1223 | var args struct {
1224 | Recipient mail.Address
1225 | }
1226 | err := parse("--recipient foo@example.com", &args)
1227 | require.NoError(t, err)
1228 | assert.Equal(t, "", args.Recipient.String())
1229 | }
1230 |
1231 | func TestInvalidMailAddr(t *testing.T) {
1232 | var args struct {
1233 | Recipient mail.Address
1234 | }
1235 | err := parse("--recipient xxx", &args)
1236 | assert.Error(t, err)
1237 | }
1238 |
1239 | type A struct {
1240 | X string
1241 | }
1242 |
1243 | type B struct {
1244 | Y int
1245 | }
1246 |
1247 | func TestEmbedded(t *testing.T) {
1248 | var args struct {
1249 | A
1250 | B
1251 | Z bool
1252 | }
1253 | err := parse("--x=hello --y=321 --z", &args)
1254 | require.NoError(t, err)
1255 | assert.Equal(t, "hello", args.X)
1256 | assert.Equal(t, 321, args.Y)
1257 | assert.Equal(t, true, args.Z)
1258 | }
1259 |
1260 | func TestEmbeddedPtr(t *testing.T) {
1261 | // embedded pointer fields are not supported so this should return an error
1262 | var args struct {
1263 | *A
1264 | }
1265 | err := parse("--x=hello", &args)
1266 | require.Error(t, err)
1267 | }
1268 |
1269 | func TestEmbeddedPtrIgnored(t *testing.T) {
1270 | // embedded pointer fields are not normally supported but here
1271 | // we explicitly exclude it so the non-nil embedded structs
1272 | // should work as expected
1273 | var args struct {
1274 | *A `arg:"-"`
1275 | B
1276 | }
1277 | err := parse("--y=321", &args)
1278 | require.NoError(t, err)
1279 | assert.Equal(t, 321, args.Y)
1280 | }
1281 |
1282 | func TestEmbeddedWithDuplicateField(t *testing.T) {
1283 | // see https://github.com/alexflint/go-arg/issues/100
1284 | type T struct {
1285 | A string `arg:"--cat"`
1286 | }
1287 | type U struct {
1288 | A string `arg:"--dog"`
1289 | }
1290 | var args struct {
1291 | T
1292 | U
1293 | }
1294 |
1295 | err := parse("--cat=cat --dog=dog", &args)
1296 | require.NoError(t, err)
1297 | assert.Equal(t, "cat", args.T.A)
1298 | assert.Equal(t, "dog", args.U.A)
1299 | }
1300 |
1301 | func TestEmbeddedWithDuplicateField2(t *testing.T) {
1302 | // see https://github.com/alexflint/go-arg/issues/100
1303 | type T struct {
1304 | A string
1305 | }
1306 | type U struct {
1307 | A string
1308 | }
1309 | var args struct {
1310 | T
1311 | U
1312 | }
1313 |
1314 | err := parse("--a=xyz", &args)
1315 | require.NoError(t, err)
1316 | assert.Equal(t, "xyz", args.T.A)
1317 | assert.Equal(t, "", args.U.A)
1318 | }
1319 |
1320 | func TestUnexportedEmbedded(t *testing.T) {
1321 | type embeddedArgs struct {
1322 | Foo string
1323 | }
1324 | var args struct {
1325 | embeddedArgs
1326 | }
1327 | err := parse("--foo bar", &args)
1328 | require.NoError(t, err)
1329 | assert.Equal(t, "bar", args.Foo)
1330 | }
1331 |
1332 | func TestIgnoredEmbedded(t *testing.T) {
1333 | type embeddedArgs struct {
1334 | Foo string
1335 | }
1336 | var args struct {
1337 | embeddedArgs `arg:"-"`
1338 | }
1339 | err := parse("--foo bar", &args)
1340 | require.Error(t, err)
1341 | }
1342 |
1343 | func TestEmptyArgs(t *testing.T) {
1344 | origArgs := os.Args
1345 |
1346 | // test what happens if somehow os.Args is empty
1347 | os.Args = nil
1348 | var args struct {
1349 | Foo string
1350 | }
1351 | MustParse(&args)
1352 |
1353 | // put the original arguments back
1354 | os.Args = origArgs
1355 | }
1356 |
1357 | func TestTooManyHyphens(t *testing.T) {
1358 | var args struct {
1359 | TooManyHyphens string `arg:"---x"`
1360 | }
1361 | err := parse("--foo -", &args)
1362 | assert.Error(t, err)
1363 | }
1364 |
1365 | func TestHyphenAsOption(t *testing.T) {
1366 | var args struct {
1367 | Foo string
1368 | }
1369 | err := parse("--foo -", &args)
1370 | require.NoError(t, err)
1371 | assert.Equal(t, "-", args.Foo)
1372 | }
1373 |
1374 | func TestHyphenAsPositional(t *testing.T) {
1375 | var args struct {
1376 | Foo string `arg:"positional"`
1377 | }
1378 | err := parse("-", &args)
1379 | require.NoError(t, err)
1380 | assert.Equal(t, "-", args.Foo)
1381 | }
1382 |
1383 | func TestHyphenInMultiOption(t *testing.T) {
1384 | var args struct {
1385 | Foo []string
1386 | Bar int
1387 | }
1388 | err := parse("--foo --- x - y --bar 3", &args)
1389 | require.NoError(t, err)
1390 | assert.Equal(t, []string{"---", "x", "-", "y"}, args.Foo)
1391 | assert.Equal(t, 3, args.Bar)
1392 | }
1393 |
1394 | func TestHyphenInMultiPositional(t *testing.T) {
1395 | var args struct {
1396 | Foo []string `arg:"positional"`
1397 | }
1398 | err := parse("--- x - y", &args)
1399 | require.NoError(t, err)
1400 | assert.Equal(t, []string{"---", "x", "-", "y"}, args.Foo)
1401 | }
1402 |
1403 | func TestSeparate(t *testing.T) {
1404 | for _, val := range []string{"-f one", "-f=one", "--foo one", "--foo=one"} {
1405 | var args struct {
1406 | Foo []string `arg:"--foo,-f,separate"`
1407 | }
1408 |
1409 | err := parse(val, &args)
1410 | require.NoError(t, err)
1411 | assert.Equal(t, []string{"one"}, args.Foo)
1412 | }
1413 | }
1414 |
1415 | func TestSeparateWithDefault(t *testing.T) {
1416 | args := struct {
1417 | Foo []string `arg:"--foo,-f,separate"`
1418 | }{
1419 | Foo: []string{"default"},
1420 | }
1421 |
1422 | err := parse("-f one -f=two", &args)
1423 | require.NoError(t, err)
1424 | assert.Equal(t, []string{"default", "one", "two"}, args.Foo)
1425 | }
1426 |
1427 | func TestSeparateWithPositional(t *testing.T) {
1428 | var args struct {
1429 | Foo []string `arg:"--foo,-f,separate"`
1430 | Bar string `arg:"positional"`
1431 | Moo string `arg:"positional"`
1432 | }
1433 |
1434 | err := parse("zzz --foo one -f=two --foo=three -f four aaa", &args)
1435 | require.NoError(t, err)
1436 | assert.Equal(t, []string{"one", "two", "three", "four"}, args.Foo)
1437 | assert.Equal(t, "zzz", args.Bar)
1438 | assert.Equal(t, "aaa", args.Moo)
1439 | }
1440 |
1441 | func TestSeparatePositionalInterweaved(t *testing.T) {
1442 | var args struct {
1443 | Foo []string `arg:"--foo,-f,separate"`
1444 | Bar []string `arg:"--bar,-b,separate"`
1445 | Pre string `arg:"positional"`
1446 | Post []string `arg:"positional"`
1447 | }
1448 |
1449 | err := parse("zzz -f foo1 -b=bar1 --foo=foo2 -b bar2 post1 -b bar3 post2 post3", &args)
1450 | require.NoError(t, err)
1451 | assert.Equal(t, []string{"foo1", "foo2"}, args.Foo)
1452 | assert.Equal(t, []string{"bar1", "bar2", "bar3"}, args.Bar)
1453 | assert.Equal(t, "zzz", args.Pre)
1454 | assert.Equal(t, []string{"post1", "post2", "post3"}, args.Post)
1455 | }
1456 |
1457 | func TestSpacesAllowedInTags(t *testing.T) {
1458 | var args struct {
1459 | Foo []string `arg:"--foo, -f, separate, required, help:quite nice really"`
1460 | }
1461 |
1462 | err := parse("--foo one -f=two --foo=three -f four", &args)
1463 | require.NoError(t, err)
1464 | assert.Equal(t, []string{"one", "two", "three", "four"}, args.Foo)
1465 | }
1466 |
1467 | func TestReuseParser(t *testing.T) {
1468 | var args struct {
1469 | Foo string `arg:"required"`
1470 | }
1471 |
1472 | p, err := NewParser(Config{}, &args)
1473 | require.NoError(t, err)
1474 |
1475 | err = p.Parse([]string{"--foo=abc"})
1476 | require.NoError(t, err)
1477 | assert.Equal(t, args.Foo, "abc")
1478 |
1479 | err = p.Parse([]string{})
1480 | assert.Error(t, err)
1481 | }
1482 |
1483 | func TestNoVersion(t *testing.T) {
1484 | var args struct{}
1485 |
1486 | p, err := NewParser(Config{}, &args)
1487 | require.NoError(t, err)
1488 |
1489 | err = p.Parse([]string{"--version"})
1490 | assert.Error(t, err)
1491 | assert.NotEqual(t, ErrVersion, err)
1492 | }
1493 |
1494 | func TestBuiltinVersion(t *testing.T) {
1495 | var args struct{}
1496 |
1497 | p, err := NewParser(Config{}, &args)
1498 | require.NoError(t, err)
1499 |
1500 | p.version = "example 3.2.1"
1501 |
1502 | err = p.Parse([]string{"--version"})
1503 | assert.Equal(t, ErrVersion, err)
1504 | }
1505 |
1506 | func TestArgsVersion(t *testing.T) {
1507 | var args struct {
1508 | Version bool `arg:"--version"`
1509 | }
1510 |
1511 | p, err := NewParser(Config{}, &args)
1512 | require.NoError(t, err)
1513 |
1514 | err = p.Parse([]string{"--version"})
1515 | require.NoError(t, err)
1516 | require.Equal(t, args.Version, true)
1517 | }
1518 |
1519 | func TestArgsAndBuiltinVersion(t *testing.T) {
1520 | var args struct {
1521 | Version bool `arg:"--version"`
1522 | }
1523 |
1524 | p, err := NewParser(Config{}, &args)
1525 | require.NoError(t, err)
1526 |
1527 | p.version = "example 3.2.1"
1528 |
1529 | err = p.Parse([]string{"--version"})
1530 | require.NoError(t, err)
1531 | require.Equal(t, args.Version, true)
1532 | }
1533 |
1534 | func TestMultipleTerminates(t *testing.T) {
1535 | var args struct {
1536 | X []string
1537 | Y string `arg:"positional"`
1538 | }
1539 |
1540 | err := parse("--x a b -- c", &args)
1541 | require.NoError(t, err)
1542 | assert.Equal(t, []string{"a", "b"}, args.X)
1543 | assert.Equal(t, "c", args.Y)
1544 | }
1545 |
1546 | func TestDefaultOptionValues(t *testing.T) {
1547 | var args struct {
1548 | A int `default:"123"`
1549 | B *int `default:"123"`
1550 | C string `default:"abc"`
1551 | D *string `default:"abc"`
1552 | E float64 `default:"1.23"`
1553 | F *float64 `default:"1.23"`
1554 | G bool `default:"true"`
1555 | H *bool `default:"true"`
1556 | }
1557 |
1558 | err := parse("--c=xyz --e=4.56", &args)
1559 | require.NoError(t, err)
1560 |
1561 | assert.Equal(t, 123, args.A)
1562 | if assert.NotNil(t, args.B) {
1563 | assert.Equal(t, 123, *args.B)
1564 | }
1565 | assert.Equal(t, "xyz", args.C)
1566 | if assert.NotNil(t, args.D) {
1567 | assert.Equal(t, "abc", *args.D)
1568 | }
1569 | assert.Equal(t, 4.56, args.E)
1570 | if assert.NotNil(t, args.F) {
1571 | assert.Equal(t, 1.23, *args.F)
1572 | }
1573 | assert.True(t, args.G)
1574 | if assert.NotNil(t, args.H) {
1575 | assert.True(t, *args.H)
1576 | }
1577 | }
1578 |
1579 | func TestDefaultUnparseable(t *testing.T) {
1580 | var args struct {
1581 | A int `default:"x"`
1582 | }
1583 |
1584 | err := parse("", &args)
1585 | assert.EqualError(t, err, `.A: error processing default value: strconv.ParseInt: parsing "x": invalid syntax`)
1586 | }
1587 |
1588 | func TestDefaultPositionalValues(t *testing.T) {
1589 | var args struct {
1590 | A int `arg:"positional" default:"123"`
1591 | B *int `arg:"positional" default:"123"`
1592 | C string `arg:"positional" default:"abc"`
1593 | D *string `arg:"positional" default:"abc"`
1594 | E float64 `arg:"positional" default:"1.23"`
1595 | F *float64 `arg:"positional" default:"1.23"`
1596 | G bool `arg:"positional" default:"true"`
1597 | H *bool `arg:"positional" default:"true"`
1598 | }
1599 |
1600 | err := parse("456 789", &args)
1601 | require.NoError(t, err)
1602 |
1603 | assert.Equal(t, 456, args.A)
1604 | if assert.NotNil(t, args.B) {
1605 | assert.Equal(t, 789, *args.B)
1606 | }
1607 | assert.Equal(t, "abc", args.C)
1608 | if assert.NotNil(t, args.D) {
1609 | assert.Equal(t, "abc", *args.D)
1610 | }
1611 | assert.Equal(t, 1.23, args.E)
1612 | if assert.NotNil(t, args.F) {
1613 | assert.Equal(t, 1.23, *args.F)
1614 | }
1615 | assert.True(t, args.G)
1616 | if assert.NotNil(t, args.H) {
1617 | assert.True(t, *args.H)
1618 | }
1619 | }
1620 |
1621 | func TestDefaultValuesNotAllowedWithRequired(t *testing.T) {
1622 | var args struct {
1623 | A int `arg:"required" default:"123"` // required not allowed with default!
1624 | }
1625 |
1626 | err := parse("", &args)
1627 | assert.EqualError(t, err, ".A: 'required' cannot be used when a default value is specified")
1628 | }
1629 |
1630 | func TestDefaultValuesNotAllowedWithSlice(t *testing.T) {
1631 | var args struct {
1632 | A []int `default:"invalid"` // default values not allowed with slices
1633 | }
1634 |
1635 | err := parse("", &args)
1636 | assert.EqualError(t, err, ".A: default values are not supported for slice or map fields")
1637 | }
1638 |
1639 | func TestUnexportedFieldsSkipped(t *testing.T) {
1640 | var args struct {
1641 | unexported struct{}
1642 | }
1643 |
1644 | _, err := NewParser(Config{}, &args)
1645 | require.NoError(t, err)
1646 | }
1647 |
1648 | func TestMustParseInvalidParser(t *testing.T) {
1649 | var exitCode int
1650 | var stdout bytes.Buffer
1651 | exit := func(code int) { exitCode = code }
1652 |
1653 | var args struct {
1654 | CannotParse struct{}
1655 | }
1656 | parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
1657 | assert.Nil(t, parser)
1658 | assert.Equal(t, 2, exitCode)
1659 | }
1660 |
1661 | func TestMustParsePrintsHelp(t *testing.T) {
1662 | originalArgs := os.Args
1663 | defer func() {
1664 | os.Args = originalArgs
1665 | }()
1666 |
1667 | os.Args = []string{"someprogram", "--help"}
1668 |
1669 | var exitCode int
1670 | var stdout bytes.Buffer
1671 | exit := func(code int) { exitCode = code }
1672 |
1673 | var args struct{}
1674 | parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
1675 | assert.NotNil(t, parser)
1676 | assert.Equal(t, 0, exitCode)
1677 | }
1678 |
1679 | func TestMustParsePrintsVersion(t *testing.T) {
1680 | originalArgs := os.Args
1681 | defer func() {
1682 | os.Args = originalArgs
1683 | }()
1684 |
1685 | var exitCode int
1686 | var stdout bytes.Buffer
1687 | exit := func(code int) { exitCode = code }
1688 |
1689 | os.Args = []string{"someprogram", "--version"}
1690 |
1691 | var args versioned
1692 | parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
1693 | require.NotNil(t, parser)
1694 | assert.Equal(t, 0, exitCode)
1695 | assert.Equal(t, "example 3.2.1\n", stdout.String())
1696 | }
1697 |
1698 | type mapWithUnmarshalText struct {
1699 | val map[string]string
1700 | }
1701 |
1702 | func (v *mapWithUnmarshalText) UnmarshalText(data []byte) error {
1703 | return json.Unmarshal(data, &v.val)
1704 | }
1705 |
1706 | func TestTextUnmarshalerEmpty(t *testing.T) {
1707 | // based on https://github.com/alexflint/go-arg/issues/184
1708 | var args struct {
1709 | Config mapWithUnmarshalText `arg:"--config"`
1710 | }
1711 |
1712 | err := parse("", &args)
1713 | require.NoError(t, err)
1714 | assert.Empty(t, args.Config)
1715 | }
1716 |
1717 | func TestTextUnmarshalerEmptyPointer(t *testing.T) {
1718 | // a slight variant on https://github.com/alexflint/go-arg/issues/184
1719 | var args struct {
1720 | Config *mapWithUnmarshalText `arg:"--config"`
1721 | }
1722 |
1723 | err := parse("", &args)
1724 | require.NoError(t, err)
1725 | assert.Nil(t, args.Config)
1726 | }
1727 |
1728 | // similar to the above but also implements MarshalText
1729 | type mapWithMarshalText struct {
1730 | val map[string]string
1731 | }
1732 |
1733 | func (v *mapWithMarshalText) MarshalText(data []byte) error {
1734 | return json.Unmarshal(data, &v.val)
1735 | }
1736 |
1737 | func (v *mapWithMarshalText) UnmarshalText(data []byte) error {
1738 | return json.Unmarshal(data, &v.val)
1739 | }
1740 |
1741 | func TestTextMarshalerUnmarshalerEmpty(t *testing.T) {
1742 | // based on https://github.com/alexflint/go-arg/issues/184
1743 | var args struct {
1744 | Config mapWithMarshalText `arg:"--config"`
1745 | }
1746 |
1747 | err := parse("", &args)
1748 | require.NoError(t, err)
1749 | assert.Empty(t, args.Config)
1750 | }
1751 |
1752 | func TestTextMarshalerUnmarshalerEmptyPointer(t *testing.T) {
1753 | // a slight variant on https://github.com/alexflint/go-arg/issues/184
1754 | var args struct {
1755 | Config *mapWithMarshalText `arg:"--config"`
1756 | }
1757 |
1758 | err := parse("", &args)
1759 | require.NoError(t, err)
1760 | assert.Nil(t, args.Config)
1761 | }
1762 |
1763 | func TestSubcommandGlobalFlag_Before(t *testing.T) {
1764 | var args struct {
1765 | Global bool `arg:"-g"`
1766 | Sub *struct {
1767 | } `arg:"subcommand"`
1768 | }
1769 |
1770 | p, err := NewParser(Config{StrictSubcommands: false}, &args)
1771 | require.NoError(t, err)
1772 |
1773 | err = p.Parse([]string{"-g", "sub"})
1774 | assert.NoError(t, err)
1775 | assert.True(t, args.Global)
1776 | }
1777 |
1778 | func TestSubcommandGlobalFlag_InCommand(t *testing.T) {
1779 | var args struct {
1780 | Global bool `arg:"-g"`
1781 | Sub *struct {
1782 | } `arg:"subcommand"`
1783 | }
1784 |
1785 | p, err := NewParser(Config{StrictSubcommands: false}, &args)
1786 | require.NoError(t, err)
1787 |
1788 | err = p.Parse([]string{"sub", "-g"})
1789 | assert.NoError(t, err)
1790 | assert.True(t, args.Global)
1791 | }
1792 |
1793 | func TestSubcommandGlobalFlag_Before_Strict(t *testing.T) {
1794 | var args struct {
1795 | Global bool `arg:"-g"`
1796 | Sub *struct {
1797 | } `arg:"subcommand"`
1798 | }
1799 |
1800 | p, err := NewParser(Config{StrictSubcommands: true}, &args)
1801 | require.NoError(t, err)
1802 |
1803 | err = p.Parse([]string{"-g", "sub"})
1804 | assert.NoError(t, err)
1805 | assert.True(t, args.Global)
1806 | }
1807 |
1808 | func TestSubcommandGlobalFlag_InCommand_Strict(t *testing.T) {
1809 | var args struct {
1810 | Global bool `arg:"-g"`
1811 | Sub *struct {
1812 | } `arg:"subcommand"`
1813 | }
1814 |
1815 | p, err := NewParser(Config{StrictSubcommands: true}, &args)
1816 | require.NoError(t, err)
1817 |
1818 | err = p.Parse([]string{"sub", "-g"})
1819 | assert.Error(t, err)
1820 | }
1821 |
1822 | func TestSubcommandGlobalFlag_InCommand_Strict_Inner(t *testing.T) {
1823 | var args struct {
1824 | Global bool `arg:"-g"`
1825 | Sub *struct {
1826 | Guard bool `arg:"-g"`
1827 | } `arg:"subcommand"`
1828 | }
1829 |
1830 | p, err := NewParser(Config{StrictSubcommands: true}, &args)
1831 | require.NoError(t, err)
1832 |
1833 | err = p.Parse([]string{"sub", "-g"})
1834 | require.NoError(t, err)
1835 | assert.False(t, args.Global)
1836 | require.NotNil(t, args.Sub)
1837 | assert.True(t, args.Sub.Guard)
1838 | }
1839 |
1840 | func TestExitFunctionAndOutStreamGetFilledIn(t *testing.T) {
1841 | var args struct{}
1842 | p, err := NewParser(Config{}, &args)
1843 | require.NoError(t, err)
1844 | assert.NotNil(t, p.config.Exit) // go prohibits function pointer comparison
1845 | assert.Equal(t, p.config.Out, os.Stdout)
1846 | }
1847 |
--------------------------------------------------------------------------------
/reflect.go:
--------------------------------------------------------------------------------
1 | package arg
2 |
3 | import (
4 | "encoding"
5 | "fmt"
6 | "reflect"
7 | "unicode"
8 | "unicode/utf8"
9 |
10 | scalar "github.com/alexflint/go-scalar"
11 | )
12 |
13 | var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()
14 |
15 | // cardinality tracks how many tokens are expected for a given spec
16 | // - zero is a boolean, which does to expect any value
17 | // - one is an ordinary option that will be parsed from a single token
18 | // - multiple is a slice or map that can accept zero or more tokens
19 | type cardinality int
20 |
21 | const (
22 | zero cardinality = iota
23 | one
24 | multiple
25 | unsupported
26 | )
27 |
28 | func (k cardinality) String() string {
29 | switch k {
30 | case zero:
31 | return "zero"
32 | case one:
33 | return "one"
34 | case multiple:
35 | return "multiple"
36 | case unsupported:
37 | return "unsupported"
38 | default:
39 | return fmt.Sprintf("unknown(%d)", int(k))
40 | }
41 | }
42 |
43 | // cardinalityOf returns true if the type can be parsed from a string
44 | func cardinalityOf(t reflect.Type) (cardinality, error) {
45 | if scalar.CanParse(t) {
46 | if isBoolean(t) {
47 | return zero, nil
48 | }
49 | return one, nil
50 | }
51 |
52 | // look inside pointer types
53 | if t.Kind() == reflect.Ptr {
54 | t = t.Elem()
55 | }
56 |
57 | // look inside slice and map types
58 | switch t.Kind() {
59 | case reflect.Slice:
60 | if !scalar.CanParse(t.Elem()) {
61 | return unsupported, fmt.Errorf("cannot parse into %v because %v not supported", t, t.Elem())
62 | }
63 | return multiple, nil
64 | case reflect.Map:
65 | if !scalar.CanParse(t.Key()) {
66 | return unsupported, fmt.Errorf("cannot parse into %v because key type %v not supported", t, t.Elem())
67 | }
68 | if !scalar.CanParse(t.Elem()) {
69 | return unsupported, fmt.Errorf("cannot parse into %v because value type %v not supported", t, t.Elem())
70 | }
71 | return multiple, nil
72 | default:
73 | return unsupported, fmt.Errorf("cannot parse into %v", t)
74 | }
75 | }
76 |
77 | // isBoolean returns true if the type is a boolean or a pointer to a boolean
78 | func isBoolean(t reflect.Type) bool {
79 | switch {
80 | case isTextUnmarshaler(t):
81 | return false
82 | case t.Kind() == reflect.Bool:
83 | return true
84 | case t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Bool:
85 | return true
86 | default:
87 | return false
88 | }
89 | }
90 |
91 | // isTextUnmarshaler returns true if the type or its pointer implements encoding.TextUnmarshaler
92 | func isTextUnmarshaler(t reflect.Type) bool {
93 | return t.Implements(textUnmarshalerType) || reflect.PtrTo(t).Implements(textUnmarshalerType)
94 | }
95 |
96 | // isExported returns true if the struct field name is exported
97 | func isExported(field string) bool {
98 | r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8
99 | return unicode.IsLetter(r) && unicode.IsUpper(r)
100 | }
101 |
102 | // isZero returns true if v contains the zero value for its type
103 | func isZero(v reflect.Value) bool {
104 | t := v.Type()
105 | if t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice || t.Kind() == reflect.Map || t.Kind() == reflect.Chan || t.Kind() == reflect.Interface {
106 | return v.IsNil()
107 | }
108 | if !t.Comparable() {
109 | return false
110 | }
111 | return v.Interface() == reflect.Zero(t).Interface()
112 | }
113 |
--------------------------------------------------------------------------------
/reflect_test.go:
--------------------------------------------------------------------------------
1 | package arg
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func assertCardinality(t *testing.T, typ reflect.Type, expected cardinality) {
11 | actual, err := cardinalityOf(typ)
12 | assert.Equal(t, expected, actual, "expected %v to have cardinality %v but got %v", typ, expected, actual)
13 | if expected == unsupported {
14 | assert.Error(t, err)
15 | }
16 | }
17 |
18 | func TestCardinalityOf(t *testing.T) {
19 | var b bool
20 | var i int
21 | var s string
22 | var f float64
23 | var bs []bool
24 | var is []int
25 | var m map[string]int
26 | var unsupported1 struct{}
27 | var unsupported2 []struct{}
28 | var unsupported3 map[string]struct{}
29 | var unsupported4 map[struct{}]string
30 |
31 | assertCardinality(t, reflect.TypeOf(b), zero)
32 | assertCardinality(t, reflect.TypeOf(i), one)
33 | assertCardinality(t, reflect.TypeOf(s), one)
34 | assertCardinality(t, reflect.TypeOf(f), one)
35 |
36 | assertCardinality(t, reflect.TypeOf(&b), zero)
37 | assertCardinality(t, reflect.TypeOf(&s), one)
38 | assertCardinality(t, reflect.TypeOf(&i), one)
39 | assertCardinality(t, reflect.TypeOf(&f), one)
40 |
41 | assertCardinality(t, reflect.TypeOf(bs), multiple)
42 | assertCardinality(t, reflect.TypeOf(is), multiple)
43 |
44 | assertCardinality(t, reflect.TypeOf(&bs), multiple)
45 | assertCardinality(t, reflect.TypeOf(&is), multiple)
46 |
47 | assertCardinality(t, reflect.TypeOf(m), multiple)
48 | assertCardinality(t, reflect.TypeOf(&m), multiple)
49 |
50 | assertCardinality(t, reflect.TypeOf(unsupported1), unsupported)
51 | assertCardinality(t, reflect.TypeOf(&unsupported1), unsupported)
52 | assertCardinality(t, reflect.TypeOf(unsupported2), unsupported)
53 | assertCardinality(t, reflect.TypeOf(&unsupported2), unsupported)
54 | assertCardinality(t, reflect.TypeOf(unsupported3), unsupported)
55 | assertCardinality(t, reflect.TypeOf(&unsupported3), unsupported)
56 | assertCardinality(t, reflect.TypeOf(unsupported4), unsupported)
57 | assertCardinality(t, reflect.TypeOf(&unsupported4), unsupported)
58 | }
59 |
60 | type implementsTextUnmarshaler struct{}
61 |
62 | func (*implementsTextUnmarshaler) UnmarshalText(text []byte) error {
63 | return nil
64 | }
65 |
66 | func TestCardinalityTextUnmarshaler(t *testing.T) {
67 | var x implementsTextUnmarshaler
68 | var s []implementsTextUnmarshaler
69 | var m []implementsTextUnmarshaler
70 | assertCardinality(t, reflect.TypeOf(x), one)
71 | assertCardinality(t, reflect.TypeOf(&x), one)
72 | assertCardinality(t, reflect.TypeOf(s), multiple)
73 | assertCardinality(t, reflect.TypeOf(&s), multiple)
74 | assertCardinality(t, reflect.TypeOf(m), multiple)
75 | assertCardinality(t, reflect.TypeOf(&m), multiple)
76 | }
77 |
78 | func TestIsExported(t *testing.T) {
79 | assert.True(t, isExported("Exported"))
80 | assert.False(t, isExported("notExported"))
81 | assert.False(t, isExported(""))
82 | assert.False(t, isExported(string([]byte{255})))
83 | }
84 |
85 | func TestCardinalityString(t *testing.T) {
86 | assert.Equal(t, "zero", zero.String())
87 | assert.Equal(t, "one", one.String())
88 | assert.Equal(t, "multiple", multiple.String())
89 | assert.Equal(t, "unsupported", unsupported.String())
90 | assert.Equal(t, "unknown(42)", cardinality(42).String())
91 | }
92 |
93 | func TestIsZero(t *testing.T) {
94 | var zero int
95 | var notZero = 3
96 | var nilSlice []int
97 | var nonNilSlice = []int{1, 2, 3}
98 | var nilMap map[string]string
99 | var nonNilMap = map[string]string{"foo": "bar"}
100 | var uncomparable = func() {}
101 |
102 | assert.True(t, isZero(reflect.ValueOf(zero)))
103 | assert.False(t, isZero(reflect.ValueOf(notZero)))
104 |
105 | assert.True(t, isZero(reflect.ValueOf(nilSlice)))
106 | assert.False(t, isZero(reflect.ValueOf(nonNilSlice)))
107 |
108 | assert.True(t, isZero(reflect.ValueOf(nilMap)))
109 | assert.False(t, isZero(reflect.ValueOf(nonNilMap)))
110 |
111 | assert.False(t, isZero(reflect.ValueOf(uncomparable)))
112 | }
113 |
--------------------------------------------------------------------------------
/sequence.go:
--------------------------------------------------------------------------------
1 | package arg
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "strings"
7 |
8 | scalar "github.com/alexflint/go-scalar"
9 | )
10 |
11 | // setSliceOrMap parses a sequence of strings into a slice or map. If clear is
12 | // true then any values already in the slice or map are first removed.
13 | func setSliceOrMap(dest reflect.Value, values []string, clear bool) error {
14 | if !dest.CanSet() {
15 | return fmt.Errorf("field is not writable")
16 | }
17 |
18 | t := dest.Type()
19 | if t.Kind() == reflect.Ptr {
20 | dest = dest.Elem()
21 | t = t.Elem()
22 | }
23 |
24 | switch t.Kind() {
25 | case reflect.Slice:
26 | return setSlice(dest, values, clear)
27 | case reflect.Map:
28 | return setMap(dest, values, clear)
29 | default:
30 | return fmt.Errorf("setSliceOrMap cannot insert values into a %v", t)
31 | }
32 | }
33 |
34 | // setSlice parses a sequence of strings and inserts them into a slice. If clear
35 | // is true then any values already in the slice are removed.
36 | func setSlice(dest reflect.Value, values []string, clear bool) error {
37 | var ptr bool
38 | elem := dest.Type().Elem()
39 | if elem.Kind() == reflect.Ptr && !elem.Implements(textUnmarshalerType) {
40 | ptr = true
41 | elem = elem.Elem()
42 | }
43 |
44 | // clear the slice in case default values exist
45 | if clear && !dest.IsNil() {
46 | dest.SetLen(0)
47 | }
48 |
49 | // parse the values one-by-one
50 | for _, s := range values {
51 | v := reflect.New(elem)
52 | if err := scalar.ParseValue(v.Elem(), s); err != nil {
53 | return err
54 | }
55 | if !ptr {
56 | v = v.Elem()
57 | }
58 | dest.Set(reflect.Append(dest, v))
59 | }
60 | return nil
61 | }
62 |
63 | // setMap parses a sequence of name=value strings and inserts them into a map.
64 | // If clear is true then any values already in the map are removed.
65 | func setMap(dest reflect.Value, values []string, clear bool) error {
66 | // determine the key and value type
67 | var keyIsPtr bool
68 | keyType := dest.Type().Key()
69 | if keyType.Kind() == reflect.Ptr && !keyType.Implements(textUnmarshalerType) {
70 | keyIsPtr = true
71 | keyType = keyType.Elem()
72 | }
73 |
74 | var valIsPtr bool
75 | valType := dest.Type().Elem()
76 | if valType.Kind() == reflect.Ptr && !valType.Implements(textUnmarshalerType) {
77 | valIsPtr = true
78 | valType = valType.Elem()
79 | }
80 |
81 | // clear the slice in case default values exist
82 | if clear && !dest.IsNil() {
83 | for _, k := range dest.MapKeys() {
84 | dest.SetMapIndex(k, reflect.Value{})
85 | }
86 | }
87 |
88 | // allocate the map if it is not allocated
89 | if dest.IsNil() {
90 | dest.Set(reflect.MakeMap(dest.Type()))
91 | }
92 |
93 | // parse the values one-by-one
94 | for _, s := range values {
95 | // split at the first equals sign
96 | pos := strings.Index(s, "=")
97 | if pos == -1 {
98 | return fmt.Errorf("cannot parse %q into a map, expected format key=value", s)
99 | }
100 |
101 | // parse the key
102 | k := reflect.New(keyType)
103 | if err := scalar.ParseValue(k.Elem(), s[:pos]); err != nil {
104 | return err
105 | }
106 | if !keyIsPtr {
107 | k = k.Elem()
108 | }
109 |
110 | // parse the value
111 | v := reflect.New(valType)
112 | if err := scalar.ParseValue(v.Elem(), s[pos+1:]); err != nil {
113 | return err
114 | }
115 | if !valIsPtr {
116 | v = v.Elem()
117 | }
118 |
119 | // add it to the map
120 | dest.SetMapIndex(k, v)
121 | }
122 | return nil
123 | }
124 |
--------------------------------------------------------------------------------
/sequence_test.go:
--------------------------------------------------------------------------------
1 | package arg
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | func TestSetSliceWithoutClearing(t *testing.T) {
12 | xs := []int{10}
13 | entries := []string{"1", "2", "3"}
14 | err := setSlice(reflect.ValueOf(&xs).Elem(), entries, false)
15 | require.NoError(t, err)
16 | assert.Equal(t, []int{10, 1, 2, 3}, xs)
17 | }
18 |
19 | func TestSetSliceAfterClearing(t *testing.T) {
20 | xs := []int{100}
21 | entries := []string{"1", "2", "3"}
22 | err := setSlice(reflect.ValueOf(&xs).Elem(), entries, true)
23 | require.NoError(t, err)
24 | assert.Equal(t, []int{1, 2, 3}, xs)
25 | }
26 |
27 | func TestSetSliceInvalid(t *testing.T) {
28 | xs := []int{100}
29 | entries := []string{"invalid"}
30 | err := setSlice(reflect.ValueOf(&xs).Elem(), entries, true)
31 | assert.Error(t, err)
32 | }
33 |
34 | func TestSetSlicePtr(t *testing.T) {
35 | var xs []*int
36 | entries := []string{"1", "2", "3"}
37 | err := setSlice(reflect.ValueOf(&xs).Elem(), entries, true)
38 | require.NoError(t, err)
39 | require.Len(t, xs, 3)
40 | assert.Equal(t, 1, *xs[0])
41 | assert.Equal(t, 2, *xs[1])
42 | assert.Equal(t, 3, *xs[2])
43 | }
44 |
45 | func TestSetSliceTextUnmarshaller(t *testing.T) {
46 | // textUnmarshaler is a struct that captures the length of the string passed to it
47 | var xs []*textUnmarshaler
48 | entries := []string{"a", "aa", "aaa"}
49 | err := setSlice(reflect.ValueOf(&xs).Elem(), entries, true)
50 | require.NoError(t, err)
51 | require.Len(t, xs, 3)
52 | assert.Equal(t, 1, xs[0].val)
53 | assert.Equal(t, 2, xs[1].val)
54 | assert.Equal(t, 3, xs[2].val)
55 | }
56 |
57 | func TestSetMapWithoutClearing(t *testing.T) {
58 | m := map[string]int{"foo": 10}
59 | entries := []string{"a=1", "b=2"}
60 | err := setMap(reflect.ValueOf(&m).Elem(), entries, false)
61 | require.NoError(t, err)
62 | require.Len(t, m, 3)
63 | assert.Equal(t, 1, m["a"])
64 | assert.Equal(t, 2, m["b"])
65 | assert.Equal(t, 10, m["foo"])
66 | }
67 |
68 | func TestSetMapAfterClearing(t *testing.T) {
69 | m := map[string]int{"foo": 10}
70 | entries := []string{"a=1", "b=2"}
71 | err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
72 | require.NoError(t, err)
73 | require.Len(t, m, 2)
74 | assert.Equal(t, 1, m["a"])
75 | assert.Equal(t, 2, m["b"])
76 | }
77 |
78 | func TestSetMapWithKeyPointer(t *testing.T) {
79 | // textUnmarshaler is a struct that captures the length of the string passed to it
80 | var m map[*string]int
81 | entries := []string{"abc=123"}
82 | err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
83 | require.NoError(t, err)
84 | require.Len(t, m, 1)
85 | }
86 |
87 | func TestSetMapWithValuePointer(t *testing.T) {
88 | // textUnmarshaler is a struct that captures the length of the string passed to it
89 | var m map[string]*int
90 | entries := []string{"abc=123"}
91 | err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
92 | require.NoError(t, err)
93 | require.Len(t, m, 1)
94 | assert.Equal(t, 123, *m["abc"])
95 | }
96 |
97 | func TestSetMapTextUnmarshaller(t *testing.T) {
98 | // textUnmarshaler is a struct that captures the length of the string passed to it
99 | var m map[textUnmarshaler]*textUnmarshaler
100 | entries := []string{"a=123", "aa=12", "aaa=1"}
101 | err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
102 | require.NoError(t, err)
103 | require.Len(t, m, 3)
104 | assert.Equal(t, &textUnmarshaler{3}, m[textUnmarshaler{1}])
105 | assert.Equal(t, &textUnmarshaler{2}, m[textUnmarshaler{2}])
106 | assert.Equal(t, &textUnmarshaler{1}, m[textUnmarshaler{3}])
107 | }
108 |
109 | func TestSetMapInvalidKey(t *testing.T) {
110 | var m map[int]int
111 | entries := []string{"invalid=123"}
112 | err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
113 | assert.Error(t, err)
114 | }
115 |
116 | func TestSetMapInvalidValue(t *testing.T) {
117 | var m map[int]int
118 | entries := []string{"123=invalid"}
119 | err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
120 | assert.Error(t, err)
121 | }
122 |
123 | func TestSetMapMalformed(t *testing.T) {
124 | // textUnmarshaler is a struct that captures the length of the string passed to it
125 | var m map[string]string
126 | entries := []string{"missing_equals_sign"}
127 | err := setMap(reflect.ValueOf(&m).Elem(), entries, true)
128 | assert.Error(t, err)
129 | }
130 |
131 | func TestSetSliceOrMapErrors(t *testing.T) {
132 | var err error
133 | var dest reflect.Value
134 |
135 | // converting a slice to a reflect.Value in this way will make it read only
136 | var cannotSet []int
137 | dest = reflect.ValueOf(cannotSet)
138 | err = setSliceOrMap(dest, nil, false)
139 | assert.Error(t, err)
140 |
141 | // check what happens when we pass in something that is not a slice or a map
142 | var notSliceOrMap string
143 | dest = reflect.ValueOf(¬SliceOrMap).Elem()
144 | err = setSliceOrMap(dest, nil, false)
145 | assert.Error(t, err)
146 |
147 | // check what happens when we pass in a pointer to something that is not a slice or a map
148 | var stringPtr *string
149 | dest = reflect.ValueOf(&stringPtr).Elem()
150 | err = setSliceOrMap(dest, nil, false)
151 | assert.Error(t, err)
152 | }
153 |
--------------------------------------------------------------------------------
/subcommand.go:
--------------------------------------------------------------------------------
1 | package arg
2 |
3 | import "fmt"
4 |
5 | // Subcommand returns the user struct for the subcommand selected by
6 | // the command line arguments most recently processed by the parser.
7 | // The return value is always a pointer to a struct. If no subcommand
8 | // was specified then it returns the top-level arguments struct. If
9 | // no command line arguments have been processed by this parser then it
10 | // returns nil.
11 | func (p *Parser) Subcommand() interface{} {
12 | if len(p.subcommand) == 0 {
13 | return nil
14 | }
15 | cmd, err := p.lookupCommand(p.subcommand...)
16 | if err != nil {
17 | return nil
18 | }
19 | return p.val(cmd.dest).Interface()
20 | }
21 |
22 | // SubcommandNames returns the sequence of subcommands specified by the
23 | // user. If no subcommands were given then it returns an empty slice.
24 | func (p *Parser) SubcommandNames() []string {
25 | return p.subcommand
26 | }
27 |
28 | // lookupCommand finds a subcommand based on a sequence of subcommand names. The
29 | // first string should be a top-level subcommand, the next should be a child
30 | // subcommand of that subcommand, and so on. If no strings are given then the
31 | // root command is returned. If no such subcommand exists then an error is
32 | // returned.
33 | func (p *Parser) lookupCommand(path ...string) (*command, error) {
34 | cmd := p.cmd
35 | for _, name := range path {
36 | found := findSubcommand(cmd.subcommands, name)
37 | if found == nil {
38 | return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
39 | }
40 | cmd = found
41 | }
42 | return cmd, nil
43 | }
44 |
--------------------------------------------------------------------------------
/subcommand_test.go:
--------------------------------------------------------------------------------
1 | package arg
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | // This file contains tests for parse.go but I decided to put them here
12 | // since that file is getting large
13 |
14 | func TestSubcommandNotAPointer(t *testing.T) {
15 | var args struct {
16 | A string `arg:"subcommand"`
17 | }
18 | _, err := NewParser(Config{}, &args)
19 | assert.Error(t, err)
20 | }
21 |
22 | func TestSubcommandNotAPointerToStruct(t *testing.T) {
23 | var args struct {
24 | A struct{} `arg:"subcommand"`
25 | }
26 | _, err := NewParser(Config{}, &args)
27 | assert.Error(t, err)
28 | }
29 |
30 | func TestPositionalAndSubcommandNotAllowed(t *testing.T) {
31 | var args struct {
32 | A string `arg:"positional"`
33 | B *struct{} `arg:"subcommand"`
34 | }
35 | _, err := NewParser(Config{}, &args)
36 | assert.Error(t, err)
37 | }
38 |
39 | func TestMinimalSubcommand(t *testing.T) {
40 | type listCmd struct {
41 | }
42 | var args struct {
43 | List *listCmd `arg:"subcommand"`
44 | }
45 | p, err := pparse("list", &args)
46 | require.NoError(t, err)
47 | assert.NotNil(t, args.List)
48 | assert.Equal(t, args.List, p.Subcommand())
49 | assert.Equal(t, []string{"list"}, p.SubcommandNames())
50 | }
51 |
52 | func TestSubcommandNamesBeforeParsing(t *testing.T) {
53 | type listCmd struct{}
54 | var args struct {
55 | List *listCmd `arg:"subcommand"`
56 | }
57 | p, err := NewParser(Config{}, &args)
58 | require.NoError(t, err)
59 | assert.Nil(t, p.Subcommand())
60 | assert.Nil(t, p.SubcommandNames())
61 | }
62 |
63 | func TestNoSuchSubcommand(t *testing.T) {
64 | type listCmd struct {
65 | }
66 | var args struct {
67 | List *listCmd `arg:"subcommand"`
68 | }
69 | _, err := pparse("invalid", &args)
70 | assert.Error(t, err)
71 | }
72 |
73 | func TestNamedSubcommand(t *testing.T) {
74 | type listCmd struct {
75 | }
76 | var args struct {
77 | List *listCmd `arg:"subcommand:ls"`
78 | }
79 | p, err := pparse("ls", &args)
80 | require.NoError(t, err)
81 | assert.NotNil(t, args.List)
82 | assert.Equal(t, args.List, p.Subcommand())
83 | assert.Equal(t, []string{"ls"}, p.SubcommandNames())
84 | }
85 |
86 | func TestSubcommandAliases(t *testing.T) {
87 | type listCmd struct {
88 | }
89 | var args struct {
90 | List *listCmd `arg:"subcommand:list|ls"`
91 | }
92 | p, err := pparse("ls", &args)
93 | require.NoError(t, err)
94 | assert.NotNil(t, args.List)
95 | assert.Equal(t, args.List, p.Subcommand())
96 | assert.Equal(t, []string{"ls"}, p.SubcommandNames())
97 | }
98 |
99 | func TestEmptySubcommand(t *testing.T) {
100 | type listCmd struct {
101 | }
102 | var args struct {
103 | List *listCmd `arg:"subcommand"`
104 | }
105 | p, err := pparse("", &args)
106 | require.NoError(t, err)
107 | assert.Nil(t, args.List)
108 | assert.Nil(t, p.Subcommand())
109 | assert.Empty(t, p.SubcommandNames())
110 | }
111 |
112 | func TestTwoSubcommands(t *testing.T) {
113 | type getCmd struct {
114 | }
115 | type listCmd struct {
116 | }
117 | var args struct {
118 | Get *getCmd `arg:"subcommand"`
119 | List *listCmd `arg:"subcommand"`
120 | }
121 | p, err := pparse("list", &args)
122 | require.NoError(t, err)
123 | assert.Nil(t, args.Get)
124 | assert.NotNil(t, args.List)
125 | assert.Equal(t, args.List, p.Subcommand())
126 | assert.Equal(t, []string{"list"}, p.SubcommandNames())
127 | }
128 |
129 | func TestTwoSubcommandsWithAliases(t *testing.T) {
130 | type getCmd struct {
131 | }
132 | type listCmd struct {
133 | }
134 | var args struct {
135 | Get *getCmd `arg:"subcommand:get|g"`
136 | List *listCmd `arg:"subcommand:list|ls"`
137 | }
138 | p, err := pparse("ls", &args)
139 | require.NoError(t, err)
140 | assert.Nil(t, args.Get)
141 | assert.NotNil(t, args.List)
142 | assert.Equal(t, args.List, p.Subcommand())
143 | assert.Equal(t, []string{"ls"}, p.SubcommandNames())
144 | }
145 |
146 | func TestSubcommandsWithOptions(t *testing.T) {
147 | type getCmd struct {
148 | Name string
149 | }
150 | type listCmd struct {
151 | Limit int
152 | }
153 | type cmd struct {
154 | Verbose bool
155 | Get *getCmd `arg:"subcommand"`
156 | List *listCmd `arg:"subcommand"`
157 | }
158 |
159 | {
160 | var args cmd
161 | err := parse("list", &args)
162 | require.NoError(t, err)
163 | assert.Nil(t, args.Get)
164 | assert.NotNil(t, args.List)
165 | }
166 |
167 | {
168 | var args cmd
169 | err := parse("list --limit 3", &args)
170 | require.NoError(t, err)
171 | assert.Nil(t, args.Get)
172 | assert.NotNil(t, args.List)
173 | assert.Equal(t, args.List.Limit, 3)
174 | }
175 |
176 | {
177 | var args cmd
178 | err := parse("list --limit 3 --verbose", &args)
179 | require.NoError(t, err)
180 | assert.Nil(t, args.Get)
181 | assert.NotNil(t, args.List)
182 | assert.Equal(t, args.List.Limit, 3)
183 | assert.True(t, args.Verbose)
184 | }
185 |
186 | {
187 | var args cmd
188 | err := parse("list --verbose --limit 3", &args)
189 | require.NoError(t, err)
190 | assert.Nil(t, args.Get)
191 | assert.NotNil(t, args.List)
192 | assert.Equal(t, args.List.Limit, 3)
193 | assert.True(t, args.Verbose)
194 | }
195 |
196 | {
197 | var args cmd
198 | err := parse("--verbose list --limit 3", &args)
199 | require.NoError(t, err)
200 | assert.Nil(t, args.Get)
201 | assert.NotNil(t, args.List)
202 | assert.Equal(t, args.List.Limit, 3)
203 | assert.True(t, args.Verbose)
204 | }
205 |
206 | {
207 | var args cmd
208 | err := parse("get", &args)
209 | require.NoError(t, err)
210 | assert.NotNil(t, args.Get)
211 | assert.Nil(t, args.List)
212 | }
213 |
214 | {
215 | var args cmd
216 | err := parse("get --name test", &args)
217 | require.NoError(t, err)
218 | assert.NotNil(t, args.Get)
219 | assert.Nil(t, args.List)
220 | assert.Equal(t, args.Get.Name, "test")
221 | }
222 | }
223 |
224 | func TestSubcommandsWithEnvVars(t *testing.T) {
225 | type getCmd struct {
226 | Name string `arg:"env"`
227 | }
228 | type listCmd struct {
229 | Limit int `arg:"env"`
230 | }
231 | type cmd struct {
232 | Verbose bool
233 | Get *getCmd `arg:"subcommand"`
234 | List *listCmd `arg:"subcommand"`
235 | }
236 |
237 | {
238 | var args cmd
239 | setenv(t, "LIMIT", "123")
240 | err := parse("list", &args)
241 | require.NoError(t, err)
242 | require.NotNil(t, args.List)
243 | assert.Equal(t, 123, args.List.Limit)
244 | }
245 |
246 | {
247 | var args cmd
248 | setenv(t, "LIMIT", "not_an_integer")
249 | err := parse("list", &args)
250 | assert.Error(t, err)
251 | }
252 | }
253 |
254 | func TestNestedSubcommands(t *testing.T) {
255 | type child struct{}
256 | type parent struct {
257 | Child *child `arg:"subcommand"`
258 | }
259 | type grandparent struct {
260 | Parent *parent `arg:"subcommand"`
261 | }
262 | type root struct {
263 | Grandparent *grandparent `arg:"subcommand"`
264 | }
265 |
266 | {
267 | var args root
268 | p, err := pparse("grandparent parent child", &args)
269 | require.NoError(t, err)
270 | require.NotNil(t, args.Grandparent)
271 | require.NotNil(t, args.Grandparent.Parent)
272 | require.NotNil(t, args.Grandparent.Parent.Child)
273 | assert.Equal(t, args.Grandparent.Parent.Child, p.Subcommand())
274 | assert.Equal(t, []string{"grandparent", "parent", "child"}, p.SubcommandNames())
275 | }
276 |
277 | {
278 | var args root
279 | p, err := pparse("grandparent parent", &args)
280 | require.NoError(t, err)
281 | require.NotNil(t, args.Grandparent)
282 | require.NotNil(t, args.Grandparent.Parent)
283 | require.Nil(t, args.Grandparent.Parent.Child)
284 | assert.Equal(t, args.Grandparent.Parent, p.Subcommand())
285 | assert.Equal(t, []string{"grandparent", "parent"}, p.SubcommandNames())
286 | }
287 |
288 | {
289 | var args root
290 | p, err := pparse("grandparent", &args)
291 | require.NoError(t, err)
292 | require.NotNil(t, args.Grandparent)
293 | require.Nil(t, args.Grandparent.Parent)
294 | assert.Equal(t, args.Grandparent, p.Subcommand())
295 | assert.Equal(t, []string{"grandparent"}, p.SubcommandNames())
296 | }
297 |
298 | {
299 | var args root
300 | p, err := pparse("", &args)
301 | require.NoError(t, err)
302 | require.Nil(t, args.Grandparent)
303 | assert.Nil(t, p.Subcommand())
304 | assert.Empty(t, p.SubcommandNames())
305 | }
306 | }
307 |
308 | func TestNestedSubcommandsWithAliases(t *testing.T) {
309 | type child struct{}
310 | type parent struct {
311 | Child *child `arg:"subcommand:child|ch"`
312 | }
313 | type grandparent struct {
314 | Parent *parent `arg:"subcommand:parent|pa"`
315 | }
316 | type root struct {
317 | Grandparent *grandparent `arg:"subcommand:grandparent|gp"`
318 | }
319 |
320 | {
321 | var args root
322 | p, err := pparse("gp parent child", &args)
323 | require.NoError(t, err)
324 | require.NotNil(t, args.Grandparent)
325 | require.NotNil(t, args.Grandparent.Parent)
326 | require.NotNil(t, args.Grandparent.Parent.Child)
327 | assert.Equal(t, args.Grandparent.Parent.Child, p.Subcommand())
328 | assert.Equal(t, []string{"gp", "parent", "child"}, p.SubcommandNames())
329 | }
330 |
331 | {
332 | var args root
333 | p, err := pparse("grandparent pa", &args)
334 | require.NoError(t, err)
335 | require.NotNil(t, args.Grandparent)
336 | require.NotNil(t, args.Grandparent.Parent)
337 | require.Nil(t, args.Grandparent.Parent.Child)
338 | assert.Equal(t, args.Grandparent.Parent, p.Subcommand())
339 | assert.Equal(t, []string{"grandparent", "pa"}, p.SubcommandNames())
340 | }
341 |
342 | {
343 | var args root
344 | p, err := pparse("grandparent", &args)
345 | require.NoError(t, err)
346 | require.NotNil(t, args.Grandparent)
347 | require.Nil(t, args.Grandparent.Parent)
348 | assert.Equal(t, args.Grandparent, p.Subcommand())
349 | assert.Equal(t, []string{"grandparent"}, p.SubcommandNames())
350 | }
351 |
352 | {
353 | var args root
354 | p, err := pparse("", &args)
355 | require.NoError(t, err)
356 | require.Nil(t, args.Grandparent)
357 | assert.Nil(t, p.Subcommand())
358 | assert.Empty(t, p.SubcommandNames())
359 | }
360 | }
361 |
362 | func TestSubcommandsWithPositionals(t *testing.T) {
363 | type listCmd struct {
364 | Pattern string `arg:"positional"`
365 | }
366 | type cmd struct {
367 | Format string
368 | List *listCmd `arg:"subcommand"`
369 | }
370 |
371 | {
372 | var args cmd
373 | err := parse("list", &args)
374 | require.NoError(t, err)
375 | assert.NotNil(t, args.List)
376 | assert.Equal(t, "", args.List.Pattern)
377 | }
378 |
379 | {
380 | var args cmd
381 | err := parse("list --format json", &args)
382 | require.NoError(t, err)
383 | assert.NotNil(t, args.List)
384 | assert.Equal(t, "", args.List.Pattern)
385 | assert.Equal(t, "json", args.Format)
386 | }
387 |
388 | {
389 | var args cmd
390 | err := parse("list somepattern", &args)
391 | require.NoError(t, err)
392 | assert.NotNil(t, args.List)
393 | assert.Equal(t, "somepattern", args.List.Pattern)
394 | }
395 |
396 | {
397 | var args cmd
398 | err := parse("list somepattern --format json", &args)
399 | require.NoError(t, err)
400 | assert.NotNil(t, args.List)
401 | assert.Equal(t, "somepattern", args.List.Pattern)
402 | assert.Equal(t, "json", args.Format)
403 | }
404 |
405 | {
406 | var args cmd
407 | err := parse("list --format json somepattern", &args)
408 | require.NoError(t, err)
409 | assert.NotNil(t, args.List)
410 | assert.Equal(t, "somepattern", args.List.Pattern)
411 | assert.Equal(t, "json", args.Format)
412 | }
413 |
414 | {
415 | var args cmd
416 | err := parse("--format json list somepattern", &args)
417 | require.NoError(t, err)
418 | assert.NotNil(t, args.List)
419 | assert.Equal(t, "somepattern", args.List.Pattern)
420 | assert.Equal(t, "json", args.Format)
421 | }
422 |
423 | {
424 | var args cmd
425 | err := parse("--format json", &args)
426 | require.NoError(t, err)
427 | assert.Nil(t, args.List)
428 | assert.Equal(t, "json", args.Format)
429 | }
430 | }
431 | func TestSubcommandsWithMultiplePositionals(t *testing.T) {
432 | type getCmd struct {
433 | Items []string `arg:"positional"`
434 | }
435 | type cmd struct {
436 | Limit int
437 | Get *getCmd `arg:"subcommand"`
438 | }
439 |
440 | {
441 | var args cmd
442 | err := parse("get", &args)
443 | require.NoError(t, err)
444 | assert.NotNil(t, args.Get)
445 | assert.Empty(t, args.Get.Items)
446 | }
447 |
448 | {
449 | var args cmd
450 | err := parse("get --limit 5", &args)
451 | require.NoError(t, err)
452 | assert.NotNil(t, args.Get)
453 | assert.Empty(t, args.Get.Items)
454 | assert.Equal(t, 5, args.Limit)
455 | }
456 |
457 | {
458 | var args cmd
459 | err := parse("get item1", &args)
460 | require.NoError(t, err)
461 | assert.NotNil(t, args.Get)
462 | assert.Equal(t, []string{"item1"}, args.Get.Items)
463 | }
464 |
465 | {
466 | var args cmd
467 | err := parse("get item1 item2 item3", &args)
468 | require.NoError(t, err)
469 | assert.NotNil(t, args.Get)
470 | assert.Equal(t, []string{"item1", "item2", "item3"}, args.Get.Items)
471 | }
472 |
473 | {
474 | var args cmd
475 | err := parse("get item1 --limit 5 item2", &args)
476 | require.NoError(t, err)
477 | assert.NotNil(t, args.Get)
478 | assert.Equal(t, []string{"item1", "item2"}, args.Get.Items)
479 | assert.Equal(t, 5, args.Limit)
480 | }
481 | }
482 |
483 | func TestValForNilStruct(t *testing.T) {
484 | type subcmd struct{}
485 | var cmd struct {
486 | Sub *subcmd `arg:"subcommand"`
487 | }
488 |
489 | p, err := NewParser(Config{}, &cmd)
490 | require.NoError(t, err)
491 |
492 | typ := reflect.TypeOf(cmd)
493 | subField, _ := typ.FieldByName("Sub")
494 |
495 | v := p.val(path{fields: []reflect.StructField{subField, subField}})
496 | assert.False(t, v.IsValid())
497 | }
498 |
499 | func TestSubcommandInvalidInternal(t *testing.T) {
500 | // this situation should never arise in practice but still good to test for it
501 | var cmd struct{}
502 | p, err := NewParser(Config{}, &cmd)
503 | require.NoError(t, err)
504 |
505 | p.subcommand = []string{"should", "never", "happen"}
506 | sub := p.Subcommand()
507 | assert.Nil(t, sub)
508 | }
509 |
--------------------------------------------------------------------------------
/usage.go:
--------------------------------------------------------------------------------
1 | package arg
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "strings"
7 | )
8 |
9 | // the width of the left column
10 | const colWidth = 25
11 |
12 | // Fail prints usage information to p.Config.Out and exits with status code 2.
13 | func (p *Parser) Fail(msg string) {
14 | p.FailSubcommand(msg)
15 | }
16 |
17 | // FailSubcommand prints usage information for a specified subcommand to p.Config.Out,
18 | // then exits with status code 2. To write usage information for a top-level
19 | // subcommand, provide just the name of that subcommand. To write usage
20 | // information for a subcommand that is nested under another subcommand, provide
21 | // a sequence of subcommand names starting with the top-level subcommand and so
22 | // on down the tree.
23 | func (p *Parser) FailSubcommand(msg string, subcommand ...string) error {
24 | err := p.WriteUsageForSubcommand(p.config.Out, subcommand...)
25 | if err != nil {
26 | return err
27 | }
28 |
29 | fmt.Fprintln(p.config.Out, "error:", msg)
30 | p.config.Exit(2)
31 | return nil
32 | }
33 |
34 | // WriteUsage writes usage information to the given writer
35 | func (p *Parser) WriteUsage(w io.Writer) {
36 | p.WriteUsageForSubcommand(w, p.subcommand...)
37 | }
38 |
39 | // WriteUsageForSubcommand writes the usage information for a specified
40 | // subcommand. To write usage information for a top-level subcommand, provide
41 | // just the name of that subcommand. To write usage information for a subcommand
42 | // that is nested under another subcommand, provide a sequence of subcommand
43 | // names starting with the top-level subcommand and so on down the tree.
44 | func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) error {
45 | cmd, err := p.lookupCommand(subcommand...)
46 | if err != nil {
47 | return err
48 | }
49 |
50 | var positionals, longOptions, shortOptions []*spec
51 | for _, spec := range cmd.specs {
52 | switch {
53 | case spec.positional:
54 | positionals = append(positionals, spec)
55 | case spec.long != "":
56 | longOptions = append(longOptions, spec)
57 | case spec.short != "":
58 | shortOptions = append(shortOptions, spec)
59 | }
60 | }
61 |
62 | // print the beginning of the usage string
63 | fmt.Fprintf(w, "Usage: %s", p.cmd.name)
64 | for _, s := range subcommand {
65 | fmt.Fprint(w, " "+s)
66 | }
67 |
68 | // write the option component of the usage message
69 | for _, spec := range shortOptions {
70 | // prefix with a space
71 | fmt.Fprint(w, " ")
72 | if !spec.required {
73 | fmt.Fprint(w, "[")
74 | }
75 | fmt.Fprint(w, synopsis(spec, "-"+spec.short))
76 | if !spec.required {
77 | fmt.Fprint(w, "]")
78 | }
79 | }
80 |
81 | for _, spec := range longOptions {
82 | // prefix with a space
83 | fmt.Fprint(w, " ")
84 | if !spec.required {
85 | fmt.Fprint(w, "[")
86 | }
87 | fmt.Fprint(w, synopsis(spec, "--"+spec.long))
88 | if !spec.required {
89 | fmt.Fprint(w, "]")
90 | }
91 | }
92 |
93 | // When we parse positionals, we check that:
94 | // 1. required positionals come before non-required positionals
95 | // 2. there is at most one multiple-value positional
96 | // 3. if there is a multiple-value positional then it comes after all other positionals
97 | // Here we merely print the usage string, so we do not explicitly re-enforce those rules
98 |
99 | // write the positionals in following form:
100 | // REQUIRED1 REQUIRED2
101 | // REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]
102 | // REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]
103 | // REQUIRED1 REQUIRED2 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]
104 | // REQUIRED1 REQUIRED2 [OPTIONAL1 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]]
105 | var closeBrackets int
106 | for _, spec := range positionals {
107 | fmt.Fprint(w, " ")
108 | if !spec.required {
109 | fmt.Fprint(w, "[")
110 | closeBrackets += 1
111 | }
112 | if spec.cardinality == multiple {
113 | fmt.Fprintf(w, "%s [%s ...]", spec.placeholder, spec.placeholder)
114 | } else {
115 | fmt.Fprint(w, spec.placeholder)
116 | }
117 | }
118 | fmt.Fprint(w, strings.Repeat("]", closeBrackets))
119 |
120 | // if the program supports subcommands, give a hint to the user about their existence
121 | if len(cmd.subcommands) > 0 {
122 | fmt.Fprint(w, " []")
123 | }
124 |
125 | fmt.Fprint(w, "\n")
126 | return nil
127 | }
128 |
129 | // print prints a line like this:
130 | //
131 | // --option FOO A description of the option [default: 123]
132 | //
133 | // If the text on the left is longer than a certain threshold, the description is moved to the next line:
134 | //
135 | // --verylongoptionoption VERY_LONG_VARIABLE
136 | // A description of the option [default: 123]
137 | //
138 | // If multiple "extras" are provided then they are put inside a single set of square brackets:
139 | //
140 | // --option FOO A description of the option [default: 123, env: FOO]
141 | func print(w io.Writer, item, description string, bracketed ...string) {
142 | lhs := " " + item
143 | fmt.Fprint(w, lhs)
144 | if description != "" {
145 | if len(lhs)+2 < colWidth {
146 | fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs)))
147 | } else {
148 | fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
149 | }
150 | fmt.Fprint(w, description)
151 | }
152 |
153 | var brack string
154 | for _, s := range bracketed {
155 | if s != "" {
156 | if brack != "" {
157 | brack += ", "
158 | }
159 | brack += s
160 | }
161 | }
162 |
163 | if brack != "" {
164 | fmt.Fprintf(w, " [%s]", brack)
165 | }
166 | fmt.Fprint(w, "\n")
167 | }
168 |
169 | func withDefault(s string) string {
170 | if s == "" {
171 | return ""
172 | }
173 | return "default: " + s
174 | }
175 |
176 | func withEnv(env string) string {
177 | if env == "" {
178 | return ""
179 | }
180 | return "env: " + env
181 | }
182 |
183 | // WriteHelp writes the usage string followed by the full help string for each option
184 | func (p *Parser) WriteHelp(w io.Writer) {
185 | p.WriteHelpForSubcommand(w, p.subcommand...)
186 | }
187 |
188 | // WriteHelpForSubcommand writes the usage string followed by the full help
189 | // string for a specified subcommand. To write help for a top-level subcommand,
190 | // provide just the name of that subcommand. To write help for a subcommand that
191 | // is nested under another subcommand, provide a sequence of subcommand names
192 | // starting with the top-level subcommand and so on down the tree.
193 | func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error {
194 | cmd, err := p.lookupCommand(subcommand...)
195 | if err != nil {
196 | return err
197 | }
198 |
199 | var positionals, longOptions, shortOptions, envOnlyOptions []*spec
200 | var hasVersionOption bool
201 | for _, spec := range cmd.specs {
202 | switch {
203 | case spec.positional:
204 | positionals = append(positionals, spec)
205 | case spec.long != "":
206 | longOptions = append(longOptions, spec)
207 | if spec.long == "version" {
208 | hasVersionOption = true
209 | }
210 | case spec.short != "":
211 | shortOptions = append(shortOptions, spec)
212 | case spec.short == "" && spec.long == "":
213 | envOnlyOptions = append(envOnlyOptions, spec)
214 | }
215 | }
216 |
217 | // obtain a flattened list of options from all ancestors
218 | // also determine if any ancestor has a version option spec
219 | var globals []*spec
220 | ancestor := cmd.parent
221 | for ancestor != nil {
222 | for _, spec := range ancestor.specs {
223 | if spec.long == "version" {
224 | hasVersionOption = true
225 | break
226 | }
227 | }
228 | globals = append(globals, ancestor.specs...)
229 | ancestor = ancestor.parent
230 | }
231 |
232 | if p.description != "" {
233 | fmt.Fprintln(w, p.description)
234 | }
235 |
236 | if !hasVersionOption && p.version != "" {
237 | fmt.Fprintln(w, p.version)
238 | }
239 |
240 | p.WriteUsageForSubcommand(w, subcommand...)
241 |
242 | // write the list of positionals
243 | if len(positionals) > 0 {
244 | fmt.Fprint(w, "\nPositional arguments:\n")
245 | for _, spec := range positionals {
246 | print(w, spec.placeholder, spec.help, withDefault(spec.defaultString), withEnv(spec.env))
247 | }
248 | }
249 |
250 | // write the list of options with the short-only ones first to match the usage string
251 | if len(shortOptions)+len(longOptions) > 0 || cmd.parent == nil {
252 | fmt.Fprint(w, "\nOptions:\n")
253 | for _, spec := range shortOptions {
254 | p.printOption(w, spec)
255 | }
256 | for _, spec := range longOptions {
257 | p.printOption(w, spec)
258 | }
259 | }
260 |
261 | // write the list of global options
262 | if len(globals) > 0 {
263 | fmt.Fprint(w, "\nGlobal options:\n")
264 | for _, spec := range globals {
265 | p.printOption(w, spec)
266 | }
267 | }
268 |
269 | // write the list of built in options
270 | p.printOption(w, &spec{
271 | cardinality: zero,
272 | long: "help",
273 | short: "h",
274 | help: "display this help and exit",
275 | })
276 | if !hasVersionOption && p.version != "" {
277 | p.printOption(w, &spec{
278 | cardinality: zero,
279 | long: "version",
280 | help: "display version and exit",
281 | })
282 | }
283 |
284 | // write the list of environment only variables
285 | if len(envOnlyOptions) > 0 {
286 | fmt.Fprint(w, "\nEnvironment variables:\n")
287 | for _, spec := range envOnlyOptions {
288 | p.printEnvOnlyVar(w, spec)
289 | }
290 | }
291 |
292 | // write the list of subcommands
293 | if len(cmd.subcommands) > 0 {
294 | fmt.Fprint(w, "\nCommands:\n")
295 | for _, subcmd := range cmd.subcommands {
296 | names := append([]string{subcmd.name}, subcmd.aliases...)
297 | print(w, strings.Join(names, ", "), subcmd.help)
298 | }
299 | }
300 |
301 | if p.epilogue != "" {
302 | fmt.Fprintln(w, "\n"+p.epilogue)
303 | }
304 | return nil
305 | }
306 |
307 | func (p *Parser) printOption(w io.Writer, spec *spec) {
308 | ways := make([]string, 0, 2)
309 | if spec.long != "" {
310 | ways = append(ways, synopsis(spec, "--"+spec.long))
311 | }
312 | if spec.short != "" {
313 | ways = append(ways, synopsis(spec, "-"+spec.short))
314 | }
315 | if len(ways) > 0 {
316 | print(w, strings.Join(ways, ", "), spec.help, withDefault(spec.defaultString), withEnv(spec.env))
317 | }
318 | }
319 |
320 | func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) {
321 | ways := make([]string, 0, 2)
322 | if spec.required {
323 | ways = append(ways, "Required.")
324 | } else {
325 | ways = append(ways, "Optional.")
326 | }
327 |
328 | if spec.help != "" {
329 | ways = append(ways, spec.help)
330 | }
331 |
332 | print(w, spec.env, strings.Join(ways, " "), withDefault(spec.defaultString))
333 | }
334 |
335 | func synopsis(spec *spec, form string) string {
336 | // if the user omits the placeholder tag then we pick one automatically,
337 | // but if the user explicitly specifies an empty placeholder then we
338 | // leave out the placeholder in the help message
339 | if spec.cardinality == zero || spec.placeholder == "" {
340 | return form
341 | }
342 | return form + " " + spec.placeholder
343 | }
344 |
--------------------------------------------------------------------------------
/usage_test.go:
--------------------------------------------------------------------------------
1 | package arg
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "os"
8 | "strings"
9 | "testing"
10 |
11 | "github.com/stretchr/testify/assert"
12 | "github.com/stretchr/testify/require"
13 | )
14 |
15 | type NameDotName struct {
16 | Head, Tail string
17 | }
18 |
19 | func (n *NameDotName) UnmarshalText(b []byte) error {
20 | s := string(b)
21 | pos := strings.Index(s, ".")
22 | if pos == -1 {
23 | return fmt.Errorf("missing period in %s", s)
24 | }
25 | n.Head = s[:pos]
26 | n.Tail = s[pos+1:]
27 | return nil
28 | }
29 |
30 | func (n *NameDotName) MarshalText() (text []byte, err error) {
31 | text = []byte(fmt.Sprintf("%s.%s", n.Head, n.Tail))
32 | return
33 | }
34 |
35 | func TestWriteUsage(t *testing.T) {
36 | 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 ...]]"
37 |
38 | expectedHelp := `
39 | Usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] [--testenv TESTENV] [--file FILE] INPUT [OUTPUT [OUTPUT ...]]
40 |
41 | Positional arguments:
42 | INPUT
43 | OUTPUT list of outputs
44 |
45 | Options:
46 | --name NAME name to use [default: Foo Bar]
47 | --value VALUE secret value [default: 42]
48 | --verbose, -v verbosity level
49 | --dataset DATASET dataset to use
50 | --optimize OPTIMIZE, -O OPTIMIZE
51 | optimization level
52 | --ids IDS Ids
53 | --values VALUES Values
54 | --workers WORKERS, -w WORKERS
55 | number of workers to start [default: 10, env: WORKERS]
56 | --testenv TESTENV, -a TESTENV [env: TEST_ENV]
57 | --file FILE, -f FILE File with mandatory extension [default: scratch.txt]
58 | --help, -h display this help and exit
59 |
60 | Environment variables:
61 | API_KEY Required. Only via env-var for security reasons
62 | TRACE Optional. Record low-level trace
63 | `
64 |
65 | var args struct {
66 | Input string `arg:"positional,required"`
67 | Output []string `arg:"positional" help:"list of outputs"`
68 | Name string `help:"name to use"`
69 | Value int `help:"secret value"`
70 | Verbose bool `arg:"-v" help:"verbosity level"`
71 | Dataset string `help:"dataset to use"`
72 | Optimize int `arg:"-O" help:"optimization level"`
73 | Ids []int64 `help:"Ids"`
74 | Values []float64 `help:"Values"`
75 | Workers int `arg:"-w,env:WORKERS" help:"number of workers to start" default:"10"`
76 | TestEnv string `arg:"-a,env:TEST_ENV"`
77 | ApiKey string `arg:"required,-,--,env:API_KEY" help:"Only via env-var for security reasons"`
78 | Trace bool `arg:"-,--,env" help:"Record low-level trace"`
79 | File *NameDotName `arg:"-f" help:"File with mandatory extension"`
80 | }
81 | args.Name = "Foo Bar"
82 | args.Value = 42
83 | args.File = &NameDotName{"scratch", "txt"}
84 | p, err := NewParser(Config{Program: "example"}, &args)
85 | require.NoError(t, err)
86 |
87 | os.Args[0] = "example"
88 |
89 | var help bytes.Buffer
90 | p.WriteHelp(&help)
91 | assert.Equal(t, expectedHelp[1:], help.String())
92 |
93 | var usage bytes.Buffer
94 | p.WriteUsage(&usage)
95 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
96 | }
97 |
98 | type MyEnum int
99 |
100 | func (n *MyEnum) UnmarshalText(b []byte) error {
101 | return nil
102 | }
103 |
104 | func (n *MyEnum) MarshalText() ([]byte, error) {
105 | return nil, errors.New("There was a problem")
106 | }
107 |
108 | func TestUsageWithDefaults(t *testing.T) {
109 | expectedUsage := "Usage: example [--label LABEL] [--content CONTENT]"
110 |
111 | expectedHelp := `
112 | Usage: example [--label LABEL] [--content CONTENT]
113 |
114 | Options:
115 | --label LABEL [default: cat]
116 | --content CONTENT [default: dog]
117 | --help, -h display this help and exit
118 | `
119 | var args struct {
120 | Label string
121 | Content string `default:"dog"`
122 | }
123 | args.Label = "cat"
124 | p, err := NewParser(Config{Program: "example"}, &args)
125 | require.NoError(t, err)
126 |
127 | args.Label = "should_ignore_this"
128 |
129 | var help bytes.Buffer
130 | p.WriteHelp(&help)
131 | assert.Equal(t, expectedHelp[1:], help.String())
132 |
133 | var usage bytes.Buffer
134 | p.WriteUsage(&usage)
135 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
136 | }
137 |
138 | func TestUsageCannotMarshalToString(t *testing.T) {
139 | var args struct {
140 | Name *MyEnum
141 | }
142 | v := MyEnum(42)
143 | args.Name = &v
144 | _, err := NewParser(Config{Program: "example"}, &args)
145 | assert.EqualError(t, err, `args.Name: error marshaling default value to string: There was a problem`)
146 | }
147 |
148 | func TestUsageLongPositionalWithHelp_legacyForm(t *testing.T) {
149 | expectedUsage := "Usage: example [VERYLONGPOSITIONALWITHHELP]"
150 |
151 | expectedHelp := `
152 | Usage: example [VERYLONGPOSITIONALWITHHELP]
153 |
154 | Positional arguments:
155 | VERYLONGPOSITIONALWITHHELP
156 | this positional argument is very long but cannot include commas
157 |
158 | Options:
159 | --help, -h display this help and exit
160 | `
161 | var args struct {
162 | VeryLongPositionalWithHelp string `arg:"positional,help:this positional argument is very long but cannot include commas"`
163 | }
164 |
165 | p, err := NewParser(Config{Program: "example"}, &args)
166 | require.NoError(t, err)
167 |
168 | var help bytes.Buffer
169 | p.WriteHelp(&help)
170 | assert.Equal(t, expectedHelp[1:], help.String())
171 |
172 | var usage bytes.Buffer
173 | p.WriteUsage(&usage)
174 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
175 | }
176 |
177 | func TestUsageLongPositionalWithHelp_newForm(t *testing.T) {
178 | expectedUsage := "Usage: example [VERYLONGPOSITIONALWITHHELP]"
179 |
180 | expectedHelp := `
181 | Usage: example [VERYLONGPOSITIONALWITHHELP]
182 |
183 | Positional arguments:
184 | VERYLONGPOSITIONALWITHHELP
185 | this positional argument is very long, and includes: commas, colons etc
186 |
187 | Options:
188 | --help, -h display this help and exit
189 | `
190 | var args struct {
191 | VeryLongPositionalWithHelp string `arg:"positional" help:"this positional argument is very long, and includes: commas, colons etc"`
192 | }
193 |
194 | p, err := NewParser(Config{Program: "example"}, &args)
195 | require.NoError(t, err)
196 |
197 | var help bytes.Buffer
198 | p.WriteHelp(&help)
199 | assert.Equal(t, expectedHelp[1:], help.String())
200 |
201 | var usage bytes.Buffer
202 | p.WriteUsage(&usage)
203 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
204 | }
205 |
206 | func TestUsageWithProgramName(t *testing.T) {
207 | expectedUsage := "Usage: myprogram"
208 |
209 | expectedHelp := `
210 | Usage: myprogram
211 |
212 | Options:
213 | --help, -h display this help and exit
214 | `
215 | config := Config{
216 | Program: "myprogram",
217 | }
218 | p, err := NewParser(config, &struct{}{})
219 | require.NoError(t, err)
220 |
221 | os.Args[0] = "example"
222 |
223 | var help bytes.Buffer
224 | p.WriteHelp(&help)
225 | assert.Equal(t, expectedHelp[1:], help.String())
226 |
227 | var usage bytes.Buffer
228 | p.WriteUsage(&usage)
229 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
230 | }
231 |
232 | type versioned struct{}
233 |
234 | // Version returns the version for this program
235 | func (versioned) Version() string {
236 | return "example 3.2.1"
237 | }
238 |
239 | func TestUsageWithVersion(t *testing.T) {
240 | expectedUsage := "Usage: example"
241 |
242 | expectedHelp := `
243 | example 3.2.1
244 | Usage: example
245 |
246 | Options:
247 | --help, -h display this help and exit
248 | --version display version and exit
249 | `
250 | os.Args[0] = "example"
251 | p, err := NewParser(Config{}, &versioned{})
252 | require.NoError(t, err)
253 |
254 | var help bytes.Buffer
255 | p.WriteHelp(&help)
256 | assert.Equal(t, expectedHelp[1:], help.String())
257 |
258 | var usage bytes.Buffer
259 | p.WriteUsage(&usage)
260 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
261 | }
262 |
263 | func TestUsageWithUserDefinedVersionFlag(t *testing.T) {
264 | expectedUsage := "Usage: example [--version]"
265 |
266 | expectedHelp := `
267 | Usage: example [--version]
268 |
269 | Options:
270 | --version this is a user-defined version flag
271 | --help, -h display this help and exit
272 | `
273 |
274 | var args struct {
275 | ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
276 | }
277 |
278 | os.Args[0] = "example"
279 | p, err := NewParser(Config{}, &args)
280 | require.NoError(t, err)
281 |
282 | var help bytes.Buffer
283 | p.WriteHelp(&help)
284 | assert.Equal(t, expectedHelp[1:], help.String())
285 |
286 | var usage bytes.Buffer
287 | p.WriteUsage(&usage)
288 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
289 | }
290 |
291 | func TestUsageWithVersionAndUserDefinedVersionFlag(t *testing.T) {
292 | expectedUsage := "Usage: example [--version]"
293 |
294 | expectedHelp := `
295 | Usage: example [--version]
296 |
297 | Options:
298 | --version this is a user-defined version flag
299 | --help, -h display this help and exit
300 | `
301 |
302 | var args struct {
303 | versioned
304 | ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
305 | }
306 |
307 | os.Args[0] = "example"
308 | p, err := NewParser(Config{}, &args)
309 | require.NoError(t, err)
310 |
311 | var help bytes.Buffer
312 | p.WriteHelp(&help)
313 | assert.Equal(t, expectedHelp[1:], help.String())
314 |
315 | var usage bytes.Buffer
316 | p.WriteUsage(&usage)
317 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
318 | }
319 |
320 | type subcommand struct {
321 | Number int `arg:"-n,--number" help:"compute something on the given number"`
322 | }
323 |
324 | func TestUsageWithVersionAndSubcommand(t *testing.T) {
325 | expectedUsage := "Usage: example []"
326 |
327 | expectedHelp := `
328 | example 3.2.1
329 | Usage: example []
330 |
331 | Options:
332 | --help, -h display this help and exit
333 | --version display version and exit
334 |
335 | Commands:
336 | cmd
337 | `
338 |
339 | var args struct {
340 | versioned
341 | Cmd *subcommand `arg:"subcommand"`
342 | }
343 |
344 | os.Args[0] = "example"
345 | p, err := NewParser(Config{}, &args)
346 | require.NoError(t, err)
347 |
348 | var help bytes.Buffer
349 | p.WriteHelp(&help)
350 | assert.Equal(t, expectedHelp[1:], help.String())
351 |
352 | var usage bytes.Buffer
353 | p.WriteUsage(&usage)
354 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
355 |
356 | expectedUsage = "Usage: example cmd [--number NUMBER]"
357 |
358 | expectedHelp = `
359 | example 3.2.1
360 | Usage: example cmd [--number NUMBER]
361 |
362 | Options:
363 | --number NUMBER, -n NUMBER
364 | compute something on the given number
365 | --help, -h display this help and exit
366 | --version display version and exit
367 | `
368 | _ = p.Parse([]string{"cmd"})
369 |
370 | help = bytes.Buffer{}
371 | p.WriteHelp(&help)
372 | assert.Equal(t, expectedHelp[1:], help.String())
373 |
374 | usage = bytes.Buffer{}
375 | p.WriteUsage(&usage)
376 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
377 | }
378 |
379 | func TestUsageWithUserDefinedVersionFlagAndSubcommand(t *testing.T) {
380 | expectedUsage := "Usage: example [--version] []"
381 |
382 | expectedHelp := `
383 | Usage: example [--version] []
384 |
385 | Options:
386 | --version this is a user-defined version flag
387 | --help, -h display this help and exit
388 |
389 | Commands:
390 | cmd
391 | `
392 |
393 | var args struct {
394 | Cmd *subcommand `arg:"subcommand"`
395 | ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
396 | }
397 |
398 | os.Args[0] = "example"
399 | p, err := NewParser(Config{}, &args)
400 | require.NoError(t, err)
401 |
402 | var help bytes.Buffer
403 | p.WriteHelp(&help)
404 | assert.Equal(t, expectedHelp[1:], help.String())
405 |
406 | var usage bytes.Buffer
407 | p.WriteUsage(&usage)
408 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
409 |
410 | expectedUsage = "Usage: example cmd [--number NUMBER]"
411 |
412 | expectedHelp = `
413 | Usage: example cmd [--number NUMBER]
414 |
415 | Options:
416 | --number NUMBER, -n NUMBER
417 | compute something on the given number
418 |
419 | Global options:
420 | --version this is a user-defined version flag
421 | --help, -h display this help and exit
422 | `
423 | _ = p.Parse([]string{"cmd"})
424 |
425 | help = bytes.Buffer{}
426 | p.WriteHelp(&help)
427 | assert.Equal(t, expectedHelp[1:], help.String())
428 |
429 | usage = bytes.Buffer{}
430 | p.WriteUsage(&usage)
431 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
432 | }
433 |
434 | func TestUsageWithVersionAndUserDefinedVersionFlagAndSubcommand(t *testing.T) {
435 | expectedUsage := "Usage: example [--version] []"
436 |
437 | expectedHelp := `
438 | Usage: example [--version] []
439 |
440 | Options:
441 | --version this is a user-defined version flag
442 | --help, -h display this help and exit
443 |
444 | Commands:
445 | cmd
446 | `
447 |
448 | var args struct {
449 | versioned
450 | Cmd *subcommand `arg:"subcommand"`
451 | ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
452 | }
453 |
454 | os.Args[0] = "example"
455 | p, err := NewParser(Config{}, &args)
456 | require.NoError(t, err)
457 |
458 | var help bytes.Buffer
459 | p.WriteHelp(&help)
460 | assert.Equal(t, expectedHelp[1:], help.String())
461 |
462 | var usage bytes.Buffer
463 | p.WriteUsage(&usage)
464 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
465 |
466 | expectedUsage = "Usage: example cmd [--number NUMBER]"
467 |
468 | expectedHelp = `
469 | Usage: example cmd [--number NUMBER]
470 |
471 | Options:
472 | --number NUMBER, -n NUMBER
473 | compute something on the given number
474 |
475 | Global options:
476 | --version this is a user-defined version flag
477 | --help, -h display this help and exit
478 | `
479 | _ = p.Parse([]string{"cmd"})
480 |
481 | help = bytes.Buffer{}
482 | p.WriteHelp(&help)
483 | assert.Equal(t, expectedHelp[1:], help.String())
484 |
485 | usage = bytes.Buffer{}
486 | p.WriteUsage(&usage)
487 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
488 | }
489 |
490 | type described struct{}
491 |
492 | // Described returns the description for this program
493 | func (described) Description() string {
494 | return "this program does this and that"
495 | }
496 |
497 | func TestUsageWithDescription(t *testing.T) {
498 | expectedUsage := "Usage: example"
499 |
500 | expectedHelp := `
501 | this program does this and that
502 | Usage: example
503 |
504 | Options:
505 | --help, -h display this help and exit
506 | `
507 | os.Args[0] = "example"
508 | p, err := NewParser(Config{}, &described{})
509 | require.NoError(t, err)
510 |
511 | var help bytes.Buffer
512 | p.WriteHelp(&help)
513 | assert.Equal(t, expectedHelp[1:], help.String())
514 |
515 | var usage bytes.Buffer
516 | p.WriteUsage(&usage)
517 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
518 | }
519 |
520 | type epilogued struct{}
521 |
522 | // Epilogued returns the epilogue for this program
523 | func (epilogued) Epilogue() string {
524 | return "For more information visit github.com/alexflint/go-arg"
525 | }
526 |
527 | func TestUsageWithEpilogue(t *testing.T) {
528 | expectedUsage := "Usage: example"
529 |
530 | expectedHelp := `
531 | Usage: example
532 |
533 | Options:
534 | --help, -h display this help and exit
535 |
536 | For more information visit github.com/alexflint/go-arg
537 | `
538 | os.Args[0] = "example"
539 | p, err := NewParser(Config{}, &epilogued{})
540 | require.NoError(t, err)
541 |
542 | var help bytes.Buffer
543 | p.WriteHelp(&help)
544 | assert.Equal(t, expectedHelp[1:], help.String())
545 |
546 | var usage bytes.Buffer
547 | p.WriteUsage(&usage)
548 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
549 | }
550 |
551 | func TestUsageForRequiredPositionals(t *testing.T) {
552 | expectedUsage := "Usage: example REQUIRED1 REQUIRED2\n"
553 | var args struct {
554 | Required1 string `arg:"positional,required"`
555 | Required2 string `arg:"positional,required"`
556 | }
557 |
558 | p, err := NewParser(Config{Program: "example"}, &args)
559 | require.NoError(t, err)
560 |
561 | var usage bytes.Buffer
562 | p.WriteUsage(&usage)
563 | assert.Equal(t, expectedUsage, usage.String())
564 | }
565 |
566 | func TestUsageForMixedPositionals(t *testing.T) {
567 | expectedUsage := "Usage: example REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]\n"
568 | var args struct {
569 | Required1 string `arg:"positional,required"`
570 | Required2 string `arg:"positional,required"`
571 | Optional1 string `arg:"positional"`
572 | Optional2 string `arg:"positional"`
573 | }
574 |
575 | p, err := NewParser(Config{Program: "example"}, &args)
576 | require.NoError(t, err)
577 |
578 | var usage bytes.Buffer
579 | p.WriteUsage(&usage)
580 | assert.Equal(t, expectedUsage, usage.String())
581 | }
582 |
583 | func TestUsageForRepeatedPositionals(t *testing.T) {
584 | expectedUsage := "Usage: example REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]\n"
585 | var args struct {
586 | Required1 string `arg:"positional,required"`
587 | Required2 string `arg:"positional,required"`
588 | Repeated []string `arg:"positional,required"`
589 | }
590 |
591 | p, err := NewParser(Config{Program: "example"}, &args)
592 | require.NoError(t, err)
593 |
594 | var usage bytes.Buffer
595 | p.WriteUsage(&usage)
596 | assert.Equal(t, expectedUsage, usage.String())
597 | }
598 |
599 | func TestUsageForMixedAndRepeatedPositionals(t *testing.T) {
600 | expectedUsage := "Usage: example REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2 [REPEATED [REPEATED ...]]]]\n"
601 | var args struct {
602 | Required1 string `arg:"positional,required"`
603 | Required2 string `arg:"positional,required"`
604 | Optional1 string `arg:"positional"`
605 | Optional2 string `arg:"positional"`
606 | Repeated []string `arg:"positional"`
607 | }
608 |
609 | p, err := NewParser(Config{Program: "example"}, &args)
610 | require.NoError(t, err)
611 |
612 | var usage bytes.Buffer
613 | p.WriteUsage(&usage)
614 | assert.Equal(t, expectedUsage, usage.String())
615 | }
616 |
617 | func TestRequiredMultiplePositionals(t *testing.T) {
618 | expectedUsage := "Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]\n"
619 |
620 | expectedHelp := `
621 | Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]
622 |
623 | Positional arguments:
624 | REQUIREDMULTIPLE required multiple positional
625 |
626 | Options:
627 | --help, -h display this help and exit
628 | `
629 | var args struct {
630 | RequiredMultiple []string `arg:"positional,required" help:"required multiple positional"`
631 | }
632 |
633 | p, err := NewParser(Config{Program: "example"}, &args)
634 | require.NoError(t, err)
635 |
636 | var help bytes.Buffer
637 | p.WriteHelp(&help)
638 | assert.Equal(t, expectedHelp[1:], help.String())
639 |
640 | var usage bytes.Buffer
641 | p.WriteUsage(&usage)
642 | assert.Equal(t, expectedUsage, usage.String())
643 | }
644 |
645 | func TestUsageWithSubcommands(t *testing.T) {
646 | expectedUsage := "Usage: example child [--values VALUES]"
647 |
648 | expectedHelp := `
649 | Usage: example child [--values VALUES]
650 |
651 | Options:
652 | --values VALUES Values
653 |
654 | Global options:
655 | --verbose, -v verbosity level
656 | --help, -h display this help and exit
657 | `
658 |
659 | var args struct {
660 | Verbose bool `arg:"-v" help:"verbosity level"`
661 | Child *struct {
662 | Values []float64 `help:"Values"`
663 | } `arg:"subcommand:child"`
664 | }
665 |
666 | os.Args[0] = "example"
667 | p, err := NewParser(Config{}, &args)
668 | require.NoError(t, err)
669 |
670 | _ = p.Parse([]string{"child"})
671 |
672 | var help bytes.Buffer
673 | p.WriteHelp(&help)
674 | assert.Equal(t, expectedHelp[1:], help.String())
675 |
676 | var help2 bytes.Buffer
677 | p.WriteHelpForSubcommand(&help2, "child")
678 | assert.Equal(t, expectedHelp[1:], help2.String())
679 |
680 | var usage bytes.Buffer
681 | p.WriteUsage(&usage)
682 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
683 |
684 | var usage2 bytes.Buffer
685 | p.WriteUsageForSubcommand(&usage2, "child")
686 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String()))
687 | }
688 |
689 | func TestUsageWithNestedSubcommands(t *testing.T) {
690 | expectedUsage := "Usage: example child nested [--enable] OUTPUT"
691 |
692 | expectedHelp := `
693 | Usage: example child nested [--enable] OUTPUT
694 |
695 | Positional arguments:
696 | OUTPUT
697 |
698 | Options:
699 | --enable
700 |
701 | Global options:
702 | --values VALUES Values
703 | --verbose, -v verbosity level
704 | --help, -h display this help and exit
705 | `
706 |
707 | var args struct {
708 | Verbose bool `arg:"-v" help:"verbosity level"`
709 | Child *struct {
710 | Values []float64 `help:"Values"`
711 | Nested *struct {
712 | Enable bool
713 | Output string `arg:"positional,required"`
714 | } `arg:"subcommand:nested"`
715 | } `arg:"subcommand:child"`
716 | }
717 |
718 | os.Args[0] = "example"
719 | p, err := NewParser(Config{}, &args)
720 | require.NoError(t, err)
721 |
722 | _ = p.Parse([]string{"child", "nested", "value"})
723 |
724 | assert.Equal(t, []string{"child", "nested"}, p.SubcommandNames())
725 |
726 | var help bytes.Buffer
727 | p.WriteHelp(&help)
728 | assert.Equal(t, expectedHelp[1:], help.String())
729 |
730 | var help2 bytes.Buffer
731 | p.WriteHelpForSubcommand(&help2, "child", "nested")
732 | assert.Equal(t, expectedHelp[1:], help2.String())
733 |
734 | var usage bytes.Buffer
735 | p.WriteUsage(&usage)
736 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
737 |
738 | var usage2 bytes.Buffer
739 | p.WriteUsageForSubcommand(&usage2, "child", "nested")
740 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String()))
741 | }
742 |
743 | func TestNonexistentSubcommand(t *testing.T) {
744 | var args struct {
745 | sub *struct{} `arg:"subcommand"`
746 | }
747 | p, err := NewParser(Config{Exit: func(int) {}}, &args)
748 | require.NoError(t, err)
749 |
750 | var b bytes.Buffer
751 |
752 | err = p.WriteUsageForSubcommand(&b, "does_not_exist")
753 | assert.Error(t, err)
754 |
755 | err = p.WriteHelpForSubcommand(&b, "does_not_exist")
756 | assert.Error(t, err)
757 |
758 | err = p.FailSubcommand("something went wrong", "does_not_exist")
759 | assert.Error(t, err)
760 |
761 | err = p.WriteUsageForSubcommand(&b, "sub", "does_not_exist")
762 | assert.Error(t, err)
763 |
764 | err = p.WriteHelpForSubcommand(&b, "sub", "does_not_exist")
765 | assert.Error(t, err)
766 |
767 | err = p.FailSubcommand("something went wrong", "sub", "does_not_exist")
768 | assert.Error(t, err)
769 | }
770 |
771 | func TestUsageWithoutLongNames(t *testing.T) {
772 | expectedUsage := "Usage: example [-a PLACEHOLDER] -b SHORTONLY2"
773 |
774 | expectedHelp := `
775 | Usage: example [-a PLACEHOLDER] -b SHORTONLY2
776 |
777 | Options:
778 | -a PLACEHOLDER some help [default: some val]
779 | -b SHORTONLY2 some help2
780 | --help, -h display this help and exit
781 | `
782 | var args struct {
783 | ShortOnly string `arg:"-a,--" help:"some help" default:"some val" placeholder:"PLACEHOLDER"`
784 | ShortOnly2 string `arg:"-b,--,required" help:"some help2"`
785 | }
786 | p, err := NewParser(Config{Program: "example"}, &args)
787 | require.NoError(t, err)
788 |
789 | var help bytes.Buffer
790 | p.WriteHelp(&help)
791 | assert.Equal(t, expectedHelp[1:], help.String())
792 |
793 | var usage bytes.Buffer
794 | p.WriteUsage(&usage)
795 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
796 | }
797 |
798 | func TestUsageWithEmptyPlaceholder(t *testing.T) {
799 | expectedUsage := "Usage: example [-a] [--b] [--c]"
800 |
801 | expectedHelp := `
802 | Usage: example [-a] [--b] [--c]
803 |
804 | Options:
805 | -a some help for a
806 | --b some help for b
807 | --c, -c some help for c
808 | --help, -h display this help and exit
809 | `
810 | var args struct {
811 | ShortOnly string `arg:"-a,--" placeholder:"" help:"some help for a"`
812 | LongOnly string `arg:"--b" placeholder:"" help:"some help for b"`
813 | Both string `arg:"-c,--c" placeholder:"" help:"some help for c"`
814 | }
815 | p, err := NewParser(Config{Program: "example"}, &args)
816 | require.NoError(t, err)
817 |
818 | var help bytes.Buffer
819 | p.WriteHelp(&help)
820 | assert.Equal(t, expectedHelp[1:], help.String())
821 |
822 | var usage bytes.Buffer
823 | p.WriteUsage(&usage)
824 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
825 | }
826 |
827 | func TestUsageWithShortFirst(t *testing.T) {
828 | expectedUsage := "Usage: example [-c CAT] [--dog DOG]"
829 |
830 | expectedHelp := `
831 | Usage: example [-c CAT] [--dog DOG]
832 |
833 | Options:
834 | -c CAT
835 | --dog DOG
836 | --help, -h display this help and exit
837 | `
838 | var args struct {
839 | Dog string
840 | Cat string `arg:"-c,--"`
841 | }
842 | p, err := NewParser(Config{Program: "example"}, &args)
843 | assert.NoError(t, err)
844 |
845 | var help bytes.Buffer
846 | p.WriteHelp(&help)
847 | assert.Equal(t, expectedHelp[1:], help.String())
848 |
849 | var usage bytes.Buffer
850 | p.WriteUsage(&usage)
851 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
852 | }
853 |
854 | func TestUsageWithEnvOptions(t *testing.T) {
855 | expectedUsage := "Usage: example [-s SHORT]"
856 |
857 | expectedHelp := `
858 | Usage: example [-s SHORT]
859 |
860 | Options:
861 | -s SHORT [env: SHORT]
862 | --help, -h display this help and exit
863 |
864 | Environment variables:
865 | ENVONLY Optional.
866 | ENVONLY2 Optional.
867 | CUSTOM Optional.
868 | `
869 | var args struct {
870 | Short string `arg:"--,-s,env"`
871 | EnvOnly string `arg:"--,env"`
872 | EnvOnly2 string `arg:"--,-,env"`
873 | EnvOnlyOverriden string `arg:"--,env:CUSTOM"`
874 | }
875 |
876 | p, err := NewParser(Config{Program: "example"}, &args)
877 | assert.NoError(t, err)
878 |
879 | var help bytes.Buffer
880 | p.WriteHelp(&help)
881 | assert.Equal(t, expectedHelp[1:], help.String())
882 |
883 | var usage bytes.Buffer
884 | p.WriteUsage(&usage)
885 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
886 | }
887 |
888 | func TestEnvOnlyArgs(t *testing.T) {
889 | expectedUsage := "Usage: example [--arg ARG]"
890 |
891 | expectedHelp := `
892 | Usage: example [--arg ARG]
893 |
894 | Options:
895 | --arg ARG, -a ARG [env: MY_ARG]
896 | --help, -h display this help and exit
897 |
898 | Environment variables:
899 | AUTH_KEY Required.
900 | `
901 | var args struct {
902 | ArgParam string `arg:"-a,--arg,env:MY_ARG"`
903 | AuthKey string `arg:"required,--,env:AUTH_KEY"`
904 | }
905 | p, err := NewParser(Config{Program: "example"}, &args)
906 | assert.NoError(t, err)
907 |
908 | var help bytes.Buffer
909 | p.WriteHelp(&help)
910 | assert.Equal(t, expectedHelp[1:], help.String())
911 |
912 | var usage bytes.Buffer
913 | p.WriteUsage(&usage)
914 | assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
915 | }
916 |
917 | func TestFail(t *testing.T) {
918 | var stdout bytes.Buffer
919 | var exitCode int
920 | exit := func(code int) { exitCode = code }
921 |
922 | expectedStdout := `
923 | Usage: example [--foo FOO]
924 | error: something went wrong
925 | `
926 |
927 | var args struct {
928 | Foo int
929 | }
930 | p, err := NewParser(Config{Program: "example", Exit: exit, Out: &stdout}, &args)
931 | require.NoError(t, err)
932 | p.Fail("something went wrong")
933 |
934 | assert.Equal(t, expectedStdout[1:], stdout.String())
935 | assert.Equal(t, 2, exitCode)
936 | }
937 |
938 | func TestFailSubcommand(t *testing.T) {
939 | var stdout bytes.Buffer
940 | var exitCode int
941 | exit := func(code int) { exitCode = code }
942 |
943 | expectedStdout := `
944 | Usage: example sub
945 | error: something went wrong
946 | `
947 |
948 | var args struct {
949 | Sub *struct{} `arg:"subcommand"`
950 | }
951 | p, err := NewParser(Config{Program: "example", Exit: exit, Out: &stdout}, &args)
952 | require.NoError(t, err)
953 |
954 | err = p.FailSubcommand("something went wrong", "sub")
955 | require.NoError(t, err)
956 |
957 | assert.Equal(t, expectedStdout[1:], stdout.String())
958 | assert.Equal(t, 2, exitCode)
959 | }
960 |
961 | type lengthOf struct {
962 | Length int
963 | }
964 |
965 | func (p *lengthOf) UnmarshalText(b []byte) error {
966 | p.Length = len(b)
967 | return nil
968 | }
969 |
970 | func TestHelpShowsDefaultValueFromOriginalTag(t *testing.T) {
971 | // check that the usage text prints the original string from the default tag, not
972 | // the serialization of the parsed value
973 |
974 | expectedHelp := `
975 | Usage: example [--test TEST]
976 |
977 | Options:
978 | --test TEST [default: some_default_value]
979 | --help, -h display this help and exit
980 | `
981 |
982 | var args struct {
983 | Test *lengthOf `default:"some_default_value"`
984 | }
985 | p, err := NewParser(Config{Program: "example"}, &args)
986 | require.NoError(t, err)
987 |
988 | var help bytes.Buffer
989 | p.WriteHelp(&help)
990 | assert.Equal(t, expectedHelp[1:], help.String())
991 | }
992 |
993 | func TestHelpShowsSubcommandAliases(t *testing.T) {
994 | expectedHelp := `
995 | Usage: example []
996 |
997 | Options:
998 | --help, -h display this help and exit
999 |
1000 | Commands:
1001 | remove, rm, r remove something from somewhere
1002 | simple do something simple
1003 | halt, stop stop now
1004 | `
1005 |
1006 | var args struct {
1007 | Remove *struct{} `arg:"subcommand:remove|rm|r" help:"remove something from somewhere"`
1008 | Simple *struct{} `arg:"subcommand" help:"do something simple"`
1009 | Stop *struct{} `arg:"subcommand:halt|stop" help:"stop now"`
1010 | }
1011 | p, err := NewParser(Config{Program: "example"}, &args)
1012 | require.NoError(t, err)
1013 |
1014 | var help bytes.Buffer
1015 | p.WriteHelp(&help)
1016 | assert.Equal(t, expectedHelp[1:], help.String())
1017 | }
1018 |
1019 | func TestHelpShowsPositionalWithDefault(t *testing.T) {
1020 | expectedHelp := `
1021 | Usage: example [FOO]
1022 |
1023 | Positional arguments:
1024 | FOO this is a positional with a default [default: bar]
1025 |
1026 | Options:
1027 | --help, -h display this help and exit
1028 | `
1029 |
1030 | var args struct {
1031 | Foo string `arg:"positional" default:"bar" help:"this is a positional with a default"`
1032 | }
1033 |
1034 | p, err := NewParser(Config{Program: "example"}, &args)
1035 | require.NoError(t, err)
1036 |
1037 | var help bytes.Buffer
1038 | p.WriteHelp(&help)
1039 | assert.Equal(t, expectedHelp[1:], help.String())
1040 | }
1041 |
1042 | func TestHelpShowsPositionalWithEnv(t *testing.T) {
1043 | expectedHelp := `
1044 | Usage: example [FOO]
1045 |
1046 | Positional arguments:
1047 | FOO this is a positional with an env variable [env: FOO]
1048 |
1049 | Options:
1050 | --help, -h display this help and exit
1051 | `
1052 |
1053 | var args struct {
1054 | Foo string `arg:"positional,env:FOO" help:"this is a positional with an env variable"`
1055 | }
1056 |
1057 | p, err := NewParser(Config{Program: "example"}, &args)
1058 | require.NoError(t, err)
1059 |
1060 | var help bytes.Buffer
1061 | p.WriteHelp(&help)
1062 | assert.Equal(t, expectedHelp[1:], help.String())
1063 | }
1064 |
1065 | func TestHelpShowsPositionalWithDefaultAndEnv(t *testing.T) {
1066 | expectedHelp := `
1067 | Usage: example [FOO]
1068 |
1069 | Positional arguments:
1070 | FOO this is a positional with a default and an env variable [default: bar, env: FOO]
1071 |
1072 | Options:
1073 | --help, -h display this help and exit
1074 | `
1075 |
1076 | var args struct {
1077 | Foo string `arg:"positional,env:FOO" default:"bar" help:"this is a positional with a default and an env variable"`
1078 | }
1079 |
1080 | p, err := NewParser(Config{Program: "example"}, &args)
1081 | require.NoError(t, err)
1082 |
1083 | var help bytes.Buffer
1084 | p.WriteHelp(&help)
1085 | assert.Equal(t, expectedHelp[1:], help.String())
1086 | }
1087 |
--------------------------------------------------------------------------------