github.com/maresnic/mr-kong@v1.0.0/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/maresnic/mr-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  }