Merging issue 280 with master changes (#296)

This commit is contained in:
Jacob Hochstetler
2022-09-20 07:55:05 -05:00
committed by GitHub
parent 9c8b401de0
commit 0c6a9f3a3d
5 changed files with 293 additions and 5 deletions
+15 -1
View File
@@ -444,6 +444,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:""`:
@@ -689,4 +703,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).
+25 -1
View File
@@ -663,6 +663,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 {
@@ -691,7 +707,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})
@@ -891,6 +910,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)
+231
View File
@@ -1736,3 +1736,234 @@ func TestCumulativeArgumentNotLast(t *testing.T) {
_, err := kong.New(&cli)
assert.Error(t, err)
}
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)
}
+18 -1
View File
@@ -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{}
@@ -669,6 +670,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 {
+4 -2
View File
@@ -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")
@@ -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
@@ -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")
}