From 259d19076d5cf65e8ee4a896880ceb5fb1874bdb Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Wed, 16 Sep 2020 15:50:50 +1000 Subject: [PATCH] Add support for plugins. --- README.md | 80 +++++++++++++++++++++++++++++++++------------------- build.go | 45 ++++++++++++++++++----------- kong_test.go | 21 ++++++++++++++ 3 files changed, 100 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index e38778f..3dea00e 100644 --- a/README.md +++ b/README.md @@ -4,34 +4,36 @@ # Kong is a command-line parser for Go [![](https://godoc.org/github.com/alecthomas/kong?status.svg)](http://godoc.org/github.com/alecthomas/kong) [![CircleCI](https://img.shields.io/circleci/project/github/alecthomas/kong.svg)](https://circleci.com/gh/alecthomas/kong) [![Go Report Card](https://goreportcard.com/badge/github.com/alecthomas/kong)](https://goreportcard.com/report/github.com/alecthomas/kong) [![Slack chat](https://img.shields.io/static/v1?logo=slack&style=flat&label=slack&color=green&message=gophers)](https://gophers.slack.com/messages/CN9DS8YF3) -[TOC levels=2-3 numbered]: # "#### Table of Contents" + -#### Table of Contents -1. [Introduction](#introduction) -1. [Help](#help) -1. [Command handling](#command-handling) - 1. [Switch on the command string](#switch-on-the-command-string) - 1. [Attach a `Run(...) error` method to each command](#attach-a-run-error-method-to-each-command) -1. [Hooks: BeforeResolve(), BeforeApply(), AfterApply() and the Bind() option](#hooks-beforeresolve-beforeapply-afterapply-and-the-bind-option) -1. [Flags](#flags) -1. [Commands and sub-commands](#commands-and-sub-commands) -1. [Branching positional arguments](#branching-positional-arguments) -1. [Terminating positional arguments](#terminating-positional-arguments) -1. [Slices](#slices) -1. [Maps](#maps) -1. [Custom named decoders](#custom-named-decoders) -1. [Custom decoders (mappers)](#custom-decoders-mappers) -1. [Supported tags](#supported-tags) -1. [Variable interpolation](#variable-interpolation) -1. [Modifying Kong's behaviour](#modifying-kongs-behaviour) - 1. [`Name(help)` and `Description(help)` - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description) - 1. [`Configuration(loader, paths...)` - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files) - 1. [`Resolver(...)` - support for default values from external sources](#resolver---support-for-default-values-from-external-sources) - 1. [`*Mapper(...)` - customising how the command-line is mapped to Go values](#mapper---customising-how-the-command-line-is-mapped-to-go-values) - 1. [`ConfigureHelp(HelpOptions)` and `Help(HelpFunc)` - customising help](#configurehelphelpoptions-and-helphelpfunc---customising-help) - 1. [`Bind(...)` - bind values for callback hooks and Run() methods](#bind---bind-values-for-callback-hooks-and-run-methods) - 1. [Other options](#other-options) +- [Introduction](#introduction) +- [Help](#help) +- [Command handling](#command-handling) + - [Switch on the command string](#switch-on-the-command-string) + - [Attach a `Run(...) error` method to each command](#attach-a-run-error-method-to-each-command) +- [Hooks: BeforeResolve(), BeforeApply(), AfterApply() and the Bind() option](#hooks-beforeresolve-beforeapply-afterapply-and-the-bind-option) +- [Flags](#flags) +- [Commands and sub-commands](#commands-and-sub-commands) +- [Branching positional arguments](#branching-positional-arguments) +- [Terminating positional arguments](#terminating-positional-arguments) +- [Slices](#slices) +- [Maps](#maps) +- [Custom named decoders](#custom-named-decoders) +- [Supported field types](#supported-field-types) +- [Custom decoders (mappers)](#custom-decoders-mappers) +- [Supported tags](#supported-tags) +- [Plugins](#plugins) +- [Variable interpolation](#variable-interpolation) +- [Modifying Kong's behaviour](#modifying-kongs-behaviour) + - [`Name(help)` and `Description(help)` - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description) + - [`Configuration(loader, paths...)` - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files) + - [`Resolver(...)` - support for default values from external sources](#resolver---support-for-default-values-from-external-sources) + - [`*Mapper(...)` - customising how the command-line is mapped to Go values](#mapper---customising-how-the-command-line-is-mapped-to-go-values) + - [`ConfigureHelp(HelpOptions)` and `Help(HelpFunc)` - customising help](#configurehelphelpoptions-and-helphelpfunc---customising-help) + - [`Bind(...)` - bind values for callback hooks and Run() methods](#bind---bind-values-for-callback-hooks-and-run-methods) + - [Other options](#other-options) + ## Introduction @@ -370,11 +372,11 @@ Kong includes a number of builtin custom type mappers. These can be used by specifying the tag `type:""`. They are registered with the option function `NamedMapper(name, mapper)`. -| Name | Description +| Name | Description |-------------------|--------------------------------------------------- -| `path` | A path. ~ expansion is applied. +| `path` | A path. ~ expansion is applied. | `existingfile` | An existing file. ~ expansion is applied. `-` is accepted for stdin. -| `existingdir` | An existing directory. ~ expansion is applied. +| `existingdir` | An existing directory. ~ expansion is applied. | `counter` | Increment a numeric field. Useful for `-vvv`. Can accept `-s`, `--long` or `--long=N`. @@ -437,6 +439,26 @@ Tag | Description `embed` | If present, this field's children will be embedded in the parent. Useful for composition. `-` | Ignore the field. Useful for adding non-CLI fields to a configuration struct. +## Plugins + +Kong CLI's can be extended by embedding the `kong.Plugin` type and populating it with pointers to Kong annotated structs. For example: + +```go +var pluginOne struct { + PluginOneFlag string +} +var pluginTwo struct { + PluginTwoFlag string +} +var cli struct { + BaseFlag string + kong.Plugins +} +cli.Plugins = kong.Plugins{&pluginOne, &pluginTwo} +``` + +Additionally if an interface type is embedded, it can also be populated with a Kong annotated struct. + ## Variable interpolation Kong supports limited variable interpolation into help strings, enum lists and diff --git a/build.go b/build.go index 70fed36..9135b4d 100644 --- a/build.go +++ b/build.go @@ -6,6 +6,11 @@ import ( "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) @@ -50,28 +55,34 @@ func flattenedFields(v reflect.Value) (out []flattenedField) { if tag.Ignored { continue } - if ft.Anonymous || tag.Embed { - if fv.Kind() == reflect.Interface { - fv = fv.Elem() + if !ft.Anonymous && !tag.Embed { + if fv.CanSet() { + out = append(out, flattenedField{field: ft, value: fv, tag: tag}) } - 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...) continue } - if !fv.CanSet() { + + // 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 } - out = append(out, flattenedField{field: ft, value: fv, tag: tag}) + 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 } diff --git a/kong_test.go b/kong_test.go index 6c2097a..8105641 100644 --- a/kong_test.go +++ b/kong_test.go @@ -866,3 +866,24 @@ func TestLoneHpyhen(t *testing.T) { require.NoError(t, err) require.Equal(t, "-", cli.Flag) } + +func TestPlugins(t *testing.T) { + var pluginOne struct { + One string + } + var pluginTwo struct { + Two string + } + var cli struct { + Base string + kong.Plugins + } + cli.Plugins = kong.Plugins{&pluginOne, &pluginTwo} + + p := mustNew(t, &cli) + _, err := p.Parse([]string{"--base=base", "--one=one", "--two=two"}) + require.NoError(t, err) + require.Equal(t, "base", cli.Base) + require.Equal(t, "one", pluginOne.One) + require.Equal(t, "two", pluginTwo.Two) +}