Files
kong/build.go
T
Alec Thomas 247574041d 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.
2021-06-21 20:35:41 +09:30

251 lines
6.4 KiB
Go

package kong
import (
"fmt"
"reflect"
"strings"
)
// Plugins are dynamically embedded command-line structures.
//
// Each element in the Plugins list *must* be a pointer to a structure.
type Plugins []interface{}
func build(k *Kong, ast interface{}) (app *Application, err error) {
defer catch(&err)
v := reflect.ValueOf(ast)
iv := reflect.Indirect(v)
if v.Kind() != reflect.Ptr || iv.Kind() != reflect.Struct {
return nil, fmt.Errorf("expected a pointer to a struct but got %T", ast)
}
app = &Application{}
extraFlags := k.extraFlags()
seenFlags := map[string]bool{}
for _, flag := range extraFlags {
seenFlags[flag.Name] = true
}
node := buildNode(k, iv, ApplicationNode, seenFlags)
if len(node.Positional) > 0 && len(node.Children) > 0 {
return nil, fmt.Errorf("can't mix positional arguments and branching arguments on %T", ast)
}
app.Node = node
app.Node.Flags = append(extraFlags, app.Node.Flags...)
app.Tag = newEmptyTag()
app.Tag.Vars = k.vars
return app, nil
}
func dashedString(s string) string {
return strings.Join(camelCase(s), "-")
}
type flattenedField struct {
field reflect.StructField
value reflect.Value
tag *Tag
}
func flattenedFields(v reflect.Value) (out []flattenedField) {
v = reflect.Indirect(v)
for i := 0; i < v.NumField(); i++ {
ft := v.Type().Field(i)
fv := v.Field(i)
tag := parseTag(v, fv, ft)
if tag.Ignored {
continue
}
if !ft.Anonymous && !tag.Embed {
if fv.CanSet() {
out = append(out, flattenedField{field: ft, value: fv, tag: tag})
}
continue
}
// Embedded type.
if fv.Kind() == reflect.Interface {
fv = fv.Elem()
} else if fv.Type() == reflect.TypeOf(Plugins{}) {
for i := 0; i < fv.Len(); i++ {
out = append(out, flattenedFields(fv.Index(i).Elem())...)
}
continue
}
sub := flattenedFields(fv)
for _, subf := range sub {
// Assign parent if it's not already set.
if subf.tag.Group == "" {
subf.tag.Group = tag.Group
}
// Accumulate prefixes.
subf.tag.Prefix = tag.Prefix + subf.tag.Prefix
// Combine parent vars.
subf.tag.Vars = tag.Vars.CloneWith(subf.tag.Vars)
}
out = append(out, sub...)
}
return out
}
func buildNode(k *Kong, v reflect.Value, typ NodeType, seenFlags map[string]bool) *Node {
node := &Node{
Type: typ,
Target: v,
Tag: newEmptyTag(),
}
for _, field := range flattenedFields(v) {
ft := field.field
fv := field.value
tag := field.tag
name := tag.Name
if name == "" {
name = tag.Prefix + strings.ToLower(dashedString(ft.Name))
} else {
name = tag.Prefix + name
}
// Nested structs are either commands or args, unless they implement the Mapper interface.
if ft.Type.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) && k.registry.ForValue(fv) == nil {
typ := CommandNode
if tag.Arg {
typ = ArgumentNode
}
buildChild(k, node, typ, v, ft, fv, tag, name, seenFlags)
} else {
buildField(k, node, v, ft, fv, tag, name, seenFlags)
}
}
// "Unsee" flags.
for _, flag := range node.Flags {
delete(seenFlags, "--"+flag.Name)
if flag.Short != 0 {
delete(seenFlags, "-"+string(flag.Short))
}
}
// Scan through argument positionals to ensure optional is never before a required.
last := true
for i, p := range node.Positional {
if !last && p.Required {
fail("argument %q can not be required after an optional", p.Name)
}
last = p.Required
p.Position = i
}
return node
}
func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) {
child := buildNode(k, fv, typ, seenFlags)
child.Tag = tag
child.Parent = node
child.Help = tag.Help
child.Hidden = tag.Hidden
child.Group = buildGroupForKey(k, tag.Group)
child.Aliases = tag.Aliases
if provider, ok := fv.Addr().Interface().(HelpProvider); ok {
child.Detail = provider.Help()
}
// A branching argument. This is a bit hairy, as we let buildNode() do the parsing, then check that
// a positional argument is provided to the child, and move it to the branching argument field.
if tag.Arg {
if len(child.Positional) == 0 {
failField(v, ft, "positional branch must have at least one child positional argument named %q", name)
}
value := child.Positional[0]
child.Positional = child.Positional[1:]
if child.Help == "" {
child.Help = value.Help
}
child.Name = value.Name
if child.Name != name {
failField(v, ft, "first field in positional branch must have the same name as the parent field (%s).", child.Name)
}
child.Argument = value
} else {
child.Name = name
}
node.Children = append(node.Children, child)
if len(child.Positional) > 0 && len(child.Children) > 0 {
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) {
mapper := k.registry.ForNamedValue(tag.Type, fv)
if mapper == nil {
failField(v, ft, "unsupported field type %s, perhaps missing a cmd:\"\" tag?", ft.Type)
}
value := &Value{
Name: name,
Help: tag.Help,
Default: tag.Default,
DefaultValue: reflect.New(fv.Type()).Elem(),
Mapper: mapper,
Tag: tag,
Target: fv,
Enum: tag.Enum,
Passthrough: tag.Passthrough,
// Flags are optional by default, and args are required by default.
Required: (!tag.Arg && tag.Required) || (tag.Arg && !tag.Optional),
Format: tag.Format,
}
if tag.Arg {
node.Positional = append(node.Positional, value)
} else {
if seenFlags["--"+value.Name] {
failField(v, ft, "duplicate flag --%s", value.Name)
} else {
seenFlags["--"+value.Name] = true
}
if tag.Short != 0 {
if seenFlags["-"+string(tag.Short)] {
failField(v, ft, "duplicate short flag -%c", tag.Short)
} else {
seenFlags["-"+string(tag.Short)] = true
}
}
flag := &Flag{
Value: value,
Short: tag.Short,
PlaceHolder: tag.PlaceHolder,
Env: tag.Env,
Group: buildGroupForKey(k, tag.Group),
Xor: tag.Xor,
Hidden: tag.Hidden,
}
value.Flag = flag
node.Flags = append(node.Flags, flag)
}
}
func buildGroupForKey(k *Kong, key string) *Group {
if key == "" {
return nil
}
for _, group := range k.groups {
if group.Key == key {
return &group
}
}
// No group provided with kong.ExplicitGroups. We create one ad-hoc for this key.
return &Group{
Key: key,
Title: key,
}
}