Enum fields must be required or have a default.
This is a breaking change, but the previous behaviour was broken so I'm not concerned. Also made most programmer errors more useful by giving type.field context information. Fixes #179.
This commit is contained in:
@@ -444,7 +444,7 @@ Tag | Description
|
|||||||
`format:"X"` | Format for parsing input, if supported.
|
`format:"X"` | Format for parsing input, if supported.
|
||||||
`sep:"X"` | Separator for sequences (defaults to ","). May be `none` to disable splitting.
|
`sep:"X"` | Separator for sequences (defaults to ","). May be `none` to disable splitting.
|
||||||
`mapsep:"X"` | Separator for maps (defaults to ";"). May be `none` to disable splitting.
|
`mapsep:"X"` | Separator for maps (defaults to ";"). May be `none` to disable splitting.
|
||||||
`enum:"X,Y,..."` | Set of valid values allowed for this flag.
|
`enum:"X,Y,..."` | Set of valid values allowed for this flag. An enum field must be `required` or have a valid `default`.
|
||||||
`group:"X"` | Logical group for a flag or command.
|
`group:"X"` | Logical group for a flag or command.
|
||||||
`xor:"X,Y,..."` | Exclusive OR groups for flags. Only one flag in the group can be used which is restricted within the same command. When combined with `required`, at least one of the `xor` group will be required.
|
`xor:"X,Y,..."` | Exclusive OR groups for flags. Only one flag in the group can be used which is restricted within the same command. When combined with `required`, at least one of the `xor` group will be required.
|
||||||
`prefix:"X"` | Prefix for all sub-flags.
|
`prefix:"X"` | Prefix for all sub-flags.
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func flattenedFields(v reflect.Value) (out []flattenedField) {
|
|||||||
for i := 0; i < v.NumField(); i++ {
|
for i := 0; i < v.NumField(); i++ {
|
||||||
ft := v.Type().Field(i)
|
ft := v.Type().Field(i)
|
||||||
fv := v.Field(i)
|
fv := v.Field(i)
|
||||||
tag := parseTag(fv, ft)
|
tag := parseTag(v, fv, ft)
|
||||||
if tag.Ignored {
|
if tag.Ignored {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -156,8 +156,7 @@ func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.S
|
|||||||
// a positional argument is provided to the child, and move it to the branching argument field.
|
// a positional argument is provided to the child, and move it to the branching argument field.
|
||||||
if tag.Arg {
|
if tag.Arg {
|
||||||
if len(child.Positional) == 0 {
|
if len(child.Positional) == 0 {
|
||||||
fail("positional branch %s.%s must have at least one child positional argument named %q",
|
failField(v, ft, "positional branch must have at least one child positional argument named %q", name)
|
||||||
v.Type().Name(), ft.Name, name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
value := child.Positional[0]
|
value := child.Positional[0]
|
||||||
@@ -168,8 +167,7 @@ func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.S
|
|||||||
|
|
||||||
child.Name = value.Name
|
child.Name = value.Name
|
||||||
if child.Name != name {
|
if child.Name != name {
|
||||||
fail("first field in positional branch %s.%s must have the same name as the parent field (%s).",
|
failField(v, ft, "first field in positional branch must have the same name as the parent field (%s).", child.Name)
|
||||||
v.Type().Name(), ft.Name, child.Name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
child.Argument = value
|
child.Argument = value
|
||||||
@@ -179,14 +177,14 @@ func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.S
|
|||||||
node.Children = append(node.Children, child)
|
node.Children = append(node.Children, child)
|
||||||
|
|
||||||
if len(child.Positional) > 0 && len(child.Children) > 0 {
|
if len(child.Positional) > 0 && len(child.Children) > 0 {
|
||||||
fail("can't mix positional arguments and branching arguments on %s.%s", v.Type().Name(), ft.Name)
|
failField(v, ft, "can't mix positional arguments and branching arguments")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) {
|
func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) {
|
||||||
mapper := k.registry.ForNamedValue(tag.Type, fv)
|
mapper := k.registry.ForNamedValue(tag.Type, fv)
|
||||||
if mapper == nil {
|
if mapper == nil {
|
||||||
fail("unsupported field type %s.%s (of type %s), perhaps missing a cmd:\"\" tag?", v.Type(), ft.Name, ft.Type)
|
failField(v, ft, "unsupported field type %s, perhaps missing a cmd:\"\" tag?", ft.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
value := &Value{
|
value := &Value{
|
||||||
@@ -209,13 +207,13 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
|
|||||||
node.Positional = append(node.Positional, value)
|
node.Positional = append(node.Positional, value)
|
||||||
} else {
|
} else {
|
||||||
if seenFlags["--"+value.Name] {
|
if seenFlags["--"+value.Name] {
|
||||||
fail("duplicate flag --%s", value.Name)
|
failField(v, ft, "duplicate flag --%s", value.Name)
|
||||||
} else {
|
} else {
|
||||||
seenFlags["--"+value.Name] = true
|
seenFlags["--"+value.Name] = true
|
||||||
}
|
}
|
||||||
if tag.Short != 0 {
|
if tag.Short != 0 {
|
||||||
if seenFlags["-"+string(tag.Short)] {
|
if seenFlags["-"+string(tag.Short)] {
|
||||||
fail("duplicate short flag -%c", tag.Short)
|
failField(v, ft, "duplicate short flag -%c", tag.Short)
|
||||||
} else {
|
} else {
|
||||||
seenFlags["-"+string(tag.Short)] = true
|
seenFlags["-"+string(tag.Short)] = true
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ func TestMultipleConfigLoading(t *testing.T) {
|
|||||||
|
|
||||||
func TestConfigValidation(t *testing.T) {
|
func TestConfigValidation(t *testing.T) {
|
||||||
var cli struct {
|
var cli struct {
|
||||||
Flag string `json:"flag,omitempty" enum:"valid"`
|
Flag string `json:"flag,omitempty" enum:"valid" required:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.Flag = "invalid"
|
cli.Flag = "invalid"
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ func fail(format string, args ...interface{}) {
|
|||||||
panic(Error{msg: fmt.Sprintf(format, args...)})
|
panic(Error{msg: fmt.Sprintf(format, args...)})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func failField(parent reflect.Value, field reflect.StructField, format string, args ...interface{}) {
|
||||||
|
name := parent.Type().Name()
|
||||||
|
if name == "" {
|
||||||
|
name = "<anonymous struct>"
|
||||||
|
}
|
||||||
|
msg := fmt.Sprintf("%s.%s: %s", name, field.Name, fmt.Sprintf(format, args...))
|
||||||
|
panic(Error{msg: msg})
|
||||||
|
}
|
||||||
|
|
||||||
// Must creates a new Parser or panics if there is an error.
|
// Must creates a new Parser or panics if there is an error.
|
||||||
func Must(ast interface{}, options ...Option) *Kong {
|
func Must(ast interface{}, options ...Option) *Kong {
|
||||||
k, err := New(ast, options...)
|
k, err := New(ast, options...)
|
||||||
|
|||||||
+5
-5
@@ -643,7 +643,7 @@ func TestRun(t *testing.T) {
|
|||||||
func TestInterpolationIntoModel(t *testing.T) {
|
func TestInterpolationIntoModel(t *testing.T) {
|
||||||
var cli struct {
|
var cli struct {
|
||||||
Flag string `default:"${default}" help:"Help, I need ${somebody}" enum:"${enum}"`
|
Flag string `default:"${default}" help:"Help, I need ${somebody}" enum:"${enum}"`
|
||||||
EnumRef string `enum:"a,b" help:"One of ${enum}"`
|
EnumRef string `enum:"a,b" required:"" help:"One of ${enum}"`
|
||||||
}
|
}
|
||||||
_, err := kong.New(&cli)
|
_, err := kong.New(&cli)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -763,7 +763,7 @@ func TestHooksCalledForDefault(t *testing.T) {
|
|||||||
|
|
||||||
func TestEnum(t *testing.T) {
|
func TestEnum(t *testing.T) {
|
||||||
var cli struct {
|
var cli struct {
|
||||||
Flag string `enum:"a,b,c"`
|
Flag string `enum:"a,b,c" required:""`
|
||||||
}
|
}
|
||||||
_, err := mustNew(t, &cli).Parse([]string{"--flag", "d"})
|
_, err := mustNew(t, &cli).Parse([]string{"--flag", "d"})
|
||||||
require.EqualError(t, err, "--flag must be one of \"a\",\"b\",\"c\" but got \"d\"")
|
require.EqualError(t, err, "--flag must be one of \"a\",\"b\",\"c\" but got \"d\"")
|
||||||
@@ -978,7 +978,7 @@ func TestIssue153(t *testing.T) {
|
|||||||
func TestEnumArg(t *testing.T) {
|
func TestEnumArg(t *testing.T) {
|
||||||
var cli struct {
|
var cli struct {
|
||||||
Nested struct {
|
Nested struct {
|
||||||
One string `arg:"" enum:"a,b,c"`
|
One string `arg:"" enum:"a,b,c" required:""`
|
||||||
Two string `arg:""`
|
Two string `arg:""`
|
||||||
} `cmd:""`
|
} `cmd:""`
|
||||||
}
|
}
|
||||||
@@ -1142,7 +1142,7 @@ func TestDuplicateShortflags(t *testing.T) {
|
|||||||
Flag2 bool `short:"t"`
|
Flag2 bool `short:"t"`
|
||||||
}{}
|
}{}
|
||||||
_, err := kong.New(&cli)
|
_, err := kong.New(&cli)
|
||||||
require.EqualError(t, err, "duplicate short flag -t")
|
require.EqualError(t, err, "<anonymous struct>.Flag2: duplicate short flag -t")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDuplicateNestedShortFlags(t *testing.T) {
|
func TestDuplicateNestedShortFlags(t *testing.T) {
|
||||||
@@ -1153,5 +1153,5 @@ func TestDuplicateNestedShortFlags(t *testing.T) {
|
|||||||
} `cmd:""`
|
} `cmd:""`
|
||||||
}{}
|
}{}
|
||||||
_, err := kong.New(&cli)
|
_, err := kong.New(&cli)
|
||||||
require.EqualError(t, err, "duplicate short flag -t")
|
require.EqualError(t, err, "<anonymous struct>.Flag2: duplicate short flag -t")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func tagSplitFn(r rune) bool {
|
|||||||
return r == ',' || r == ' '
|
return r == ',' || r == ' '
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
|
func parseTag(parent, fv reflect.Value, ft reflect.StructField) *Tag {
|
||||||
if ft.Tag.Get("kong") == "-" {
|
if ft.Tag.Get("kong") == "-" {
|
||||||
t := newEmptyTag()
|
t := newEmptyTag()
|
||||||
t.Ignored = true
|
t.Ignored = true
|
||||||
@@ -146,7 +146,7 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
|
|||||||
required := t.Has("required")
|
required := t.Has("required")
|
||||||
optional := t.Has("optional")
|
optional := t.Has("optional")
|
||||||
if required && optional {
|
if required && optional {
|
||||||
fail("can't specify both required and optional")
|
failField(parent, ft, "can't specify both required and optional")
|
||||||
}
|
}
|
||||||
t.Required = required
|
t.Required = required
|
||||||
t.Optional = optional
|
t.Optional = optional
|
||||||
@@ -161,7 +161,7 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
|
|||||||
t.Env = t.Get("env")
|
t.Env = t.Get("env")
|
||||||
t.Short, err = t.GetRune("short")
|
t.Short, err = t.GetRune("short")
|
||||||
if err != nil && t.Get("short") != "" {
|
if err != nil && t.Get("short") != "" {
|
||||||
fail("invalid short flag name %q: %s", t.Get("short"), err)
|
failField(parent, ft, "invalid short flag name %q: %s", t.Get("short"), err)
|
||||||
}
|
}
|
||||||
t.Hidden = t.Has("hidden")
|
t.Hidden = t.Has("hidden")
|
||||||
t.Format = t.Get("format")
|
t.Format = t.Get("format")
|
||||||
@@ -175,7 +175,7 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
|
|||||||
t.Embed = t.Has("embed")
|
t.Embed = t.Has("embed")
|
||||||
negatable := t.Has("negatable")
|
negatable := t.Has("negatable")
|
||||||
if negatable && ft.Type.Kind() != reflect.Bool {
|
if negatable && ft.Type.Kind() != reflect.Bool {
|
||||||
fail("negatable can only be set on booleans")
|
failField(parent, ft, "negatable can only be set on booleans")
|
||||||
}
|
}
|
||||||
t.Negatable = negatable
|
t.Negatable = negatable
|
||||||
aliases := t.Get("aliases")
|
aliases := t.Get("aliases")
|
||||||
@@ -186,7 +186,7 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
|
|||||||
for _, set := range t.GetAll("set") {
|
for _, set := range t.GetAll("set") {
|
||||||
parts := strings.SplitN(set, "=", 2)
|
parts := strings.SplitN(set, "=", 2)
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 {
|
||||||
fail("set should be in the form key=value but got %q", set)
|
failField(parent, ft, "set should be in the form key=value but got %q", set)
|
||||||
}
|
}
|
||||||
t.Vars[parts[0]] = parts[1]
|
t.Vars[parts[0]] = parts[1]
|
||||||
}
|
}
|
||||||
@@ -195,9 +195,12 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
|
|||||||
t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name()))
|
t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name()))
|
||||||
}
|
}
|
||||||
t.Enum = t.Get("enum")
|
t.Enum = t.Get("enum")
|
||||||
|
if t.Enum != "" && !(t.Required || t.Default != "") {
|
||||||
|
failField(parent, ft, "enum value is only valid if it is either required or has a valid default value")
|
||||||
|
}
|
||||||
passthrough := t.Has("passthrough")
|
passthrough := t.Has("passthrough")
|
||||||
if passthrough && !t.Arg {
|
if passthrough && !t.Arg {
|
||||||
fail("passthrough only makes sense for positional arguments")
|
failField(parent, ft, "passthrough only makes sense for positional arguments")
|
||||||
}
|
}
|
||||||
t.Passthrough = passthrough
|
t.Passthrough = passthrough
|
||||||
return t
|
return t
|
||||||
|
|||||||
+1
-1
@@ -198,5 +198,5 @@ func TestInvalidRuneErrors(t *testing.T) {
|
|||||||
Flag bool `short:"invalid"`
|
Flag bool `short:"invalid"`
|
||||||
}{}
|
}{}
|
||||||
_, err := kong.New(&cli)
|
_, err := kong.New(&cli)
|
||||||
require.EqualError(t, err, "invalid short flag name \"invalid\": invalid rune")
|
require.EqualError(t, err, "<anonymous struct>.Flag: invalid short flag name \"invalid\": invalid rune")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user