d4dd709445
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.
242 lines
6.2 KiB
Go
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,
|
|
}
|
|
}
|