├── .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 | go-arg 3 |
4 | go-arg 5 |
6 |

7 |

Struct-based argument parsing for Go

8 |

9 | Sourcegraph 10 | Documentation 11 | Build Status 12 | Coverage Status 13 | Go Report Card 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 | --------------------------------------------------------------------------------