Allow DynamicCommand to specify arbitrary tags.

Fixes #185.
This commit is contained in:
Alec Thomas
2021-07-13 13:53:01 +10:00
parent 89315e74ad
commit d1a818b5a1
6 changed files with 57 additions and 21 deletions
+6
View File
@@ -25,6 +25,7 @@
- [Custom decoders mappers](#custom-decoders-mappers) - [Custom decoders mappers](#custom-decoders-mappers)
- [Supported tags](#supported-tags) - [Supported tags](#supported-tags)
- [Plugins](#plugins) - [Plugins](#plugins)
- [Dynamic Commands](#dynamic-commands)
- [Variable interpolation](#variable-interpolation) - [Variable interpolation](#variable-interpolation)
- [Validation](#validation) - [Validation](#validation)
- [Modifying Kong's behaviour](#modifying-kongs-behaviour) - [Modifying Kong's behaviour](#modifying-kongs-behaviour)
@@ -474,6 +475,11 @@ cli.Plugins = kong.Plugins{&pluginOne, &pluginTwo}
Additionally if an interface type is embedded, it can also be populated with a Kong annotated struct. Additionally if an interface type is embedded, it can also be populated with a Kong annotated struct.
## Dynamic Commands
While plugins give complete control over extending command-line interfaces, Kong
also supports dynamically adding commands via `kong.DynamicCommand()`.
## Variable interpolation ## Variable interpolation
Kong supports limited variable interpolation into help strings, enum lists and Kong supports limited variable interpolation into help strings, enum lists and
+1 -1
View File
@@ -51,7 +51,7 @@ func flattenedFields(v reflect.Value) (out []flattenedField) {
for i := 0; i < v.NumField(); i++ { for i := 0; i < v.NumField(); i++ {
ft := v.Type().Field(i) ft := v.Type().Field(i)
fv := v.Field(i) fv := v.Field(i)
tag := parseTag(v, fv, ft) tag := parseTag(v, ft)
if tag.Ignored { if tag.Ignored {
continue continue
} }
+1 -1
View File
@@ -118,7 +118,7 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
// Synthesise command nodes. // Synthesise command nodes.
for _, dcmd := range k.dynamicCommands { for _, dcmd := range k.dynamicCommands {
tag := newEmptyTag() tag := parseTagString(strings.Join(dcmd.tags, " "))
tag.Name = dcmd.name tag.Name = dcmd.name
tag.Help = dcmd.help tag.Help = dcmd.help
tag.Group = dcmd.group tag.Group = dcmd.group
+11 -2
View File
@@ -1242,9 +1242,14 @@ func TestDynamicCommands(t *testing.T) {
cli := struct { cli := struct {
One struct{} `cmd:"one"` One struct{} `cmd:"one"`
}{} }{}
help := &strings.Builder{}
two := &dynamicCommand{} two := &dynamicCommand{}
var twoi interface{} = &two three := &dynamicCommand{}
p := mustNew(t, &cli, kong.DynamicCommand("two", "", "", twoi)) p := mustNew(t, &cli,
kong.DynamicCommand("two", "", "", &two),
kong.DynamicCommand("three", "", "", three, "hidden"),
kong.Writers(help, help),
kong.Exit(func(int) {}))
kctx, err := p.Parse([]string{"two", "--flag=flag"}) kctx, err := p.Parse([]string{"two", "--flag=flag"})
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "flag", two.Flag) require.Equal(t, "flag", two.Flag)
@@ -1252,6 +1257,10 @@ func TestDynamicCommands(t *testing.T) {
err = kctx.Run() err = kctx.Run()
require.NoError(t, err) require.NoError(t, err)
require.True(t, two.ran) require.True(t, two.ran)
_, err = p.Parse([]string{"--help"})
require.EqualError(t, err, `expected one of "one", "two"`)
require.NotContains(t, help.String(), "three", help.String())
} }
func TestDuplicateShortflags(t *testing.T) { func TestDuplicateShortflags(t *testing.T) {
+5 -1
View File
@@ -58,19 +58,23 @@ type dynamicCommand struct {
name string name string
help string help string
group string group string
tags []string
cmd interface{} cmd interface{}
} }
// DynamicCommand registers a dynamically constructed command with the root of the CLI. // DynamicCommand registers a dynamically constructed command with the root of the CLI.
// //
// This is useful for command-line structures that are extensible via user-provided plugins. // This is useful for command-line structures that are extensible via user-provided plugins.
func DynamicCommand(name, help, group string, cmd interface{}) Option { //
// "tags" is a list of extra tag strings to parse, in the form <key>:"<value>".
func DynamicCommand(name, help, group string, cmd interface{}, tags ...string) Option {
return OptionFunc(func(k *Kong) error { return OptionFunc(func(k *Kong) error {
k.dynamicCommands = append(k.dynamicCommands, &dynamicCommand{ k.dynamicCommands = append(k.dynamicCommands, &dynamicCommand{
name: name, name: name,
help: help, help: help,
group: group, group: group,
cmd: cmd, cmd: cmd,
tags: tags,
}) })
return nil return nil
}) })
+33 -16
View File
@@ -129,24 +129,41 @@ func tagSplitFn(r rune) bool {
return r == ',' || r == ' ' return r == ',' || r == ' '
} }
func parseTag(parent, fv reflect.Value, ft reflect.StructField) *Tag { func parseTagString(s string) *Tag {
t := &Tag{
items: parseTagItems(s, bareChars),
}
err := hydrateTag(t, "", false)
if err != nil {
fail("%s: %s", s, err)
}
return t
}
func parseTag(parent reflect.Value, ft reflect.StructField) *Tag {
if ft.Tag.Get("kong") == "-" { if ft.Tag.Get("kong") == "-" {
t := newEmptyTag() t := newEmptyTag()
t.Ignored = true t.Ignored = true
return t return t
} }
var ( t := &Tag{
err error items: parseTagItems(getTagInfo(ft)),
t = &Tag{ }
items: parseTagItems(getTagInfo(ft)), err := hydrateTag(t, ft.Type.Name(), ft.Type.Kind() == reflect.Bool)
} if err != nil {
) failField(parent, ft, "%s", err)
}
return t
}
func hydrateTag(t *Tag, typeName string, isBool bool) error {
var err error
t.Cmd = t.Has("cmd") t.Cmd = t.Has("cmd")
t.Arg = t.Has("arg") t.Arg = t.Has("arg")
required := t.Has("required") required := t.Has("required")
optional := t.Has("optional") optional := t.Has("optional")
if required && optional { if required && optional {
failField(parent, ft, "can't specify both required and optional") return fmt.Errorf("can't specify both required and optional")
} }
t.Required = required t.Required = required
t.Optional = optional t.Optional = optional
@@ -161,7 +178,7 @@ func parseTag(parent, fv reflect.Value, ft reflect.StructField) *Tag {
t.Env = t.Get("env") t.Env = t.Get("env")
t.Short, err = t.GetRune("short") t.Short, err = t.GetRune("short")
if err != nil && t.Get("short") != "" { if err != nil && t.Get("short") != "" {
failField(parent, ft, "invalid short flag name %q: %s", t.Get("short"), err) return fmt.Errorf("invalid short flag name %q: %s", t.Get("short"), err)
} }
t.Hidden = t.Has("hidden") t.Hidden = t.Has("hidden")
t.Format = t.Get("format") t.Format = t.Get("format")
@@ -174,8 +191,8 @@ func parseTag(parent, fv reflect.Value, ft reflect.StructField) *Tag {
t.Prefix = t.Get("prefix") t.Prefix = t.Get("prefix")
t.Embed = t.Has("embed") t.Embed = t.Has("embed")
negatable := t.Has("negatable") negatable := t.Has("negatable")
if negatable && ft.Type.Kind() != reflect.Bool { if negatable && !isBool {
failField(parent, ft, "negatable can only be set on booleans") return fmt.Errorf("negatable can only be set on booleans")
} }
t.Negatable = negatable t.Negatable = negatable
aliases := t.Get("aliases") aliases := t.Get("aliases")
@@ -186,24 +203,24 @@ func parseTag(parent, fv reflect.Value, ft reflect.StructField) *Tag {
for _, set := range t.GetAll("set") { for _, set := range t.GetAll("set") {
parts := strings.SplitN(set, "=", 2) parts := strings.SplitN(set, "=", 2)
if len(parts) == 0 { if len(parts) == 0 {
failField(parent, ft, "set should be in the form key=value but got %q", set) return fmt.Errorf("set should be in the form key=value but got %q", set)
} }
t.Vars[parts[0]] = parts[1] t.Vars[parts[0]] = parts[1]
} }
t.PlaceHolder = t.Get("placeholder") t.PlaceHolder = t.Get("placeholder")
if t.PlaceHolder == "" { if t.PlaceHolder == "" {
t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name())) t.PlaceHolder = strings.ToUpper(dashedString(typeName))
} }
t.Enum = t.Get("enum") t.Enum = t.Get("enum")
if t.Enum != "" && !(t.Required || t.Default != "") { if t.Enum != "" && !(t.Required || t.Default != "") {
failField(parent, ft, "enum value is only valid if it is either required or has a valid default value") return fmt.Errorf("enum value is only valid if it is either required or has a valid default value")
} }
passthrough := t.Has("passthrough") passthrough := t.Has("passthrough")
if passthrough && !t.Arg { if passthrough && !t.Arg {
failField(parent, ft, "passthrough only makes sense for positional arguments") return fmt.Errorf("passthrough only makes sense for positional arguments")
} }
t.Passthrough = passthrough t.Passthrough = passthrough
return t return nil
} }
// Has returns true if the tag contained the given key. // Has returns true if the tag contained the given key.