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