github.com/alecthomas/kong@v0.9.1-0.20240410131203-2ab5733f1179/mapper_test.go (about) 1 package kong_test 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "math" 8 "net/url" 9 "os" 10 "path/filepath" 11 "reflect" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/alecthomas/assert/v2" 17 "github.com/alecthomas/kong" 18 ) 19 20 func TestValueMapper(t *testing.T) { 21 var cli struct { 22 Flag string 23 } 24 k := mustNew(t, &cli, kong.ValueMapper(&cli.Flag, testMooMapper{})) 25 _, err := k.Parse(nil) 26 assert.NoError(t, err) 27 assert.Equal(t, "", cli.Flag) 28 _, err = k.Parse([]string{"--flag"}) 29 assert.NoError(t, err) 30 assert.Equal(t, "MOO", cli.Flag) 31 } 32 33 type textUnmarshalerValue int 34 35 func (m *textUnmarshalerValue) UnmarshalText(text []byte) error { 36 s := string(text) 37 if s == "hello" { 38 *m = 10 39 } else { 40 return fmt.Errorf("expected \"hello\"") 41 } 42 return nil 43 } 44 45 func TestTextUnmarshaler(t *testing.T) { 46 var cli struct { 47 Value textUnmarshalerValue 48 } 49 p := mustNew(t, &cli) 50 _, err := p.Parse([]string{"--value=hello"}) 51 assert.NoError(t, err) 52 assert.Equal(t, 10, int(cli.Value)) 53 _, err = p.Parse([]string{"--value=other"}) 54 assert.Error(t, err) 55 } 56 57 type jsonUnmarshalerValue string 58 59 func (j *jsonUnmarshalerValue) UnmarshalJSON(text []byte) error { 60 var v string 61 err := json.Unmarshal(text, &v) 62 if err != nil { 63 return err 64 } 65 *j = jsonUnmarshalerValue(strings.ToUpper(v)) 66 return nil 67 } 68 69 func TestJSONUnmarshaler(t *testing.T) { 70 var cli struct { 71 Value jsonUnmarshalerValue 72 } 73 p := mustNew(t, &cli) 74 _, err := p.Parse([]string{"--value=\"hello\""}) 75 assert.NoError(t, err) 76 assert.Equal(t, "HELLO", string(cli.Value)) 77 } 78 79 func TestNamedMapper(t *testing.T) { 80 var cli struct { 81 Flag string `type:"moo"` 82 } 83 k := mustNew(t, &cli, kong.NamedMapper("moo", testMooMapper{})) 84 _, err := k.Parse(nil) 85 assert.NoError(t, err) 86 assert.Equal(t, "", cli.Flag) 87 _, err = k.Parse([]string{"--flag"}) 88 assert.NoError(t, err) 89 assert.Equal(t, "MOO", cli.Flag) 90 } 91 92 type testMooMapper struct { 93 text string 94 } 95 96 func (t testMooMapper) Decode(ctx *kong.DecodeContext, target reflect.Value) error { 97 if t.text == "" { 98 target.SetString("MOO") 99 } else { 100 target.SetString(t.text) 101 } 102 return nil 103 } 104 func (testMooMapper) IsBool() bool { return true } 105 106 func TestTimeMapper(t *testing.T) { 107 var cli struct { 108 Flag time.Time `format:"2006"` 109 } 110 k := mustNew(t, &cli) 111 _, err := k.Parse([]string{"--flag=2008"}) 112 assert.NoError(t, err) 113 expected, err := time.Parse("2006", "2008") 114 assert.NoError(t, err) 115 assert.Equal(t, 2008, expected.Year()) 116 assert.Equal(t, expected, cli.Flag) 117 } 118 119 func TestDurationMapper(t *testing.T) { 120 var cli struct { 121 Flag time.Duration 122 } 123 k := mustNew(t, &cli) 124 _, err := k.Parse([]string{"--flag=5s"}) 125 assert.NoError(t, err) 126 assert.Equal(t, time.Second*5, cli.Flag) 127 } 128 129 func TestDurationMapperJSONResolver(t *testing.T) { 130 var cli struct { 131 Flag time.Duration 132 } 133 resolver, err := kong.JSON(strings.NewReader(`{"flag": 5000000000}`)) 134 assert.NoError(t, err) 135 k := mustNew(t, &cli, kong.Resolvers(resolver)) 136 _, err = k.Parse(nil) 137 assert.NoError(t, err) 138 assert.Equal(t, time.Second*5, cli.Flag) 139 } 140 141 func TestSplitEscaped(t *testing.T) { 142 assert.Equal(t, []string{"a", "b"}, kong.SplitEscaped("a,b", ',')) 143 assert.Equal(t, []string{"a,b", "c"}, kong.SplitEscaped(`a\,b,c`, ',')) 144 assert.Equal(t, []string{"a,b,c"}, kong.SplitEscaped(`a,b,c`, -1)) 145 } 146 147 func TestJoinEscaped(t *testing.T) { 148 assert.Equal(t, `a,b`, kong.JoinEscaped([]string{"a", "b"}, ',')) 149 assert.Equal(t, `a\,b,c`, kong.JoinEscaped([]string{`a,b`, `c`}, ',')) 150 assert.Equal(t, kong.JoinEscaped(kong.SplitEscaped(`a\,b,c`, ','), ','), `a\,b,c`) 151 } 152 153 func TestMapWithNamedTypes(t *testing.T) { 154 var cli struct { 155 TypedValue map[string]string `type:":moo"` 156 TypedKey map[string]string `type:"upper:"` 157 } 158 k := mustNew(t, &cli, kong.NamedMapper("moo", testMooMapper{}), kong.NamedMapper("upper", testUppercaseMapper{})) 159 _, err := k.Parse([]string{"--typed-value", "first=5s", "--typed-value", "second=10s"}) 160 assert.NoError(t, err) 161 assert.Equal(t, map[string]string{"first": "MOO", "second": "MOO"}, cli.TypedValue) 162 _, err = k.Parse([]string{"--typed-key", "first=5s", "--typed-key", "second=10s"}) 163 assert.NoError(t, err) 164 assert.Equal(t, map[string]string{"FIRST": "5s", "SECOND": "10s"}, cli.TypedKey) 165 } 166 167 func TestMapWithMultipleValues(t *testing.T) { 168 var cli struct { 169 Value map[string]string 170 } 171 k := mustNew(t, &cli) 172 _, err := k.Parse([]string{"--value=a=b;c=d"}) 173 assert.NoError(t, err) 174 assert.Equal(t, map[string]string{"a": "b", "c": "d"}, cli.Value) 175 } 176 177 func TestMapWithDifferentSeparator(t *testing.T) { 178 var cli struct { 179 Value map[string]string `mapsep:","` 180 } 181 k := mustNew(t, &cli) 182 _, err := k.Parse([]string{"--value=a=b,c=d"}) 183 assert.NoError(t, err) 184 assert.Equal(t, map[string]string{"a": "b", "c": "d"}, cli.Value) 185 } 186 187 func TestMapWithNoSeparator(t *testing.T) { 188 var cli struct { 189 Slice []string `sep:"none"` 190 Value map[string]string `mapsep:"none"` 191 } 192 k := mustNew(t, &cli) 193 _, err := k.Parse([]string{"--slice=a,n,c", "--value=a=b;n=d"}) 194 assert.NoError(t, err) 195 assert.Equal(t, []string{"a,n,c"}, cli.Slice) 196 assert.Equal(t, map[string]string{"a": "b;n=d"}, cli.Value) 197 } 198 199 func TestURLMapper(t *testing.T) { 200 var cli struct { 201 URL *url.URL `arg:""` 202 } 203 p := mustNew(t, &cli) 204 _, err := p.Parse([]string{"http://w3.org"}) 205 assert.NoError(t, err) 206 assert.Equal(t, "http://w3.org", cli.URL.String()) 207 _, err = p.Parse([]string{":foo"}) 208 assert.Error(t, err) 209 } 210 211 func TestSliceConsumesRemainingPositionalArgs(t *testing.T) { 212 var cli struct { 213 Remainder []string `arg:""` 214 } 215 p := mustNew(t, &cli) 216 _, err := p.Parse([]string{"--", "ls", "-lart"}) 217 assert.NoError(t, err) 218 assert.Equal(t, []string{"ls", "-lart"}, cli.Remainder) 219 } 220 221 func TestPassthroughStopsParsing(t *testing.T) { 222 type cli struct { 223 Interactive bool `short:"i"` 224 Image string `arg:""` 225 Argv []string `arg:"" optional:"" passthrough:""` 226 } 227 228 var actual cli 229 p := mustNew(t, &actual) 230 231 _, err := p.Parse([]string{"alpine", "sudo", "-i", "true"}) 232 assert.NoError(t, err) 233 assert.Equal(t, cli{ 234 Interactive: false, 235 Image: "alpine", 236 Argv: []string{"sudo", "-i", "true"}, 237 }, actual) 238 239 _, err = p.Parse([]string{"alpine", "-i", "sudo", "-i", "true"}) 240 assert.NoError(t, err) 241 assert.Equal(t, cli{ 242 Interactive: true, 243 Image: "alpine", 244 Argv: []string{"sudo", "-i", "true"}, 245 }, actual) 246 } 247 248 type mappedValue struct { 249 decoded string 250 } 251 252 func (m *mappedValue) Decode(ctx *kong.DecodeContext) error { 253 err := ctx.Scan.PopValueInto("mapped", &m.decoded) 254 return err 255 } 256 257 func TestMapperValue(t *testing.T) { 258 var cli struct { 259 Value mappedValue `arg:""` 260 } 261 p := mustNew(t, &cli) 262 _, err := p.Parse([]string{"foo"}) 263 assert.NoError(t, err) 264 assert.Equal(t, "foo", cli.Value.decoded) 265 } 266 267 func TestFileContentFlag(t *testing.T) { 268 var cli struct { 269 File kong.FileContentFlag 270 } 271 f, err := os.CreateTemp("", "") 272 assert.NoError(t, err) 273 defer os.Remove(f.Name()) 274 fmt.Fprint(f, "hello world") 275 f.Close() 276 _, err = mustNew(t, &cli).Parse([]string{"--file", f.Name()}) 277 assert.NoError(t, err) 278 assert.Equal(t, []byte("hello world"), []byte(cli.File)) 279 } 280 281 func TestNamedFileContentFlag(t *testing.T) { 282 var cli struct { 283 File kong.NamedFileContentFlag 284 } 285 f, err := os.CreateTemp("", "") 286 assert.NoError(t, err) 287 defer os.Remove(f.Name()) 288 fmt.Fprint(f, "hello world") 289 f.Close() 290 _, err = mustNew(t, &cli).Parse([]string{"--file", f.Name()}) 291 assert.NoError(t, err) 292 assert.Equal(t, []byte("hello world"), cli.File.Contents) 293 assert.Equal(t, f.Name(), cli.File.Filename) 294 } 295 296 func TestNamedSliceTypesDontHaveEllipsis(t *testing.T) { 297 var cli struct { 298 File kong.FileContentFlag 299 } 300 b := bytes.NewBuffer(nil) 301 parser := mustNew(t, &cli, kong.Writers(b, b), kong.Exit(func(int) { panic("exit") })) 302 // Ensure that --help 303 assert.Panics(t, func() { 304 _, err := parser.Parse([]string{"--help"}) 305 assert.NoError(t, err) 306 }) 307 assert.NotContains(t, b.String(), `--file=FILE-CONTENT-FLAG,...`) 308 } 309 310 func TestCounter(t *testing.T) { 311 var cli struct { 312 Int int `type:"counter" short:"i"` 313 Uint uint `type:"counter" short:"u"` 314 Float float64 `type:"counter" short:"f"` 315 } 316 p := mustNew(t, &cli) 317 318 _, err := p.Parse([]string{"--int", "--int", "--int"}) 319 assert.NoError(t, err) 320 assert.Equal(t, 3, cli.Int) 321 322 _, err = p.Parse([]string{"--int=5"}) 323 assert.NoError(t, err) 324 assert.Equal(t, 5, cli.Int) 325 326 _, err = p.Parse([]string{"-iii"}) 327 assert.NoError(t, err) 328 assert.Equal(t, 3, cli.Int) 329 330 _, err = p.Parse([]string{"-uuu"}) 331 assert.NoError(t, err) 332 assert.Equal(t, uint(3), cli.Uint) 333 334 _, err = p.Parse([]string{"-fff"}) 335 assert.NoError(t, err) 336 assert.Equal(t, 3., cli.Float) 337 } 338 339 func TestNumbers(t *testing.T) { 340 type CLI struct { 341 F32 float32 342 F64 float64 343 I8 int8 344 I16 int16 345 I32 int32 346 I64 int64 347 U8 uint8 348 U16 uint16 349 U32 uint32 350 U64 uint64 351 } 352 var cli CLI 353 p := mustNew(t, &cli) 354 t.Run("Max", func(t *testing.T) { 355 _, err := p.Parse([]string{ 356 "--f-32", fmt.Sprintf("%v", math.MaxFloat32), 357 "--f-64", fmt.Sprintf("%v", math.MaxFloat64), 358 "--i-8", fmt.Sprintf("%v", int8(math.MaxInt8)), //nolint:perfsprint // want int8 359 "--i-16", fmt.Sprintf("%v", int16(math.MaxInt16)), //nolint:perfsprint // want int16 360 "--i-32", fmt.Sprintf("%v", int32(math.MaxInt32)), //nolint:perfsprint // want int32 361 "--i-64", fmt.Sprintf("%v", int64(math.MaxInt64)), //nolint:perfsprint // want int64 362 "--u-8", fmt.Sprintf("%v", uint8(math.MaxUint8)), //nolint:perfsprint // want uint8 363 "--u-16", fmt.Sprintf("%v", uint16(math.MaxUint16)), //nolint:perfsprint // want uint16 364 "--u-32", fmt.Sprintf("%v", uint32(math.MaxUint32)), //nolint:perfsprint // want uint32 365 "--u-64", fmt.Sprintf("%v", uint64(math.MaxUint64)), //nolint:perfsprint // want uint64 366 }) 367 assert.NoError(t, err) 368 assert.Equal(t, CLI{ 369 F32: math.MaxFloat32, 370 F64: math.MaxFloat64, 371 I8: math.MaxInt8, 372 I16: math.MaxInt16, 373 I32: math.MaxInt32, 374 I64: math.MaxInt64, 375 U8: math.MaxUint8, 376 U16: math.MaxUint16, 377 U32: math.MaxUint32, 378 U64: math.MaxUint64, 379 }, cli) 380 }) 381 t.Run("Min", func(t *testing.T) { 382 _, err := p.Parse([]string{ 383 fmt.Sprintf("--i-8=%v", int8(math.MinInt8)), 384 fmt.Sprintf("--i-16=%v", int16(math.MinInt16)), 385 fmt.Sprintf("--i-32=%v", int32(math.MinInt32)), 386 fmt.Sprintf("--i-64=%v", int64(math.MinInt64)), 387 fmt.Sprintf("--u-8=%v", 0), 388 fmt.Sprintf("--u-16=%v", 0), 389 fmt.Sprintf("--u-32=%v", 0), 390 fmt.Sprintf("--u-64=%v", 0), 391 }) 392 assert.NoError(t, err) 393 assert.Equal(t, CLI{ 394 I8: math.MinInt8, 395 I16: math.MinInt16, 396 I32: math.MinInt32, 397 I64: math.MinInt64, 398 }, cli) 399 }) 400 } 401 402 func TestJSONLargeNumber(t *testing.T) { 403 // Make sure that large numbers are not internally converted to 404 // scientific notation when the mapper parses the values. 405 // (Scientific notation is e.g. `1e+06` instead of `1000000`.) 406 407 // Large signed integers: 408 { 409 var cli struct { 410 N int64 411 } 412 json := `{"n": 1000000}` 413 r, err := kong.JSON(strings.NewReader(json)) 414 assert.NoError(t, err) 415 parser := mustNew(t, &cli, kong.Resolvers(r)) 416 _, err = parser.Parse([]string{}) 417 assert.NoError(t, err) 418 assert.Equal(t, int64(1000000), cli.N) 419 } 420 421 // Large unsigned integers: 422 { 423 var cli struct { 424 N uint64 425 } 426 json := `{"n": 1000000}` 427 r, err := kong.JSON(strings.NewReader(json)) 428 assert.NoError(t, err) 429 parser := mustNew(t, &cli, kong.Resolvers(r)) 430 _, err = parser.Parse([]string{}) 431 assert.NoError(t, err) 432 assert.Equal(t, uint64(1000000), cli.N) 433 } 434 435 // Large floats: 436 { 437 var cli struct { 438 N float64 439 } 440 json := `{"n": 1000000.1}` 441 r, err := kong.JSON(strings.NewReader(json)) 442 assert.NoError(t, err) 443 parser := mustNew(t, &cli, kong.Resolvers(r)) 444 _, err = parser.Parse([]string{}) 445 assert.NoError(t, err) 446 assert.Equal(t, float64(1000000.1), cli.N) 447 } 448 } 449 450 func TestFileMapper(t *testing.T) { 451 type CLI struct { 452 File *os.File `arg:""` 453 } 454 var cli CLI 455 p := mustNew(t, &cli) 456 _, err := p.Parse([]string{"testdata/file.txt"}) 457 assert.NoError(t, err) 458 assert.NotZero(t, cli.File) 459 _ = cli.File.Close() 460 _, err = p.Parse([]string{"testdata/missing.txt"}) 461 assert.Error(t, err) 462 assert.Contains(t, err.Error(), "missing.txt:") 463 assert.IsError(t, err, os.ErrNotExist) 464 _, err = p.Parse([]string{"-"}) 465 assert.NoError(t, err) 466 assert.Equal(t, os.Stdin, cli.File) 467 } 468 469 func TestFileContentMapper(t *testing.T) { 470 type CLI struct { 471 File []byte `type:"filecontent"` 472 } 473 var cli CLI 474 p := mustNew(t, &cli) 475 _, err := p.Parse([]string{"--file", "testdata/file.txt"}) 476 assert.NoError(t, err) 477 assert.Equal(t, []byte(`Hello world.`), cli.File) 478 p = mustNew(t, &cli) 479 _, err = p.Parse([]string{"--file", "testdata/missing.txt"}) 480 assert.Error(t, err) 481 assert.Contains(t, err.Error(), "missing.txt:") 482 assert.IsError(t, err, os.ErrNotExist) 483 p = mustNew(t, &cli) 484 485 _, err = p.Parse([]string{"--file", "testdata"}) 486 assert.Error(t, err) 487 assert.Contains(t, err.Error(), "is a directory") 488 } 489 490 func TestPathMapperUsingStringPointer(t *testing.T) { 491 type CLI struct { 492 Path *string `type:"path"` 493 } 494 var cli CLI 495 496 t.Run("With value", func(t *testing.T) { 497 pwd, err := os.Getwd() 498 assert.NoError(t, err) 499 p := mustNew(t, &cli) 500 _, err = p.Parse([]string{"--path", "."}) 501 assert.NoError(t, err) 502 assert.NotZero(t, cli.Path) 503 assert.Equal(t, pwd, *cli.Path) 504 }) 505 506 t.Run("Zero value", func(t *testing.T) { 507 p := mustNew(t, &cli) 508 _, err := p.Parse([]string{"--path", ""}) 509 assert.NoError(t, err) 510 assert.NotZero(t, cli.Path) 511 wd, err := os.Getwd() 512 assert.NoError(t, err) 513 assert.Equal(t, wd, *cli.Path) 514 }) 515 516 t.Run("Without value", func(t *testing.T) { 517 p := mustNew(t, &cli) 518 _, err := p.Parse([]string{"--"}) 519 assert.NoError(t, err) 520 assert.Equal(t, nil, cli.Path) 521 }) 522 523 t.Run("Non-string pointer", func(t *testing.T) { 524 type CLI struct { 525 Path *any `type:"path"` 526 } 527 var cli CLI 528 p := mustNew(t, &cli) 529 _, err := p.Parse([]string{"--path", ""}) 530 assert.Error(t, err) 531 assert.Contains(t, err.Error(), `"path" type must be applied to a string`) 532 }) 533 } 534 535 //nolint:dupl 536 func TestExistingFileMapper(t *testing.T) { 537 type CLI struct { 538 File string `type:"existingfile"` 539 } 540 var cli CLI 541 p := mustNew(t, &cli) 542 _, err := p.Parse([]string{"--file", "testdata/file.txt"}) 543 assert.NoError(t, err) 544 assert.NotZero(t, cli.File) 545 p = mustNew(t, &cli) 546 _, err = p.Parse([]string{"--file", "testdata/missing.txt"}) 547 assert.Error(t, err) 548 assert.Contains(t, err.Error(), "missing.txt:") 549 assert.IsError(t, err, os.ErrNotExist) 550 p = mustNew(t, &cli) 551 _, err = p.Parse([]string{"--file", "testdata/"}) 552 assert.Error(t, err) 553 assert.Contains(t, err.Error(), "exists but is a directory") 554 } 555 556 func TestExistingFileMapperSlice(t *testing.T) { 557 type CLI struct { 558 Files []string `type:"existingfile"` 559 } 560 var cli CLI 561 p := mustNew(t, &cli) 562 _, err := p.Parse([]string{"--files", "testdata/file.txt", "--files", "testdata/file.txt"}) 563 assert.NoError(t, err) 564 assert.NotZero(t, cli.Files) 565 pwd, err := os.Getwd() 566 assert.NoError(t, err) 567 assert.Equal(t, []string{filepath.Join(pwd, "testdata", "file.txt"), filepath.Join(pwd, "testdata", "file.txt")}, cli.Files) 568 } 569 570 func TestExistingFileMapperDefaultMissing(t *testing.T) { 571 type CLI struct { 572 File string `type:"existingfile" default:"testdata/missing.txt"` 573 } 574 var cli CLI 575 p := mustNew(t, &cli) 576 file := filepath.Join("testdata", "file.txt") 577 _, err := p.Parse([]string{"--file", file}) 578 assert.NoError(t, err) 579 assert.NotZero(t, cli.File) 580 assert.Contains(t, cli.File, file) 581 p = mustNew(t, &cli) 582 _, err = p.Parse([]string{}) 583 assert.Error(t, err) 584 assert.Contains(t, err.Error(), "missing.txt:") 585 assert.IsError(t, err, os.ErrNotExist) 586 } 587 588 func TestExistingFileMapperDefaultMissingCmds(t *testing.T) { 589 type CLI struct { 590 CmdA struct { 591 FileA string `type:"existingfile" default:"testdata/aaa-missing.txt"` 592 FileB string `type:"existingfile" default:"testdata/bbb-missing.txt"` 593 } `cmd:""` 594 CmdC struct { 595 FileC string `type:"existingfile" default:"testdata/ccc-missing.txt"` 596 } `cmd:""` 597 } 598 var cli CLI 599 file := filepath.Join("testdata", "file.txt") 600 p := mustNew(t, &cli) 601 _, err := p.Parse([]string{"cmd-a", "--file-a", file, "--file-b", file}) 602 assert.NoError(t, err) 603 assert.NotZero(t, cli.CmdA.FileA) 604 assert.Contains(t, cli.CmdA.FileA, file) 605 assert.NotZero(t, cli.CmdA.FileB) 606 assert.Contains(t, cli.CmdA.FileB, file) 607 p = mustNew(t, &cli) 608 _, err = p.Parse([]string{"cmd-a", "--file-a", file}) 609 assert.Error(t, err) 610 assert.Contains(t, err.Error(), "bbb-missing.txt:") 611 assert.IsError(t, err, os.ErrNotExist) 612 } 613 614 //nolint:dupl 615 func TestExistingDirMapper(t *testing.T) { 616 type CLI struct { 617 Dir string `type:"existingdir"` 618 } 619 var cli CLI 620 p := mustNew(t, &cli) 621 _, err := p.Parse([]string{"--dir", "testdata/"}) 622 assert.NoError(t, err) 623 assert.NotZero(t, cli.Dir) 624 p = mustNew(t, &cli) 625 _, err = p.Parse([]string{"--dir", "missingdata/"}) 626 assert.Error(t, err) 627 assert.Contains(t, err.Error(), "missingdata:") 628 assert.IsError(t, err, os.ErrNotExist) 629 p = mustNew(t, &cli) 630 _, err = p.Parse([]string{"--dir", "testdata/file.txt"}) 631 assert.Error(t, err) 632 assert.Contains(t, err.Error(), "exists but is not a directory") 633 } 634 635 func TestExistingDirMapperDefaultMissing(t *testing.T) { 636 type CLI struct { 637 Dir string `type:"existingdir" default:"missing-dir"` 638 } 639 var cli CLI 640 p := mustNew(t, &cli) 641 dir := "testdata" 642 _, err := p.Parse([]string{"--dir", dir}) 643 assert.NoError(t, err) 644 assert.NotZero(t, cli.Dir) 645 assert.Contains(t, cli.Dir, dir) 646 p = mustNew(t, &cli) 647 _, err = p.Parse([]string{}) 648 assert.Error(t, err) 649 assert.Contains(t, err.Error(), "missing-dir:") 650 assert.IsError(t, err, os.ErrNotExist) 651 } 652 653 func TestExistingDirMapperDefaultMissingCmds(t *testing.T) { 654 type CLI struct { 655 CmdA struct { 656 DirA string `type:"existingdir" default:"aaa-missing-dir"` 657 DirB string `type:"existingdir" default:"bbb-missing-dir"` 658 } `cmd:""` 659 CmdC struct { 660 DirC string `type:"existingdir" default:"ccc-missing-dir"` 661 } `cmd:""` 662 } 663 var cli CLI 664 dir := "testdata" 665 p := mustNew(t, &cli) 666 _, err := p.Parse([]string{"cmd-a", "--dir-a", dir, "--dir-b", dir}) 667 assert.NoError(t, err) 668 assert.NotZero(t, cli.CmdA.DirA) 669 assert.NotZero(t, cli.CmdA.DirB) 670 assert.Contains(t, cli.CmdA.DirA, dir) 671 assert.Contains(t, cli.CmdA.DirB, dir) 672 p = mustNew(t, &cli) 673 _, err = p.Parse([]string{"cmd-a", "--dir-a", dir}) 674 assert.Error(t, err) 675 assert.Contains(t, err.Error(), "bbb-missing-dir:") 676 assert.IsError(t, err, os.ErrNotExist) 677 } 678 679 func TestMapperPlaceHolder(t *testing.T) { 680 var cli struct { 681 Flag string 682 } 683 b := bytes.NewBuffer(nil) 684 k := mustNew( 685 t, 686 &cli, 687 kong.Writers(b, b), 688 kong.ValueMapper(&cli.Flag, testMapperWithPlaceHolder{}), 689 kong.Exit(func(int) { panic("exit") }), 690 ) 691 // Ensure that --help 692 assert.Panics(t, func() { 693 _, err := k.Parse([]string{"--help"}) 694 assert.NoError(t, err) 695 }) 696 assert.Contains(t, b.String(), "--flag=/a/b/c") 697 } 698 699 type testMapperWithPlaceHolder struct{} 700 701 func (t testMapperWithPlaceHolder) Decode(ctx *kong.DecodeContext, target reflect.Value) error { 702 target.SetString("hi") 703 return nil 704 } 705 706 func (t testMapperWithPlaceHolder) PlaceHolder(flag *kong.Flag) string { 707 return "/a/b/c" 708 } 709 710 func TestMapperVarsContributor(t *testing.T) { 711 var cli struct { 712 Flag string `help:"Some help with ${avar}"` 713 } 714 b := bytes.NewBuffer(nil) 715 k := mustNew( 716 t, 717 &cli, 718 kong.Writers(b, b), 719 kong.ValueMapper(&cli.Flag, testMapperVarsContributor{}), 720 kong.Exit(func(int) { panic("exit") }), 721 ) 722 // Ensure that --help 723 assert.Panics(t, func() { 724 _, err := k.Parse([]string{"--help"}) 725 assert.NoError(t, err) 726 }) 727 assert.Contains(t, b.String(), "--flag=STRING") 728 assert.Contains(t, b.String(), "Some help with a var", b.String()) 729 } 730 731 type testMapperVarsContributor struct{} 732 733 func (t testMapperVarsContributor) Vars(value *kong.Value) kong.Vars { 734 return kong.Vars{"avar": "a var"} 735 } 736 737 func (t testMapperVarsContributor) Decode(ctx *kong.DecodeContext, target reflect.Value) error { 738 target.SetString("hi") 739 return nil 740 } 741 742 func TestValuesThatLookLikeFlags(t *testing.T) { 743 var cli struct { 744 Slice []string 745 Map map[string]string 746 } 747 k := mustNew(t, &cli) 748 _, err := k.Parse([]string{"--slice", "-foo"}) 749 assert.Error(t, err) 750 _, err = k.Parse([]string{"--map", "-foo=-bar"}) 751 assert.Error(t, err) 752 _, err = k.Parse([]string{"--slice=-foo", "--slice=-bar"}) 753 assert.NoError(t, err) 754 assert.Equal(t, []string{"-foo", "-bar"}, cli.Slice) 755 _, err = k.Parse([]string{"--map=-foo=-bar"}) 756 assert.NoError(t, err) 757 assert.Equal(t, map[string]string{"-foo": "-bar"}, cli.Map) 758 }