@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user