Skip to content

Commit

Permalink
Add support for pointer fields.
Browse files Browse the repository at this point in the history
  • Loading branch information
scottwis committed Mar 19, 2022
1 parent 556f8b7 commit 0983e49
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 1 deletion.
14 changes: 14 additions & 0 deletions README.md
Expand Up @@ -384,6 +384,20 @@ var CLI struct {

For flags, multiple key+value pairs should be separated by `mapsep:"rune"` tag (defaults to `;`) eg. `--set="key1=value1;key2=value2"`.

## Pointers

Pointers work like the underlying type, except that you can differentiate between the presence of the zero value and no value being supplied.

For example:

```go
var CLI struct {
Foo *int
}
```

Would produce a nil value for `Foo` if no `--foo` argument is supplied, but would have a pointer to the value 0 if the argument `--foo=0` was supplied.

## Nested data structure

Kong support a nested data structure as well with `embed:""`. You can combine `embed:""` with `prefix:""`:
Expand Down
103 changes: 103 additions & 0 deletions kong_test.go
Expand Up @@ -1542,3 +1542,106 @@ func TestPassthroughCmdOnlyStringArgs(t *testing.T) {
_, err := kong.New(&cli)
require.EqualError(t, err, "<anonymous struct>.Command: passthrough command command [<args> ...] must contain exactly one positional argument of []string type")
}

func TestStringPointer(t *testing.T) {
var cli struct {
Foo *string
}
k, err := kong.New(&cli)
require.NoError(t, err)
require.NotNil(t, k)
ctx, err := k.Parse([]string{"--foo", "wtf"})
require.NoError(t, err)
require.NotNil(t, ctx)
require.NotNil(t, cli.Foo)
require.Equal(t, "wtf", *cli.Foo)
}

func TestStringPointerNoValue(t *testing.T) {
var cli struct {
Foo *string
}
k, err := kong.New(&cli)
require.NoError(t, err)
require.NotNil(t, k)
ctx, err := k.Parse([]string{})
require.NoError(t, err)
require.NotNil(t, ctx)
require.Nil(t, cli.Foo)
}

func TestStringPointerDefault(t *testing.T) {
var cli struct {
Foo *string `default:"stuff"`
}
k, err := kong.New(&cli)
require.NoError(t, err)
require.NotNil(t, k)
ctx, err := k.Parse([]string{})
require.NoError(t, err)
require.NotNil(t, ctx)
require.NotNil(t, cli.Foo)
require.Equal(t, "stuff", *cli.Foo)
}

func TestStringPointerAliasNoValue(t *testing.T) {
type Foo string
var cli struct {
F *Foo
}
k, err := kong.New(&cli)
require.NoError(t, err)
require.NotNil(t, k)
ctx, err := k.Parse([]string{})
require.NoError(t, err)
require.NotNil(t, ctx)
require.Nil(t, cli.F)
}

func TestStringPointerAlias(t *testing.T) {
type Foo string
var cli struct {
F *Foo
}
k, err := kong.New(&cli)
require.NoError(t, err)
require.NotNil(t, k)
ctx, err := k.Parse([]string{"--f=value"})
require.NoError(t, err)
require.NotNil(t, ctx)
require.NotNil(t, cli.F)
require.Equal(t, Foo("value"), *cli.F)
}

func TestStringPointerEmptyValue(t *testing.T) {
var cli struct {
F *string
G *string
}
k, err := kong.New(&cli)
require.NoError(t, err)
require.NotNil(t, k)
ctx, err := k.Parse([]string{"--f", "", "--g="})
require.NoError(t, err)
require.NotNil(t, ctx)
require.NotNil(t, cli.F)
require.NotNil(t, cli.G)
require.Equal(t, "", *cli.F)
require.Equal(t, "", *cli.G)
}

func TestIntPtr(t *testing.T) {
var cli struct {
F *int
G *int
}
k, err := kong.New(&cli)
require.NoError(t, err)
require.NotNil(t, k)
ctx, err := k.Parse([]string{"--f=6"})
require.NoError(t, err)
require.NotNil(t, ctx)
require.NotNil(t, cli.F)
require.Nil(t, cli.G)
require.Equal(t, 6, *cli.F)
}
19 changes: 18 additions & 1 deletion mapper.go
Expand Up @@ -275,7 +275,8 @@ func (r *Registry) RegisterDefaults() *Registry {
RegisterName("path", pathMapper(r)).
RegisterName("existingfile", existingFileMapper(r)).
RegisterName("existingdir", existingDirMapper(r)).
RegisterName("counter", counterMapper())
RegisterName("counter", counterMapper()).
RegisterKind(reflect.Ptr, ptrMapper(r))
}

type boolMapper struct{}
Expand Down Expand Up @@ -654,6 +655,22 @@ func existingDirMapper(r *Registry) MapperFunc {
}
}

func ptrMapper(r *Registry) MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error {
elem := reflect.New(target.Type().Elem()).Elem()
nestedMapper := r.ForValue(elem)
if nestedMapper == nil {
return errors.New("cannot find member for pointer element type")
}
err := nestedMapper.Decode(ctx, elem)
if err != nil {
return err
}
target.Set(elem.Addr())
return nil
}
}

func counterMapper() MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error {
if ctx.Scan.Peek().Type == FlagValueToken {
Expand Down

0 comments on commit 0983e49

Please sign in to comment.