1package flags 2 3import ( 4 "errors" 5 "fmt" 6 "os" 7 "reflect" 8 "runtime" 9 "strconv" 10 "strings" 11 "testing" 12 "time" 13) 14 15type defaultOptions struct { 16 Int int `long:"i"` 17 IntDefault int `long:"id" default:"1"` 18 19 Float64 float64 `long:"f"` 20 Float64Default float64 `long:"fd" default:"-3.14"` 21 22 NumericFlag bool `short:"3"` 23 24 String string `long:"str"` 25 StringDefault string `long:"strd" default:"abc"` 26 StringNotUnquoted string `long:"strnot" unquote:"false"` 27 28 Time time.Duration `long:"t"` 29 TimeDefault time.Duration `long:"td" default:"1m"` 30 31 Map map[string]int `long:"m"` 32 MapDefault map[string]int `long:"md" default:"a:1"` 33 34 Slice []int `long:"s"` 35 SliceDefault []int `long:"sd" default:"1" default:"2"` 36} 37 38func TestDefaults(t *testing.T) { 39 var tests = []struct { 40 msg string 41 args []string 42 expected defaultOptions 43 }{ 44 { 45 msg: "no arguments, expecting default values", 46 args: []string{}, 47 expected: defaultOptions{ 48 Int: 0, 49 IntDefault: 1, 50 51 Float64: 0.0, 52 Float64Default: -3.14, 53 54 NumericFlag: false, 55 56 String: "", 57 StringDefault: "abc", 58 59 Time: 0, 60 TimeDefault: time.Minute, 61 62 Map: map[string]int{}, 63 MapDefault: map[string]int{"a": 1}, 64 65 Slice: []int{}, 66 SliceDefault: []int{1, 2}, 67 }, 68 }, 69 { 70 msg: "non-zero value arguments, expecting overwritten arguments", 71 args: []string{"--i=3", "--id=3", "--f=-2.71", "--fd=2.71", "-3", "--str=def", "--strd=def", "--t=3ms", "--td=3ms", "--m=c:3", "--md=c:3", "--s=3", "--sd=3"}, 72 expected: defaultOptions{ 73 Int: 3, 74 IntDefault: 3, 75 76 Float64: -2.71, 77 Float64Default: 2.71, 78 79 NumericFlag: true, 80 81 String: "def", 82 StringDefault: "def", 83 84 Time: 3 * time.Millisecond, 85 TimeDefault: 3 * time.Millisecond, 86 87 Map: map[string]int{"c": 3}, 88 MapDefault: map[string]int{"c": 3}, 89 90 Slice: []int{3}, 91 SliceDefault: []int{3}, 92 }, 93 }, 94 { 95 msg: "zero value arguments, expecting overwritten arguments", 96 args: []string{"--i=0", "--id=0", "--f=0", "--fd=0", "--str", "", "--strd=\"\"", "--t=0ms", "--td=0s", "--m=:0", "--md=:0", "--s=0", "--sd=0"}, 97 expected: defaultOptions{ 98 Int: 0, 99 IntDefault: 0, 100 101 Float64: 0, 102 Float64Default: 0, 103 104 String: "", 105 StringDefault: "", 106 107 Time: 0, 108 TimeDefault: 0, 109 110 Map: map[string]int{"": 0}, 111 MapDefault: map[string]int{"": 0}, 112 113 Slice: []int{0}, 114 SliceDefault: []int{0}, 115 }, 116 }, 117 } 118 119 for _, test := range tests { 120 var opts defaultOptions 121 122 _, err := ParseArgs(&opts, test.args) 123 if err != nil { 124 t.Fatalf("%s:\nUnexpected error: %v", test.msg, err) 125 } 126 127 if opts.Slice == nil { 128 opts.Slice = []int{} 129 } 130 131 if !reflect.DeepEqual(opts, test.expected) { 132 t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts) 133 } 134 } 135} 136 137func TestNoDefaultsForBools(t *testing.T) { 138 var opts struct { 139 DefaultBool bool `short:"d" default:"true"` 140 } 141 142 if runtime.GOOS == "windows" { 143 assertParseFail(t, ErrInvalidTag, "boolean flag `/d' may not have default values, they always default to `false' and can only be turned on", &opts) 144 } else { 145 assertParseFail(t, ErrInvalidTag, "boolean flag `-d' may not have default values, they always default to `false' and can only be turned on", &opts) 146 } 147} 148 149func TestUnquoting(t *testing.T) { 150 var tests = []struct { 151 arg string 152 err error 153 value string 154 }{ 155 { 156 arg: "\"abc", 157 err: strconv.ErrSyntax, 158 value: "", 159 }, 160 { 161 arg: "\"\"abc\"", 162 err: strconv.ErrSyntax, 163 value: "", 164 }, 165 { 166 arg: "\"abc\"", 167 err: nil, 168 value: "abc", 169 }, 170 { 171 arg: "\"\\\"abc\\\"\"", 172 err: nil, 173 value: "\"abc\"", 174 }, 175 { 176 arg: "\"\\\"abc\"", 177 err: nil, 178 value: "\"abc", 179 }, 180 } 181 182 for _, test := range tests { 183 var opts defaultOptions 184 185 for _, delimiter := range []bool{false, true} { 186 p := NewParser(&opts, None) 187 188 var err error 189 if delimiter { 190 _, err = p.ParseArgs([]string{"--str=" + test.arg, "--strnot=" + test.arg}) 191 } else { 192 _, err = p.ParseArgs([]string{"--str", test.arg, "--strnot", test.arg}) 193 } 194 195 if test.err == nil { 196 if err != nil { 197 t.Fatalf("Expected no error but got: %v", err) 198 } 199 200 if test.value != opts.String { 201 t.Fatalf("Expected String to be %q but got %q", test.value, opts.String) 202 } 203 if q := strconv.Quote(test.value); q != opts.StringNotUnquoted { 204 t.Fatalf("Expected StringDefault to be %q but got %q", q, opts.StringNotUnquoted) 205 } 206 } else { 207 if err == nil { 208 t.Fatalf("Expected error") 209 } else if e, ok := err.(*Error); ok { 210 if strings.HasPrefix(e.Message, test.err.Error()) { 211 t.Fatalf("Expected error message to end with %q but got %v", test.err.Error(), e.Message) 212 } 213 } 214 } 215 } 216 } 217} 218 219// EnvRestorer keeps a copy of a set of env variables and can restore the env from them 220type EnvRestorer struct { 221 env map[string]string 222} 223 224func (r *EnvRestorer) Restore() { 225 os.Clearenv() 226 227 for k, v := range r.env { 228 os.Setenv(k, v) 229 } 230} 231 232// EnvSnapshot returns a snapshot of the currently set env variables 233func EnvSnapshot() *EnvRestorer { 234 r := EnvRestorer{make(map[string]string)} 235 236 for _, kv := range os.Environ() { 237 parts := strings.SplitN(kv, "=", 2) 238 239 if len(parts) != 2 { 240 panic("got a weird env variable: " + kv) 241 } 242 243 r.env[parts[0]] = parts[1] 244 } 245 246 return &r 247} 248 249type envNestedOptions struct { 250 Foo string `long:"foo" default:"z" env:"FOO"` 251} 252 253type envDefaultOptions struct { 254 Int int `long:"i" default:"1" env:"TEST_I"` 255 Time time.Duration `long:"t" default:"1m" env:"TEST_T"` 256 Map map[string]int `long:"m" default:"a:1" env:"TEST_M" env-delim:";"` 257 Slice []int `long:"s" default:"1" default:"2" env:"TEST_S" env-delim:","` 258 Nested envNestedOptions `group:"nested" namespace:"nested" env-namespace:"NESTED"` 259} 260 261func TestEnvDefaults(t *testing.T) { 262 var tests = []struct { 263 msg string 264 args []string 265 expected envDefaultOptions 266 expectedErr string 267 env map[string]string 268 }{ 269 { 270 msg: "no arguments, no env, expecting default values", 271 args: []string{}, 272 expected: envDefaultOptions{ 273 Int: 1, 274 Time: time.Minute, 275 Map: map[string]int{"a": 1}, 276 Slice: []int{1, 2}, 277 Nested: envNestedOptions{ 278 Foo: "z", 279 }, 280 }, 281 }, 282 { 283 msg: "no arguments, env defaults, expecting env default values", 284 args: []string{}, 285 expected: envDefaultOptions{ 286 Int: 2, 287 Time: 2 * time.Minute, 288 Map: map[string]int{"a": 2, "b": 3}, 289 Slice: []int{4, 5, 6}, 290 Nested: envNestedOptions{ 291 Foo: "a", 292 }, 293 }, 294 env: map[string]string{ 295 "TEST_I": "2", 296 "TEST_T": "2m", 297 "TEST_M": "a:2;b:3", 298 "TEST_S": "4,5,6", 299 "NESTED_FOO": "a", 300 }, 301 }, 302 { 303 msg: "no arguments, malformed env defaults, expecting parse error", 304 args: []string{}, 305 expectedErr: `parsing "two": invalid syntax`, 306 env: map[string]string{ 307 "TEST_I": "two", 308 }, 309 }, 310 { 311 msg: "non-zero value arguments, expecting overwritten arguments", 312 args: []string{"--i=3", "--t=3ms", "--m=c:3", "--s=3", "--nested.foo=\"p\""}, 313 expected: envDefaultOptions{ 314 Int: 3, 315 Time: 3 * time.Millisecond, 316 Map: map[string]int{"c": 3}, 317 Slice: []int{3}, 318 Nested: envNestedOptions{ 319 Foo: "p", 320 }, 321 }, 322 env: map[string]string{ 323 "TEST_I": "2", 324 "TEST_T": "2m", 325 "TEST_M": "a:2;b:3", 326 "TEST_S": "4,5,6", 327 "NESTED_FOO": "a", 328 }, 329 }, 330 { 331 msg: "zero value arguments, expecting overwritten arguments", 332 args: []string{"--i=0", "--t=0ms", "--m=:0", "--s=0", "--nested.foo=\"\""}, 333 expected: envDefaultOptions{ 334 Int: 0, 335 Time: 0, 336 Map: map[string]int{"": 0}, 337 Slice: []int{0}, 338 Nested: envNestedOptions{ 339 Foo: "", 340 }, 341 }, 342 env: map[string]string{ 343 "TEST_I": "2", 344 "TEST_T": "2m", 345 "TEST_M": "a:2;b:3", 346 "TEST_S": "4,5,6", 347 "NESTED_FOO": "a", 348 }, 349 }, 350 } 351 352 oldEnv := EnvSnapshot() 353 defer oldEnv.Restore() 354 355 for _, test := range tests { 356 var opts envDefaultOptions 357 oldEnv.Restore() 358 for envKey, envValue := range test.env { 359 os.Setenv(envKey, envValue) 360 } 361 _, err := NewParser(&opts, None).ParseArgs(test.args) 362 if test.expectedErr != "" { 363 if err == nil { 364 t.Errorf("%s:\nExpected error containing substring %q", test.msg, test.expectedErr) 365 } else if !strings.Contains(err.Error(), test.expectedErr) { 366 t.Errorf("%s:\nExpected error %q to contain substring %q", test.msg, err, test.expectedErr) 367 } 368 } else { 369 if err != nil { 370 t.Fatalf("%s:\nUnexpected error: %v", test.msg, err) 371 } 372 373 if opts.Slice == nil { 374 opts.Slice = []int{} 375 } 376 377 if !reflect.DeepEqual(opts, test.expected) { 378 t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts) 379 } 380 } 381 } 382} 383 384type CustomFlag struct { 385 Value string 386} 387 388func (c *CustomFlag) UnmarshalFlag(s string) error { 389 c.Value = s 390 return nil 391} 392 393func (c *CustomFlag) IsValidValue(s string) error { 394 if !(s == "-1" || s == "-foo") { 395 return errors.New("invalid flag value") 396 } 397 return nil 398} 399 400func TestOptionAsArgument(t *testing.T) { 401 var tests = []struct { 402 args []string 403 expectError bool 404 errType ErrorType 405 errMsg string 406 rest []string 407 }{ 408 { 409 // short option must not be accepted as argument 410 args: []string{"--string-slice", "foobar", "--string-slice", "-o"}, 411 expectError: true, 412 errType: ErrExpectedArgument, 413 errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-o'", 414 }, 415 { 416 // long option must not be accepted as argument 417 args: []string{"--string-slice", "foobar", "--string-slice", "--other-option"}, 418 expectError: true, 419 errType: ErrExpectedArgument, 420 errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `--other-option'", 421 }, 422 { 423 // long option must not be accepted as argument 424 args: []string{"--string-slice", "--"}, 425 expectError: true, 426 errType: ErrExpectedArgument, 427 errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got double dash `--'", 428 }, 429 { 430 // quoted and appended option should be accepted as argument (even if it looks like an option) 431 args: []string{"--string-slice", "foobar", "--string-slice=\"--other-option\""}, 432 }, 433 { 434 // Accept any single character arguments including '-' 435 args: []string{"--string-slice", "-"}, 436 }, 437 { 438 // Do not accept arguments which start with '-' even if the next character is a digit 439 args: []string{"--string-slice", "-3.14"}, 440 expectError: true, 441 errType: ErrExpectedArgument, 442 errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-3.14'", 443 }, 444 { 445 // Do not accept arguments which start with '-' if the next character is not a digit 446 args: []string{"--string-slice", "-character"}, 447 expectError: true, 448 errType: ErrExpectedArgument, 449 errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-character'", 450 }, 451 { 452 args: []string{"-o", "-", "-"}, 453 rest: []string{"-", "-"}, 454 }, 455 { 456 // Accept arguments which start with '-' if the next character is a digit 457 args: []string{"--int-slice", "-3"}, 458 }, 459 { 460 // Accept arguments which start with '-' if the next character is a digit 461 args: []string{"--int16", "-3"}, 462 }, 463 { 464 // Accept arguments which start with '-' if the next character is a digit 465 args: []string{"--float32", "-3.2"}, 466 }, 467 { 468 // Accept arguments which start with '-' if the next character is a digit 469 args: []string{"--float32ptr", "-3.2"}, 470 }, 471 { 472 // Accept arguments for values that pass the IsValidValue fuction for value validators 473 args: []string{"--custom-flag", "-foo"}, 474 }, 475 { 476 // Accept arguments for values that pass the IsValidValue fuction for value validators 477 args: []string{"--custom-flag", "-1"}, 478 }, 479 { 480 // Rejects arguments for values that fail the IsValidValue fuction for value validators 481 args: []string{"--custom-flag", "-2"}, 482 expectError: true, 483 errType: ErrExpectedArgument, 484 errMsg: "invalid flag value", 485 }, 486 } 487 488 var opts struct { 489 StringSlice []string `long:"string-slice"` 490 IntSlice []int `long:"int-slice"` 491 Int16 int16 `long:"int16"` 492 Float32 float32 `long:"float32"` 493 Float32Ptr *float32 `long:"float32ptr"` 494 OtherOption bool `long:"other-option" short:"o"` 495 Custom CustomFlag `long:"custom-flag" short:"c"` 496 } 497 498 for _, test := range tests { 499 if test.expectError { 500 assertParseFail(t, test.errType, test.errMsg, &opts, test.args...) 501 } else { 502 args := assertParseSuccess(t, &opts, test.args...) 503 504 assertStringArray(t, args, test.rest) 505 } 506 } 507} 508 509func TestUnknownFlagHandler(t *testing.T) { 510 511 var opts struct { 512 Flag1 string `long:"flag1"` 513 Flag2 string `long:"flag2"` 514 } 515 516 p := NewParser(&opts, None) 517 518 var unknownFlag1 string 519 var unknownFlag2 bool 520 var unknownFlag3 string 521 522 // Set up a callback to intercept unknown options during parsing 523 p.UnknownOptionHandler = func(option string, arg SplitArgument, args []string) ([]string, error) { 524 if option == "unknownFlag1" { 525 if argValue, ok := arg.Value(); ok { 526 unknownFlag1 = argValue 527 return args, nil 528 } 529 // consume a value from remaining args list 530 unknownFlag1 = args[0] 531 return args[1:], nil 532 } else if option == "unknownFlag2" { 533 // treat this one as a bool switch, don't consume any args 534 unknownFlag2 = true 535 return args, nil 536 } else if option == "unknownFlag3" { 537 if argValue, ok := arg.Value(); ok { 538 unknownFlag3 = argValue 539 return args, nil 540 } 541 // consume a value from remaining args list 542 unknownFlag3 = args[0] 543 return args[1:], nil 544 } 545 546 return args, fmt.Errorf("Unknown flag: %v", option) 547 } 548 549 // Parse args containing some unknown flags, verify that 550 // our callback can handle all of them 551 _, err := p.ParseArgs([]string{"--flag1=stuff", "--unknownFlag1", "blah", "--unknownFlag2", "--unknownFlag3=baz", "--flag2=foo"}) 552 553 if err != nil { 554 assertErrorf(t, "Parser returned unexpected error %v", err) 555 } 556 557 assertString(t, opts.Flag1, "stuff") 558 assertString(t, opts.Flag2, "foo") 559 assertString(t, unknownFlag1, "blah") 560 assertString(t, unknownFlag3, "baz") 561 562 if !unknownFlag2 { 563 assertErrorf(t, "Flag should have been set by unknown handler, but had value: %v", unknownFlag2) 564 } 565 566 // Parse args with unknown flags that callback doesn't handle, verify it returns error 567 _, err = p.ParseArgs([]string{"--flag1=stuff", "--unknownFlagX", "blah", "--flag2=foo"}) 568 569 if err == nil { 570 assertErrorf(t, "Parser should have returned error, but returned nil") 571 } 572} 573 574func TestChoices(t *testing.T) { 575 var opts struct { 576 Choice string `long:"choose" choice:"v1" choice:"v2"` 577 } 578 579 assertParseFail(t, ErrInvalidChoice, "Invalid value `invalid' for option `"+defaultLongOptDelimiter+"choose'. Allowed values are: v1 or v2", &opts, "--choose", "invalid") 580 assertParseSuccess(t, &opts, "--choose", "v2") 581 assertString(t, opts.Choice, "v2") 582} 583 584func TestEmbedded(t *testing.T) { 585 type embedded struct { 586 V bool `short:"v"` 587 } 588 var opts struct { 589 embedded 590 } 591 592 assertParseSuccess(t, &opts, "-v") 593 594 if !opts.V { 595 t.Errorf("Expected V to be true") 596 } 597} 598 599type command struct { 600} 601 602func (c *command) Execute(args []string) error { 603 return nil 604} 605 606func TestCommandHandlerNoCommand(t *testing.T) { 607 var opts = struct { 608 Value bool `short:"v"` 609 }{} 610 611 parser := NewParser(&opts, Default&^PrintErrors) 612 613 var executedCommand Commander 614 var executedArgs []string 615 616 executed := false 617 618 parser.CommandHandler = func(command Commander, args []string) error { 619 executed = true 620 621 executedCommand = command 622 executedArgs = args 623 624 return nil 625 } 626 627 _, err := parser.ParseArgs([]string{"arg1", "arg2"}) 628 629 if err != nil { 630 t.Fatalf("Unexpected parse error: %s", err) 631 } 632 633 if !executed { 634 t.Errorf("Expected command handler to be executed") 635 } 636 637 if executedCommand != nil { 638 t.Errorf("Did not exect an executed command") 639 } 640 641 assertStringArray(t, executedArgs, []string{"arg1", "arg2"}) 642} 643 644func TestCommandHandler(t *testing.T) { 645 var opts = struct { 646 Value bool `short:"v"` 647 648 Command command `command:"cmd"` 649 }{} 650 651 parser := NewParser(&opts, Default&^PrintErrors) 652 653 var executedCommand Commander 654 var executedArgs []string 655 656 executed := false 657 658 parser.CommandHandler = func(command Commander, args []string) error { 659 executed = true 660 661 executedCommand = command 662 executedArgs = args 663 664 return nil 665 } 666 667 _, err := parser.ParseArgs([]string{"cmd", "arg1", "arg2"}) 668 669 if err != nil { 670 t.Fatalf("Unexpected parse error: %s", err) 671 } 672 673 if !executed { 674 t.Errorf("Expected command handler to be executed") 675 } 676 677 if executedCommand == nil { 678 t.Errorf("Expected command handler to be executed") 679 } 680 681 assertStringArray(t, executedArgs, []string{"arg1", "arg2"}) 682} 683