1package zli_test 2 3import ( 4 "fmt" 5 "regexp" 6 "strings" 7 "testing" 8 9 "zgo.at/zli" 10) 11 12func ExampleFlags() { 13 // Create new flags from os.Args. 14 f := zli.NewFlags([]string{"example", "-vv", "-f=csv", "-a", "xx", "yy"}) 15 16 // Add a string, bool, and "counter" flag. 17 var ( 18 verbose = f.IntCounter(0, "v", "verbose") 19 all = f.Bool(false, "a", "all") 20 format = f.String("", "f", "format") 21 ) 22 23 // Shift the first argument (i.e. os.Args[1], if any, empty string if there 24 // isn't). Useful to get the "subcommand" name. This works before and after 25 // Parse(). 26 switch f.Shift() { 27 case "help": 28 // Run help 29 case "install": 30 // Run install 31 case "": 32 // Error: need a command (or just print the usage) 33 default: 34 // Error: Unknown command 35 } 36 37 // Parse the shebang! 38 err := f.Parse() 39 if err != nil { 40 // Print error, usage. 41 } 42 43 // You can check if the flag was present on the CLI with Set(). This way you 44 // can distinguish between "was an empty value passed" // (-format '') and 45 // "this flag wasn't on the CLI". 46 if format.Set() { 47 fmt.Println("Format was set to", format.String()) 48 } 49 50 // The IntCounter adds 1 for every time the -v flag is on the CLI. 51 if verbose.Int() > 1 { 52 // ...Print very verbose info. 53 } else if verbose.Int() > 0 { 54 // ...Print less verbose info. 55 } 56 57 // Just a bool! 58 fmt.Println("All:", all.Bool()) 59 60 // f.Args is set to everything that's not a flag or argument. 61 fmt.Println("Remaining:", f.Args) 62 63 // Output: 64 // Format was set to csv 65 // All: true 66 // Remaining: [xx yy] 67} 68 69func TestFlags(t *testing.T) { 70 tests := []struct { 71 name string 72 args []string 73 flags func(*zli.Flags) []interface{} 74 want string 75 wantErr string 76 }{ 77 // No arguments, no problem. 78 {"nil args", nil, 79 func(f *zli.Flags) []interface{} { 80 return []interface{}{f.Bool(false, "b")} 81 }, ` 82 bool 1 → false 83 args → 0 [] 84 `, ""}, 85 {"empty args", []string{}, 86 func(f *zli.Flags) []interface{} { 87 return []interface{}{f.Bool(false, "b")} 88 }, ` 89 bool 1 → false 90 args → 0 [] 91 `, ""}, 92 {"prog name", []string{"progname"}, 93 func(f *zli.Flags) []interface{} { 94 return []interface{}{f.Bool(false, "b")} 95 }, ` 96 bool 1 → false 97 args → 0 [] 98 `, ""}, 99 100 // Get positional arguments 101 {"1 arg", []string{"progname", "pos1"}, 102 func(f *zli.Flags) []interface{} { 103 return []interface{}{f.String("", "s")} 104 }, ` 105 string 1 → "" 106 args → 1 [pos1] 107 `, ""}, 108 {"args with space", []string{"progname", "pos1", "pos 2", "pos\n3"}, 109 func(f *zli.Flags) []interface{} { 110 return []interface{}{f.String("", "s")} 111 }, ` 112 string 1 → "" 113 args → 3 [pos1 pos 2 pos 114 3] 115 `, ""}, 116 {"after flag", []string{"progname", "-s", "arg", "pos 1", "pos 2"}, 117 func(f *zli.Flags) []interface{} { 118 return []interface{}{f.String("", "s")} 119 }, ` 120 string 1 → "arg" 121 args → 2 [pos 1 pos 2] 122 `, ""}, 123 {"before flag", []string{"progname", "pos 1", "pos 2", "-s", "arg"}, 124 func(f *zli.Flags) []interface{} { 125 return []interface{}{f.String("", "s")} 126 }, ` 127 string 1 → "arg" 128 args → 2 [pos 1 pos 2] 129 `, ""}, 130 {"before and after flag", []string{"progname", "pos 1", "-s", "arg", "pos 2"}, 131 func(f *zli.Flags) []interface{} { 132 return []interface{}{f.String("", "s")} 133 }, ` 134 string 1 → "arg" 135 args → 2 [pos 1 pos 2] 136 `, ""}, 137 138 {"single - is a valid argument", []string{"progname", "-s", "-", "-"}, 139 func(f *zli.Flags) []interface{} { 140 return []interface{}{f.String("", "s")} 141 }, ` 142 string 1 → "-" 143 args → 1 [-] 144 `, ""}, 145 // Make sure parsing is stopped after -- 146 { 147 "-- bool", []string{"prog", "-b", "--"}, 148 func(f *zli.Flags) []interface{} { 149 return []interface{}{ 150 f.Bool(false, "b"), 151 } 152 }, ` 153 bool 1 → true 154 args → 0 [] 155 `, ""}, 156 // 157 /* 158 {[]string{"prog", "-b", "--", "-str"}, "", ` 159 str | false | "default" 160 bool | true | true 161 args | 1 | [-str] 162 `}, 163 {[]string{"prog", "-b", "--", ""}, "", ` 164 str | false | "default" 165 bool | true | true 166 args | 1 | [] 167 `}, 168 // Various -- 169 */ 170 171 // Basic test for all the different flag types. 172 {"bool", []string{"prog", "-b"}, 173 func(f *zli.Flags) []interface{} { 174 return []interface{}{f.Bool(false, "b")} 175 }, ` 176 bool 1 → true 177 args → 0 [] 178 `, ""}, 179 {"string", []string{"prog", "-s", "val"}, 180 func(f *zli.Flags) []interface{} { 181 return []interface{}{f.String("", "s")} 182 }, ` 183 string 1 → "val" 184 args → 0 [] 185 `, ""}, 186 {"int", []string{"prog", "-i", "42"}, 187 func(f *zli.Flags) []interface{} { 188 return []interface{}{f.Int(0, "i")} 189 }, ` 190 int 1 → 42 191 args → 0 [] 192 `, ""}, 193 {"int64", []string{"prog", "-i", "42"}, 194 func(f *zli.Flags) []interface{} { 195 return []interface{}{f.Int64(0, "i")} 196 }, ` 197 int64 1 → 42 198 args → 0 [] 199 `, ""}, 200 {"int64", []string{"prog", "-i", "1_000_000"}, 201 func(f *zli.Flags) []interface{} { 202 return []interface{}{f.Int64(0, "i")} 203 }, ` 204 int64 1 → 1000000 205 args → 0 [] 206 `, ""}, 207 {"int64", []string{"prog", "-i", "0x10"}, 208 func(f *zli.Flags) []interface{} { 209 return []interface{}{f.Int64(0, "i")} 210 }, ` 211 int64 1 → 16 212 args → 0 [] 213 `, ""}, 214 {"float64", []string{"prog", "-i", "42.666"}, 215 func(f *zli.Flags) []interface{} { 216 return []interface{}{f.Float64(0, "i")} 217 }, ` 218 float64 1 → 42.666000 219 args → 0 [] 220 `, ""}, 221 {"intcounter", []string{"prog", "-i", "-i", "-i"}, 222 func(f *zli.Flags) []interface{} { 223 return []interface{}{f.IntCounter(0, "i")} 224 }, ` 225 int 1 → 3 226 args → 0 [] 227 `, ""}, 228 {"stringlist", []string{"prog", "-s", "a", "-s", "b", "-s", "c"}, 229 func(f *zli.Flags) []interface{} { 230 return []interface{}{f.StringList(nil, "s")} 231 }, ` 232 list 1 → [a b c] 233 args → 0 [] 234 `, ""}, 235 {"intlist", []string{"prog", "-s", "1", "-s", "3", "-s", "5"}, 236 func(f *zli.Flags) []interface{} { 237 return []interface{}{f.IntList(nil, "s")} 238 }, ` 239 list 1 → [1 3 5] 240 args → 0 [] 241 `, ""}, 242 243 // Various kinds of wrong input. 244 {"unknown", []string{"prog", "-x"}, 245 func(f *zli.Flags) []interface{} { 246 return []interface{}{f.String("", "s")} 247 }, ` 248 string 1 → "" 249 args → 1 [-x] 250 `, `unknown flag: "-x"`}, 251 {"no argument", []string{"prog", "-s"}, 252 func(f *zli.Flags) []interface{} { 253 return []interface{}{f.String("", "s")} 254 }, ` 255 string 1 → "" 256 args → 1 [-s] 257 `, "-s: needs an argument"}, 258 {"multiple", []string{"prog", "-s=a", "-s=b"}, 259 func(f *zli.Flags) []interface{} { 260 return []interface{}{f.String("", "s")} 261 }, ` 262 string 1 → "a" 263 args → 2 [-s=a -s=b] 264 `, `flag given more than once: "-s=b"`}, 265 {"not an int", []string{"prog", "-i=no"}, 266 func(f *zli.Flags) []interface{} { 267 return []interface{}{f.Int(42, "i")} 268 }, ` 269 int 1 → 42 270 args → 1 [-i=no] 271 `, `-i=no: invalid syntax (must be a number)`}, 272 {"not an int64", []string{"prog", "-i=no"}, 273 func(f *zli.Flags) []interface{} { 274 return []interface{}{f.Int64(42, "i")} 275 }, ` 276 int64 1 → 42 277 args → 1 [-i=no] 278 `, `-i=no: invalid syntax (must be a number)`}, 279 {"not a float", []string{"prog", "-i=no"}, 280 func(f *zli.Flags) []interface{} { 281 return []interface{}{f.Float64(42, "i")} 282 }, ` 283 float64 1 → 42.000000 284 args → 1 [-i=no] 285 `, `-i=no: invalid syntax (must be a number)`}, 286 287 // Argument parsing 288 {"-s=arg", []string{"prog", "-s=xx"}, 289 func(f *zli.Flags) []interface{} { 290 return []interface{}{ 291 f.String("default", "s"), 292 } 293 }, ` 294 string 1 → "xx" 295 args → 0 [] 296 `, ""}, 297 {"--s=arg", []string{"prog", "--s=xx"}, 298 func(f *zli.Flags) []interface{} { 299 return []interface{}{ 300 f.String("default", "s"), 301 } 302 }, ` 303 string 1 → "xx" 304 args → 0 [] 305 `, ""}, 306 {"--s=-arg", []string{"prog", "--s=-xx"}, 307 func(f *zli.Flags) []interface{} { 308 return []interface{}{ 309 f.String("default", "s"), 310 } 311 }, ` 312 string 1 → "-xx" 313 args → 0 [] 314 `, ""}, 315 {"--s=-o", []string{"prog", "--s=-o"}, 316 func(f *zli.Flags) []interface{} { 317 return []interface{}{ 318 f.String("default", "s"), 319 f.String("default", "o"), 320 } 321 }, ` 322 string 1 → "-o" 323 string 2 → "default" 324 args → 0 [] 325 `, ""}, 326 // TODO: this should probably be an error? 327 {"--s -o", []string{"prog", "-s", "-o"}, 328 func(f *zli.Flags) []interface{} { 329 return []interface{}{ 330 f.String("", "s"), 331 f.String("", "o"), 332 } 333 }, ` 334 string 1 → "-o" 335 string 2 → "" 336 args → 0 [] 337 `, ""}, 338 {"--s arg", []string{"prog", "--s", "xx"}, 339 func(f *zli.Flags) []interface{} { 340 return []interface{}{ 341 f.String("default", "s"), 342 } 343 }, ` 344 string 1 → "xx" 345 args → 0 [] 346 `, ""}, 347 {"blank =", []string{"prog", "-s="}, 348 func(f *zli.Flags) []interface{} { 349 return []interface{}{ 350 f.String("default", "s"), 351 } 352 }, ` 353 string 1 → "" 354 args → 0 [] 355 `, ""}, 356 {"blank space", []string{"prog", "-s", ""}, 357 func(f *zli.Flags) []interface{} { 358 return []interface{}{ 359 f.String("default", "s"), 360 } 361 }, ` 362 string 1 → "" 363 args → 0 [] 364 `, ""}, 365 366 // Okay for booleans to have multiple flags, as it doesn't really 367 // matter. 368 {"multiple bool", []string{"prog", "-b", "-b"}, 369 func(f *zli.Flags) []interface{} { 370 return []interface{}{f.Bool(false, "b")} 371 }, ` 372 bool 1 → true 373 args → 0 [] 374 `, ""}, 375 376 // Group -ab as -a -b if they're booleans. 377 {"group bool", []string{"prog", "-a", "-b"}, 378 func(f *zli.Flags) []interface{} { 379 return []interface{}{ 380 f.Bool(false, "a"), 381 f.Bool(false, "b"), 382 } 383 }, ` 384 bool 1 → true 385 bool 2 → true 386 args → 0 [] 387 `, ""}, 388 {"group bool", []string{"prog", "-ab"}, 389 func(f *zli.Flags) []interface{} { 390 return []interface{}{ 391 f.Bool(false, "a"), 392 f.Bool(false, "b"), 393 } 394 }, ` 395 bool 1 → true 396 bool 2 → true 397 args → 0 [] 398 `, ""}, 399 {"group bool only with single -", []string{"prog", "--ab"}, 400 func(f *zli.Flags) []interface{} { 401 return []interface{}{ 402 f.Bool(false, "a"), 403 f.Bool(false, "b"), 404 } 405 }, ` 406 bool 1 → false 407 bool 2 → false 408 args → 1 [--ab] 409 `, `unknown flag: "--ab"`}, 410 {"long flag overrides grouped bool", []string{"prog", "--ab", "x"}, 411 func(f *zli.Flags) []interface{} { 412 return []interface{}{ 413 f.String("", "ab"), 414 f.Bool(false, "a"), 415 f.Bool(false, "b"), 416 } 417 }, ` 418 string 1 → "x" 419 bool 2 → false 420 bool 3 → false 421 args → 0 [] 422 `, ""}, 423 424 {"arguments starting with - work", []string{"prog", "-b", "-arg", "--long", "--long"}, 425 func(f *zli.Flags) []interface{} { 426 return []interface{}{ 427 f.String("", "b"), 428 f.String("", "l", "long"), 429 } 430 }, ` 431 string 1 → "-arg" 432 string 2 → "--long" 433 args → 0 [] 434 `, ""}, 435 436 {"prefer_long", []string{"prog", "-long"}, 437 func(f *zli.Flags) []interface{} { 438 return []interface{}{ 439 f.Bool(false, "long"), 440 f.Bool(false, "l"), 441 f.Bool(false, "o"), 442 f.Bool(false, "n"), 443 f.Bool(false, "g"), 444 } 445 }, ` 446 bool 1 → true 447 bool 2 → false 448 bool 3 → false 449 bool 4 → false 450 bool 5 → false 451 args → 0 [] 452 `, ""}, 453 454 {"prefer_long", []string{"prog", "-long"}, 455 func(f *zli.Flags) []interface{} { 456 return []interface{}{ 457 f.Bool(false, "l"), 458 f.Bool(false, "o"), 459 f.Bool(false, "long"), 460 f.Bool(false, "n"), 461 f.Bool(false, "g"), 462 } 463 }, ` 464 bool 1 → false 465 bool 2 → false 466 bool 3 → true 467 bool 4 → false 468 bool 5 → false 469 args → 0 [] 470 `, ""}, 471 } 472 473 type ( 474 booler interface{ Bool() bool } 475 stringer interface{ String() string } 476 inter interface{ Int() int } 477 int64er interface{ Int64() int64 } 478 floater interface{ Float64() float64 } 479 stringlister interface{ Strings() []string } 480 intlister interface{ Ints() []int } 481 ) 482 483 for _, tt := range tests { 484 t.Run(tt.name, func(t *testing.T) { 485 flag := zli.NewFlags(tt.args) 486 setFlags := tt.flags(&flag) 487 err := flag.Parse() 488 if !errorContains(err, tt.wantErr) { 489 t.Fatalf("wrong error\nout: %v\nwant: %v", err, tt.wantErr) 490 } 491 492 var out string 493 for i, f := range setFlags { 494 switch ff := f.(type) { 495 case booler: 496 out += fmt.Sprintf("bool %d → %t\n", i+1, ff.Bool()) 497 case stringer: 498 out += fmt.Sprintf("string %d → %q\n", i+1, ff.String()) 499 case inter: 500 out += fmt.Sprintf("int %d → %d\n", i+1, ff.Int()) 501 case int64er: 502 out += fmt.Sprintf("int64 %d → %d\n", i+1, ff.Int64()) 503 case floater: 504 out += fmt.Sprintf("float64 %d → %f\n", i+1, ff.Float64()) 505 case stringlister: 506 out += fmt.Sprintf("list %d → %v\n", i+1, ff.Strings()) 507 case intlister: 508 out += fmt.Sprintf("list %d → %v\n", i+1, ff.Ints()) 509 default: 510 t.Fatalf("unknown type: %T", f) 511 } 512 } 513 out += fmt.Sprintf("args → %d %v", len(flag.Args), flag.Args) 514 515 want := strings.TrimSpace(strings.ReplaceAll(tt.want, "\t", "")) 516 want = regexp.MustCompile(`\s+→\s+`).ReplaceAllString(want, " → ") 517 518 // Indent so it looks nicer. 519 out = " " + strings.ReplaceAll(out, "\n", "\n ") 520 want = " " + strings.ReplaceAll(want, "\n", "\n ") 521 522 if out != want { 523 t.Errorf("\nout:\n%s\nwant:\n%s\n", out, want) 524 } 525 }) 526 } 527} 528 529func TestShiftCommand(t *testing.T) { 530 tests := []struct { 531 in []string 532 commands []string 533 want string 534 }{ 535 {[]string{""}, nil, zli.CommandNoneGiven}, 536 {[]string{"-a"}, nil, zli.CommandNoneGiven}, 537 538 {[]string{"help"}, []string{"asd"}, zli.CommandUnknown}, 539 540 {[]string{"help"}, []string{"help", "heee"}, "help"}, 541 {[]string{"hel"}, []string{"help", "heee"}, "help"}, 542 {[]string{"he"}, []string{"help", "heee"}, zli.CommandAmbiguous}, 543 544 {[]string{"usage"}, []string{"help", "usage=help"}, "help"}, 545 546 {[]string{"create", "-db=x"}, []string{"create"}, "create"}, 547 {[]string{"-flag", "create", "-db=x"}, []string{"create"}, "create"}, 548 549 {[]string{"-flag", "create", "-db=x"}, nil, "create"}, 550 } 551 552 for _, tt := range tests { 553 t.Run("", func(t *testing.T) { 554 f := zli.NewFlags(append([]string{"prog"}, tt.in...)) 555 got := f.ShiftCommand(tt.commands...) 556 f.Bool(false, "a") 557 f.Parse() 558 559 if got != tt.want { 560 t.Errorf("\ngot: %q\nwant: %q", got, tt.want) 561 } 562 }) 563 } 564} 565 566/* 567func TestDoubleParse(t *testing.T) { 568 f := zli.NewFlags([]string{"prog", "-global", "cmd", "-other"}) 569 f.IgnoreUnknown(true) 570 571 var global = f.Bool(false, "global") 572 { 573 err := f.Parse() 574 if err != nil { 575 t.Fatal(err) 576 } 577 if !global.Set() { 578 t.Fatal("global not set") 579 } 580 } 581 582 t.Log(f.Args) 583 f.IgnoreUnknown(false) 584 var other = f.Bool(false, "other") 585 err := f.Parse() 586 if err != nil { 587 t.Fatal(err) 588 } 589 590 if other.Set() { 591 t.Error("other not set", f.Args) 592 } 593 if len(f.Args) != 1 && f.Args[1] != "cmd" { 594 t.Error(f.Args) 595 } 596} 597*/ 598 599// Just to make sure it's not ridiculously slow or anything. 600func BenchmarkFlag(b *testing.B) { 601 b.ReportAllocs() 602 var err error 603 for n := 0; n < b.N; n++ { 604 flag := zli.NewFlags([]string{"prog", "cmd", "-vv", "-V", "str foo"}) 605 flag.Shift() 606 flag.String("", "s", "str") 607 flag.Bool(false, "V", "version") 608 flag.IntCounter(0, "v", "verbose") 609 err = flag.Parse() 610 } 611 _ = err 612} 613 614func errorContains(out error, want string) bool { 615 if out == nil { 616 return want == "" 617 } 618 if want == "" { 619 return false 620 } 621 return strings.Contains(out.Error(), want) 622} 623