diff --git a/README.md b/README.md index faae736..5520033 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ - [Custom decoders mappers](#custom-decoders-mappers) - [Supported tags](#supported-tags) - [Plugins](#plugins) +- [Dynamic Commands](#dynamic-commands) - [Variable interpolation](#variable-interpolation) - [Validation](#validation) - [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. +## Dynamic Commands + +While plugins give complete control over extending command-line interfaces, Kong +also supports dynamically adding commands via `kong.DynamicCommand()`. + ## Variable interpolation Kong supports limited variable interpolation into help strings, enum lists and diff --git a/build.go b/build.go index 35e9411..36581e3 100644 --- a/build.go +++ b/build.go @@ -51,7 +51,7 @@ func flattenedFields(v reflect.Value) (out []flattenedField) { for i := 0; i < v.NumField(); i++ { ft := v.Type().Field(i) fv := v.Field(i) - tag := parseTag(v, fv, ft) + tag := parseTag(v, ft) if tag.Ignored { continue } diff --git a/kong.go b/kong.go index 3edfc3b..aa60c91 100644 --- a/kong.go +++ b/kong.go @@ -118,7 +118,7 @@ func New(grammar interface{}, options ...Option) (*Kong, error) { // Synthesise command nodes. for _, dcmd := range k.dynamicCommands { - tag := newEmptyTag() + tag := parseTagString(strings.Join(dcmd.tags, " ")) tag.Name = dcmd.name tag.Help = dcmd.help tag.Group = dcmd.group diff --git a/kong_test.go b/kong_test.go index 33a2c52..7c81826 100644 --- a/kong_test.go +++ b/kong_test.go @@ -1242,9 +1242,14 @@ func TestDynamicCommands(t *testing.T) { cli := struct { One struct{} `cmd:"one"` }{} + help := &strings.Builder{} two := &dynamicCommand{} - var twoi interface{} = &two - p := mustNew(t, &cli, kong.DynamicCommand("two", "", "", twoi)) + three := &dynamicCommand{} + 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"}) require.NoError(t, err) require.Equal(t, "flag", two.Flag) @@ -1252,6 +1257,10 @@ func TestDynamicCommands(t *testing.T) { err = kctx.Run() require.NoError(t, err) 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) { diff --git a/options.go b/options.go index 57a8f13..aec57d6 100644 --- a/options.go +++ b/options.go @@ -58,19 +58,23 @@ type dynamicCommand struct { name string help string group string + tags []string cmd interface{} } // 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. -func DynamicCommand(name, help, group string, cmd interface{}) Option { +// +// "tags" is a list of extra tag strings to parse, in the form :"". +func DynamicCommand(name, help, group string, cmd interface{}, tags ...string) Option { return OptionFunc(func(k *Kong) error { k.dynamicCommands = append(k.dynamicCommands, &dynamicCommand{ name: name, help: help, group: group, cmd: cmd, + tags: tags, }) return nil }) diff --git a/tag.go b/tag.go index 7c9ae99..c30052b 100644 --- a/tag.go +++ b/tag.go @@ -129,24 +129,41 @@ func tagSplitFn(r rune) bool { 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") == "-" { t := newEmptyTag() t.Ignored = true return t } - var ( - err error - t = &Tag{ - 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.Arg = t.Has("arg") required := t.Has("required") optional := t.Has("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.Optional = optional @@ -161,7 +178,7 @@ func parseTag(parent, fv reflect.Value, ft reflect.StructField) *Tag { t.Env = t.Get("env") t.Short, err = t.GetRune("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.Format = t.Get("format") @@ -174,8 +191,8 @@ func parseTag(parent, fv reflect.Value, ft reflect.StructField) *Tag { t.Prefix = t.Get("prefix") t.Embed = t.Has("embed") negatable := t.Has("negatable") - if negatable && ft.Type.Kind() != reflect.Bool { - failField(parent, ft, "negatable can only be set on booleans") + if negatable && !isBool { + return fmt.Errorf("negatable can only be set on booleans") } t.Negatable = negatable aliases := t.Get("aliases") @@ -186,24 +203,24 @@ func parseTag(parent, fv reflect.Value, ft reflect.StructField) *Tag { for _, set := range t.GetAll("set") { parts := strings.SplitN(set, "=", 2) 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.PlaceHolder = t.Get("placeholder") if t.PlaceHolder == "" { - t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name())) + t.PlaceHolder = strings.ToUpper(dashedString(typeName)) } t.Enum = t.Get("enum") 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") 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 - return t + return nil } // Has returns true if the tag contained the given key.