Files
kong/build.go
T
Franklin "Snaipe" Mathieu d4dd709445 tag: add passthrough for positional arguments
This new tag tells the parser to stop processing flags after the
positional argument is encountered.

This is particularly useful for subcommands that forward their arguments
to an external program, and makes it possible to implement commands
like `kubectl exec` or `docker run`.

Fixes #80.
2021-04-06 07:53:16 +10:00

242 lines
6.2 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(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)
}
// 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 {
fail("positional branch %s.%s must have at least one child positional argument named %q",
v.Type().Name(), ft.Name, 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 {
fail("first field in positional branch %s.%s must have the same name as the parent field (%s).",
v.Type().Name(), ft.Name, child.Name)
}
child.Argument = value
} else {
child.Name = name
}
node.Children = append(node.Children, child)
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)
}
}
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 {
fail("unsupported field type %s.%s (of type %s), perhaps missing a cmd:\"\" tag?", v.Type(), ft.Name, 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] {
fail("duplicate flag --%s", value.Name)
}
seenFlags[value.Name] = 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,
}
}