Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merging issue 280 with master changes #296

Merged
merged 2 commits into from Sep 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 15 additions & 1 deletion 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 Expand Up @@ -628,4 +642,4 @@ See the [section on hooks](#hooks-beforeresolve-beforeapply-afterapply-and-the-b

### Other options

The full set of options can be found [here](https://godoc.org/github.com/alecthomas/kong#Option).
The full set of options can be found [here](https://godoc.org/github.com/alecthomas/kong#Option).
26 changes: 25 additions & 1 deletion context.go
Expand Up @@ -661,6 +661,22 @@ func (c *Context) Apply() (string, error) {
return strings.Join(path, " "), nil
}

func flipBoolValue(value reflect.Value) error {
if value.Kind() == reflect.Bool {
value.SetBool(!value.Bool())
return nil
}

if value.Kind() == reflect.Ptr {
if !value.IsNil() {
return flipBoolValue(value.Elem())
}
return nil
}

return fmt.Errorf("cannot negate a value of %s", value.Type().String())
}

func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
candidates := []string{}
for _, flag := range flags {
Expand Down Expand Up @@ -689,7 +705,10 @@ func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
}
if flag.Negated {
value := c.getValue(flag.Value)
value.SetBool(!value.Bool())
err := flipBoolValue(value)
if err != nil {
return err
}
flag.Value.Apply(value)
}
c.Path = append(c.Path, &Path{Flag: flag})
Expand Down Expand Up @@ -889,6 +908,11 @@ func checkEnum(value *Value, target reflect.Value) error {
case reflect.Map, reflect.Struct:
return errors.New("enum can only be applied to a slice or value")

case reflect.Ptr:
if target.IsNil() {
return nil
}
return checkEnum(value, target.Elem())
default:
enumSlice := value.EnumSlice()
v := fmt.Sprintf("%v", target)
Expand Down
231 changes: 231 additions & 0 deletions kong_test.go
Expand Up @@ -1542,3 +1542,234 @@ 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)
}

func TestBoolPtr(t *testing.T) {
var cli struct {
X *bool
}
k, err := kong.New(&cli)
require.NoError(t, err)
require.NotNil(t, k)
ctx, err := k.Parse([]string{"--x"})
require.NoError(t, err)
require.NotNil(t, ctx)
require.NotNil(t, cli.X)
require.Equal(t, true, *cli.X)
}

func TestBoolPtrFalse(t *testing.T) {
var cli struct {
X *bool
}
k, err := kong.New(&cli)
require.NoError(t, err)
require.NotNil(t, k)
ctx, err := k.Parse([]string{"--x=false"})
require.NoError(t, err)
require.NotNil(t, ctx)
require.NotNil(t, cli.X)
require.Equal(t, false, *cli.X)
}

func TestBoolPtrNegated(t *testing.T) {
var cli struct {
X *bool `negatable:""`
}
k, err := kong.New(&cli)
require.NoError(t, err)
require.NotNil(t, k)
ctx, err := k.Parse([]string{"--no-x"})
require.NoError(t, err)
require.NotNil(t, ctx)
require.NotNil(t, cli.X)
require.Equal(t, false, *cli.X)
}

func TestNilNegatableBoolPtr(t *testing.T) {
var cli struct {
X *bool `negatable:""`
}
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.X)
}

func TestBoolPtrNil(t *testing.T) {
var cli struct {
X *bool
}
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.X)
}

func TestUnsupportedPtr(t *testing.T) {
//nolint:structcheck,unused
type Foo struct {
x int
y int
}

var cli struct {
F *Foo
}
k, err := kong.New(&cli)
require.NoError(t, err)
require.NotNil(t, k)
ctx, err := k.Parse([]string{"--f=whatever"})
require.Nil(t, ctx)
require.Error(t, err)
require.Equal(t, "--f: cannot find mapper for kong_test.Foo", err.Error())
}

func TestEnumPtr(t *testing.T) {
var cli struct {
X *string `enum:"A,B,C" default:"C"`
}
k, err := kong.New(&cli)
require.NoError(t, err)
require.NotNil(t, k)
ctx, err := k.Parse([]string{"--x=A"})
require.NoError(t, err)
require.NotNil(t, ctx)
require.NotNil(t, cli.X)
require.Equal(t, "A", *cli.X)
}

func TestEnumPtrOmitted(t *testing.T) {
var cli struct {
X *string `enum:"A,B,C" default:"C"`
}
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.X)
require.Equal(t, "C", *cli.X)
}

func TestEnumPtrOmittedNoDefault(t *testing.T) {
var cli struct {
X *string `enum:"A,B,C"`
}
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.X)
}
19 changes: 18 additions & 1 deletion mapper.go
Expand Up @@ -274,7 +274,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 @@ -653,6 +654,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 fmt.Errorf("cannot find mapper for %v", target.Type().Elem().String())
}
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
6 changes: 4 additions & 2 deletions tag.go
Expand Up @@ -169,9 +169,11 @@ func parseTag(parent reflect.Value, ft reflect.StructField) (*Tag, error) {
func hydrateTag(t *Tag, typ reflect.Type) error { // nolint: gocyclo
var typeName string
var isBool bool
var isBoolPtr bool
if typ != nil {
typeName = typ.Name()
isBool = typ.Kind() == reflect.Bool
isBoolPtr = typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Bool
}
var err error
t.Cmd = t.Has("cmd")
Expand Down Expand Up @@ -212,7 +214,7 @@ func hydrateTag(t *Tag, typ reflect.Type) error { // nolint: gocyclo
t.EnvPrefix = t.Get("envprefix")
t.Embed = t.Has("embed")
negatable := t.Has("negatable")
if negatable && !isBool {
if negatable && !isBool && !isBoolPtr {
return fmt.Errorf("negatable can only be set on booleans")
}
t.Negatable = negatable
Expand All @@ -230,7 +232,7 @@ func hydrateTag(t *Tag, typ reflect.Type) error { // nolint: gocyclo
}
t.PlaceHolder = t.Get("placeholder")
t.Enum = t.Get("enum")
scalarType := (typ == nil || !(typ.Kind() == reflect.Slice || typ.Kind() == reflect.Map))
scalarType := (typ == nil || !(typ.Kind() == reflect.Slice || typ.Kind() == reflect.Map || typ.Kind() == reflect.Ptr))
if t.Enum != "" && !(t.Required || t.HasDefault) && scalarType {
return fmt.Errorf("enum value is only valid if it is either required or has a valid default value")
}
Expand Down