89315e74ad
* Move default command validation to build Move the validation of default commands - checking if a node has multiple default commands or a default command has children - to the build phase rather than tracing. These errors are with the structure of the CLI ast and are detectable before parsing the command line. Add a couple of tests for some of the default command error cases. * Disallow positional args on a default command Do not allow a default command to have positional arguments. The current check is only for branching args, but the error message implies that positional args are not allowed either. So add a check for positional args too, and add a test case for it. This is breaking change to the API but is unlikely to have ill-effect as positional args on a default command cannot be used without explicitly naming the command (i.e. not using it as a default). * Allow default commands with cmds/args/flags Allow default commands to have sub-commands, args and flags when tagged with `default:"withargs"`. This makes specifying the name of the command on the CLI completely optional as long as the args to that command are not ambiguous with other commands.
254 lines
6.7 KiB
Go
254 lines
6.7 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.Name = name
|
|
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)
|
|
}
|
|
if child.Positional[0].Name != name {
|
|
failField(v, ft, "first field in positional branch must have the same name as the parent field (%s).", child.Name)
|
|
}
|
|
|
|
child.Argument = child.Positional[0]
|
|
child.Positional = child.Positional[1:]
|
|
if child.Help == "" {
|
|
child.Help = child.Argument.Help
|
|
}
|
|
} else if tag.Default != "" {
|
|
if node.DefaultCmd != nil {
|
|
failField(v, ft, "can't have more than one default command under %s", node.Summary())
|
|
}
|
|
if tag.Default != "withargs" && (len(child.Children) > 0 || len(child.Positional) > 0) {
|
|
failField(v, ft, "default command %s must not have subcommands or arguments", child.Summary())
|
|
}
|
|
node.DefaultCmd = child
|
|
}
|
|
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,
|
|
}
|
|
}
|