diff --git a/kong.go b/kong.go index b59d61c..9c0e437 100644 --- a/kong.go +++ b/kong.go @@ -66,6 +66,7 @@ type Kong struct { // Set temporarily by Options. These are applied after build(). postBuildOptions []Option + dynamicCommands []*dynamicCommand } // New creates a new Kong parser on grammar. @@ -106,6 +107,20 @@ func New(grammar interface{}, options ...Option) (*Kong, error) { k.Model = model k.Model.HelpFlag = k.helpFlag + // Synthesise command nodes. + for _, dcmd := range k.dynamicCommands { + tag := newEmptyTag() + tag.Name = dcmd.name + tag.Help = dcmd.help + tag.Group = dcmd.group + tag.Cmd = true + v := reflect.Indirect(reflect.ValueOf(dcmd.cmd)) + buildChild(k, k.Model.Node, CommandNode, reflect.Value{}, reflect.StructField{ + Name: dcmd.name, + Type: v.Type(), + }, v, tag, dcmd.name, map[string]bool{}) + } + for _, option := range k.postBuildOptions { if err = option.Apply(k); err != nil { return nil, err diff --git a/kong_test.go b/kong_test.go index 9163141..2f333b7 100644 --- a/kong_test.go +++ b/kong_test.go @@ -1002,3 +1002,30 @@ func TestPointers(t *testing.T) { require.NotNil(t, cli.JSON) require.Equal(t, "FOO", string(*cli.JSON)) } + +type dynamicCommand struct { + Flag string + + ran bool +} + +func (d *dynamicCommand) Run() error { + d.ran = true + return nil +} + +func TestDynamicCommands(t *testing.T) { + cli := struct { + One struct{} `cmd:"one"` + }{} + two := &dynamicCommand{} + var twoi interface{} = &two + p := mustNew(t, &cli, kong.DynamicCommand("two", "", "", twoi)) + kctx, err := p.Parse([]string{"two", "--flag=flag"}) + require.NoError(t, err) + require.Equal(t, "flag", two.Flag) + require.False(t, two.ran) + err = kctx.Run() + require.NoError(t, err) + require.True(t, two.ran) +} diff --git a/options.go b/options.go index ef83970..259b6b1 100644 --- a/options.go +++ b/options.go @@ -54,6 +54,28 @@ func Exit(exit func(int)) Option { }) } +type dynamicCommand struct { + name string + help string + group 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 { + return OptionFunc(func(k *Kong) error { + k.dynamicCommands = append(k.dynamicCommands, &dynamicCommand{ + name: name, + help: help, + group: group, + cmd: cmd, + }) + return nil + }) +} + // NoDefaultHelp disables the default help flags. func NoDefaultHelp() Option { return OptionFunc(func(k *Kong) error {