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.
Alec Thomas
2 years ago
443 | 443 | `format:"X"` | Format for parsing input, if supported. |
444 | 444 | `sep:"X"` | Separator for sequences (defaults to ","). May be `none` to disable splitting. |
445 | 445 | `mapsep:"X"` | Separator for maps (defaults to ";"). May be `none` to disable splitting. |
446 | `enum:"X,Y,..."` | Set of valid values allowed for this flag. | |
446 | `enum:"X,Y,..."` | Set of valid values allowed for this flag. An enum field must be `required` or have a valid `default`. | |
447 | 447 | `group:"X"` | Logical group for a flag or command. |
448 | 448 | `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. |
449 | 449 | `prefix:"X"` | Prefix for all sub-flags. |
50 | 50 | for i := 0; i < v.NumField(); i++ { |
51 | 51 | ft := v.Type().Field(i) |
52 | 52 | fv := v.Field(i) |
53 | tag := parseTag(fv, ft) | |
53 | tag := parseTag(v, fv, ft) | |
54 | 54 | if tag.Ignored { |
55 | 55 | continue |
56 | 56 | } |
155 | 155 | // a positional argument is provided to the child, and move it to the branching argument field. |
156 | 156 | if tag.Arg { |
157 | 157 | if len(child.Positional) == 0 { |
158 | fail("positional branch %s.%s must have at least one child positional argument named %q", | |
159 | v.Type().Name(), ft.Name, name) | |
158 | failField(v, ft, "positional branch must have at least one child positional argument named %q", name) | |
160 | 159 | } |
161 | 160 | |
162 | 161 | value := child.Positional[0] |
167 | 166 | |
168 | 167 | child.Name = value.Name |
169 | 168 | if child.Name != name { |
170 | fail("first field in positional branch %s.%s must have the same name as the parent field (%s).", | |
171 | v.Type().Name(), ft.Name, child.Name) | |
169 | failField(v, ft, "first field in positional branch must have the same name as the parent field (%s).", child.Name) | |
172 | 170 | } |
173 | 171 | |
174 | 172 | child.Argument = value |
178 | 176 | node.Children = append(node.Children, child) |
179 | 177 | |
180 | 178 | if len(child.Positional) > 0 && len(child.Children) > 0 { |
181 | fail("can't mix positional arguments and branching arguments on %s.%s", v.Type().Name(), ft.Name) | |
179 | failField(v, ft, "can't mix positional arguments and branching arguments") | |
182 | 180 | } |
183 | 181 | } |
184 | 182 | |
185 | 183 | func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) { |
186 | 184 | mapper := k.registry.ForNamedValue(tag.Type, fv) |
187 | 185 | if mapper == nil { |
188 | fail("unsupported field type %s.%s (of type %s), perhaps missing a cmd:\"\" tag?", v.Type(), ft.Name, ft.Type) | |
186 | failField(v, ft, "unsupported field type %s, perhaps missing a cmd:\"\" tag?", ft.Type) | |
189 | 187 | } |
190 | 188 | |
191 | 189 | value := &Value{ |
208 | 206 | node.Positional = append(node.Positional, value) |
209 | 207 | } else { |
210 | 208 | if seenFlags["--"+value.Name] { |
211 | fail("duplicate flag --%s", value.Name) | |
209 | failField(v, ft, "duplicate flag --%s", value.Name) | |
212 | 210 | } else { |
213 | 211 | seenFlags["--"+value.Name] = true |
214 | 212 | } |
215 | 213 | if tag.Short != 0 { |
216 | 214 | if seenFlags["-"+string(tag.Short)] { |
217 | fail("duplicate short flag -%c", tag.Short) | |
215 | failField(v, ft, "duplicate short flag -%c", tag.Short) | |
218 | 216 | } else { |
219 | 217 | seenFlags["-"+string(tag.Short)] = true |
220 | 218 | } |
31 | 31 | |
32 | 32 | func TestConfigValidation(t *testing.T) { |
33 | 33 | var cli struct { |
34 | Flag string `json:"flag,omitempty" enum:"valid"` | |
34 | Flag string `json:"flag,omitempty" enum:"valid" required:""` | |
35 | 35 | } |
36 | 36 | |
37 | 37 | cli.Flag = "invalid" |
19 | 19 | |
20 | 20 | func fail(format string, args ...interface{}) { |
21 | 21 | panic(Error{msg: fmt.Sprintf(format, args...)}) |
22 | } | |
23 | ||
24 | func failField(parent reflect.Value, field reflect.StructField, format string, args ...interface{}) { | |
25 | name := parent.Type().Name() | |
26 | if name == "" { | |
27 | name = "<anonymous struct>" | |
28 | } | |
29 | msg := fmt.Sprintf("%s.%s: %s", name, field.Name, fmt.Sprintf(format, args...)) | |
30 | panic(Error{msg: msg}) | |
22 | 31 | } |
23 | 32 | |
24 | 33 | // Must creates a new Parser or panics if there is an error. |
642 | 642 | func TestInterpolationIntoModel(t *testing.T) { |
643 | 643 | var cli struct { |
644 | 644 | Flag string `default:"${default}" help:"Help, I need ${somebody}" enum:"${enum}"` |
645 | EnumRef string `enum:"a,b" help:"One of ${enum}"` | |
645 | EnumRef string `enum:"a,b" required:"" help:"One of ${enum}"` | |
646 | 646 | } |
647 | 647 | _, err := kong.New(&cli) |
648 | 648 | require.Error(t, err) |
762 | 762 | |
763 | 763 | func TestEnum(t *testing.T) { |
764 | 764 | var cli struct { |
765 | Flag string `enum:"a,b,c"` | |
765 | Flag string `enum:"a,b,c" required:""` | |
766 | 766 | } |
767 | 767 | _, err := mustNew(t, &cli).Parse([]string{"--flag", "d"}) |
768 | 768 | require.EqualError(t, err, "--flag must be one of \"a\",\"b\",\"c\" but got \"d\"") |
977 | 977 | func TestEnumArg(t *testing.T) { |
978 | 978 | var cli struct { |
979 | 979 | Nested struct { |
980 | One string `arg:"" enum:"a,b,c"` | |
980 | One string `arg:"" enum:"a,b,c" required:""` | |
981 | 981 | Two string `arg:""` |
982 | 982 | } `cmd:""` |
983 | 983 | } |
1141 | 1141 | Flag2 bool `short:"t"` |
1142 | 1142 | }{} |
1143 | 1143 | _, err := kong.New(&cli) |
1144 | require.EqualError(t, err, "duplicate short flag -t") | |
1144 | require.EqualError(t, err, "<anonymous struct>.Flag2: duplicate short flag -t") | |
1145 | 1145 | } |
1146 | 1146 | |
1147 | 1147 | func TestDuplicateNestedShortFlags(t *testing.T) { |
1152 | 1152 | } `cmd:""` |
1153 | 1153 | }{} |
1154 | 1154 | _, err := kong.New(&cli) |
1155 | require.EqualError(t, err, "duplicate short flag -t") | |
1156 | } | |
1155 | require.EqualError(t, err, "<anonymous struct>.Flag2: duplicate short flag -t") | |
1156 | } |
128 | 128 | return r == ',' || r == ' ' |
129 | 129 | } |
130 | 130 | |
131 | func parseTag(fv reflect.Value, ft reflect.StructField) *Tag { | |
131 | func parseTag(parent, fv reflect.Value, ft reflect.StructField) *Tag { | |
132 | 132 | if ft.Tag.Get("kong") == "-" { |
133 | 133 | t := newEmptyTag() |
134 | 134 | t.Ignored = true |
145 | 145 | required := t.Has("required") |
146 | 146 | optional := t.Has("optional") |
147 | 147 | if required && optional { |
148 | fail("can't specify both required and optional") | |
148 | failField(parent, ft, "can't specify both required and optional") | |
149 | 149 | } |
150 | 150 | t.Required = required |
151 | 151 | t.Optional = optional |
160 | 160 | t.Env = t.Get("env") |
161 | 161 | t.Short, err = t.GetRune("short") |
162 | 162 | if err != nil && t.Get("short") != "" { |
163 | fail("invalid short flag name %q: %s", t.Get("short"), err) | |
163 | failField(parent, ft, "invalid short flag name %q: %s", t.Get("short"), err) | |
164 | 164 | } |
165 | 165 | t.Hidden = t.Has("hidden") |
166 | 166 | t.Format = t.Get("format") |
174 | 174 | t.Embed = t.Has("embed") |
175 | 175 | negatable := t.Has("negatable") |
176 | 176 | if negatable && ft.Type.Kind() != reflect.Bool { |
177 | fail("negatable can only be set on booleans") | |
177 | failField(parent, ft, "negatable can only be set on booleans") | |
178 | 178 | } |
179 | 179 | t.Negatable = negatable |
180 | 180 | aliases := t.Get("aliases") |
185 | 185 | for _, set := range t.GetAll("set") { |
186 | 186 | parts := strings.SplitN(set, "=", 2) |
187 | 187 | if len(parts) == 0 { |
188 | fail("set should be in the form key=value but got %q", set) | |
188 | failField(parent, ft, "set should be in the form key=value but got %q", set) | |
189 | 189 | } |
190 | 190 | t.Vars[parts[0]] = parts[1] |
191 | 191 | } |
194 | 194 | t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name())) |
195 | 195 | } |
196 | 196 | t.Enum = t.Get("enum") |
197 | if t.Enum != "" && !(t.Required || t.Default != "") { | |
198 | failField(parent, ft, "enum value is only valid if it is either required or has a valid default value") | |
199 | } | |
197 | 200 | passthrough := t.Has("passthrough") |
198 | 201 | if passthrough && !t.Arg { |
199 | fail("passthrough only makes sense for positional arguments") | |
202 | failField(parent, ft, "passthrough only makes sense for positional arguments") | |
200 | 203 | } |
201 | 204 | t.Passthrough = passthrough |
202 | 205 | return t |