From f72f53d947e038efbff23ec3ea336460d205c48a Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 27 Sep 2018 14:20:16 +1000 Subject: [PATCH] Support for adding bindings to the Context. This is very useful for hooks to pre-construct objects that can be used by all subsequent downstream commands, for example. --- README.md | 28 +++------------------------- callbacks.go | 6 ++++++ context.go | 18 +++++++++++++++--- kong.go | 5 ++++- kong_test.go | 26 ++++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index bb791e5..b5ba8a9 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,6 @@ - ## Introduction Kong aims to support arbitrarily complex command-line structures with as little developer effort as possible. @@ -75,7 +74,6 @@ func main() { } ``` - ## Help Help is automatically generated. With no other arguments provided, help will display a full summary of all available commands. @@ -116,12 +114,10 @@ eg. -f, --force Force removal. -r, --recursive Recursively remove files. - ## Command handling There are two ways to handle commands in Kong. - ### Switch on the command string When you call `kong.Parse()` it will return a unique string representation of the command. Each command branch in the hierarchy will be a bare word and each branching argument or required positional argument will be the name surrounded by angle brackets. Here's an example: @@ -161,7 +157,6 @@ func main() { This has the advantage that it is convenient, but the downside that if you modify your CLI structure, the strings may change. This can be fragile. - ### Attach a `Run(...) error` method to each command A more robust approach is to break each command out into their own structs: @@ -175,6 +170,8 @@ In addition to values bound with the `kong.Bind(...)` option, any values passed through to `kong.Context.Run(...)` are also bindable to the target's `Run()` arguments. +Finally, hooks can also contribute bindings via `kong.Context.Bind()` and `kong.Context.BindTo()`. + There's a full example emulating part of the Docker CLI [here](https://github.com/alecthomas/kong/tree/master/_examples/docker). eg. @@ -217,14 +214,13 @@ func main() { ``` - ## Hooks: BeforeResolve(), BeforeApply(), AfterApply() and the Bind() option If a node in the grammar has a `BeforeResolve(...)`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those methods will be called before validation/assignment and after validation/assignment, respectively. The `--help` flag is implemented with a `BeforeApply` hook. -Arguments to hooks are provided via the `Bind(...)` option. `*Kong`, `*Context` and `*Path` are also bound. +Arguments to hooks are provided via the `Bind(...)` option. `*Kong`, `*Context` and `*Path` are also bound and finally, hooks can also contribute bindings via `kong.Context.Bind()` and `kong.Context.BindTo()`. eg. @@ -251,7 +247,6 @@ func main() { } ``` - ## Flags Any [mapped](#mapper---customising-how-the-command-line-is-mapped-to-go-values) field in the command structure *not* tagged with `cmd` or `arg` will be a flag. Flags are optional by default. @@ -264,7 +259,6 @@ type CLI struct { } ``` - ## Commands and sub-commands Sub-commands are specified by tagging a struct field with `cmd`. Kong supports arbitrarily nested commands. @@ -282,7 +276,6 @@ type CLI struct { } ``` - ## Branching positional arguments In addition to sub-commands, structs can also be configured as branching positional arguments. @@ -310,14 +303,12 @@ var CLI struct { This looks a little verbose in this contrived example, but typically this will not be the case. - ## Terminating positional arguments If a [mapped type](#mapper---customising-how-the-command-line-is-mapped-to-go-values) is tagged with `arg` it will be treated as the final positional values to be parsed on the command line. If a positional argument is a slice, all remaining arguments will be appended to that slice. - ## Slices Slice values are treated specially. First the input is split on the `sep:""` tag (defaults to `,`), then each element is parsed by the slice element type and appended to the slice. If the same value is encountered multiple times, elements continue to be appended. @@ -336,7 +327,6 @@ var CLI struct { } ``` - ## Maps Maps are similar to slices except that only one key/value pair can be assigned per value, and the `sep` tag denotes the assignment character and defaults to `=`. @@ -359,7 +349,6 @@ var CLI struct { For flags, multiple key+value pairs should be separated by `;` eg. `--set="key1=value1;key2=value2"`. - ## Custom named decoders Kong includes a number of builtin custom type mappers. These can be used by @@ -378,13 +367,11 @@ specifies the element type. For maps, the tag has the format `tag:"[]:[]"` where either may be omitted. - ## Custom decoders (mappers) If a field implements the [MapperValue](https://godoc.org/github.com/alecthomas/kong#MapperValue) interface it will be used to decode arguments into the field. - ## Supported tags Tags can be in two forms: @@ -416,7 +403,6 @@ Tag | Description `set:"K=V"` | Set a variable for expansion by child elements. Multiples can occur. `embed` | If present, this field's children will be embedded in the parent. Useful for composition. - ## Variable interpolation Kong supports limited variable interpolation into help strings, enum lists and @@ -454,14 +440,12 @@ func main() { } ``` - ## Modifying Kong's behaviour Each Kong parser can be configured via functional options passed to `New(cli interface{}, options...Option)`. The full set of options can be found [here](https://godoc.org/github.com/alecthomas/kong#Option). - ### `Name(help)` and `Description(help)` - set the application name description Set the application name and/or description. @@ -470,7 +454,6 @@ The name of the application will default to the binary name, but can be overridd As with all help in Kong, text will be wrapped to the terminal. - ### `Configuration(loader, paths...)` - load defaults from configuration files This option provides Kong with support for loading defaults from a set of configuration files. Each file is opened, if possible, and the loader called to create a resolver for that file. @@ -481,14 +464,12 @@ eg. kong.Parse(&cli, kong.Configuration(kong.JSON, "/etc/myapp.json", "~/.myapp.json")) ``` - ### `Resolver(...)` - support for default values from external sources Resolvers are Kong's extension point for providing default values from external sources. As an example, support for environment variables via the `env` tag is provided by a resolver. There's also a builtin resolver for JSON configuration files. Example resolvers can be found in [resolver.go](https://github.com/alecthomas/kong/blob/master/resolver.go). - ### `*Mapper(...)` - customising how the command-line is mapped to Go values Command-line arguments are mapped to Go values via the Mapper interface: @@ -511,7 +492,6 @@ All builtin Go types (as well as a bunch of useful stdlib types like `time.Time` 3. `TypeMapper(reflect.Type, Mapper)`. 4. `ValueMapper(interface{}, Mapper)`, passing in a pointer to a field of the grammar. - ### `ConfigureHelp(HelpOptions)` and `Help(HelpFunc)` - customising help The default help output is usually sufficient, but if not there are two solutions. @@ -519,12 +499,10 @@ The default help output is usually sufficient, but if not there are two solution 1. Use `ConfigureHelp(HelpOptions)` to configure how help is formatted (see [HelpOptions](https://godoc.org/github.com/alecthomas/kong#HelpOptions) for details). 2. Custom help can be wired into Kong via the `Help(HelpFunc)` option. The `HelpFunc` is passed a `Context`, which contains the parsed context for the current command-line. See the implementation of `PrintHelp` for an example. - ### `Bind(...)` - bind values for callback hooks and Run() methods See the [section on hooks](#BeforeApply-AfterApply-and-the-bind-option) for details. - ### Other options The full set of options can be found [here](https://godoc.org/github.com/alecthomas/kong#Option). diff --git a/callbacks.go b/callbacks.go index 351ee48..112d35a 100644 --- a/callbacks.go +++ b/callbacks.go @@ -23,6 +23,12 @@ func (b bindings) clone() bindings { return out } +func (b bindings) merge(other bindings) { + for k, v := range other { + b[k] = v + } +} + func getMethod(value reflect.Value, name string) reflect.Value { method := value.MethodByName(name) if !method.IsValid() { diff --git a/context.go b/context.go index 7f67199..201ce16 100644 --- a/context.go +++ b/context.go @@ -51,7 +51,8 @@ type Context struct { Error error values map[*Value]reflect.Value // Temporary values during tracing. - resolvers []Resolver // Extra context-specific resolvers. + bindings bindings + resolvers []Resolver // Extra context-specific resolvers. scan *Scanner } @@ -68,8 +69,9 @@ func Trace(k *Kong, args []string) (*Context, error) { Path: []*Path{ {App: k.Model, Flags: k.Model.Flags}, }, - values: map[*Value]reflect.Value{}, - scan: Scan(args...), + values: map[*Value]reflect.Value{}, + scan: Scan(args...), + bindings: bindings{}, } c.Error = c.trace(c.Model.Node) err := c.reset(c.Model.Node) @@ -80,6 +82,16 @@ func Trace(k *Kong, args []string) (*Context, error) { return c, nil } +// Bind adds bindings to the Context. +func (c *Context) Bind(args ...interface{}) { + c.bindings.add(args...) +} + +// BindTo adds a binding to the Context. +func (c *Context) BindTo(impl, iface interface{}) { + c.bindings[reflect.TypeOf(iface).Elem()] = reflect.ValueOf(impl) +} + // Value returns the value for a particular path element. func (c *Context) Value(path *Path) reflect.Value { switch { diff --git a/kong.go b/kong.go index 920e966..408cf44 100644 --- a/kong.go +++ b/kong.go @@ -239,7 +239,10 @@ func (k *Kong) applyHook(ctx *Context, name string) error { if !method.IsValid() { continue } - binds := k.bindings.clone().add(ctx, trace).add(trace.Node().Vars().CloneWith(k.vars)) + binds := k.bindings.clone() + binds.add(ctx, trace) + binds.add(trace.Node().Vars().CloneWith(k.vars)) + binds.merge(ctx.bindings) if err := callMethod(name, value, method, binds); err != nil { return err } diff --git a/kong_test.go b/kong_test.go index 5278c54..d8218bd 100644 --- a/kong_test.go +++ b/kong_test.go @@ -662,3 +662,29 @@ func TestEnum(t *testing.T) { _, err := mustNew(t, &cli).Parse([]string{"--flag", "d"}) require.EqualError(t, err, "--flag=STRING must be one of a,b,c but got \"\"") } + +type commandWithHook struct { + value string +} + +func (c *commandWithHook) AfterApply(cli *cliWithHook) error { + c.value = cli.Flag + return nil +} + +type cliWithHook struct { + Flag string + Command commandWithHook `cmd:""` +} + +func (c *cliWithHook) AfterApply(ctx *kong.Context) error { + ctx.Bind(c) + return nil +} + +func TestParentBindings(t *testing.T) { + cli := &cliWithHook{} + _, err := mustNew(t, cli).Parse([]string{"command", "--flag=foo"}) + require.NoError(t, err) + require.Equal(t, "foo", cli.Command.value) +}