From 29fe92f28634fdac35207c08ec5f74212f391ebc Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Wed, 13 Jun 2018 12:07:35 +1000 Subject: [PATCH] Only reset grammar when Apply()ing. Also add a Kong.Help() function for writing context-sensitive help. --- README.md | 117 +++++++++++++++++++++++++++++++----------------- context.go | 69 ++++++++++++++-------------- global_test.go | 2 +- help_test.go | 2 +- kong.go | 20 ++++++--- kong_test.go | 4 +- options.go | 4 +- options_test.go | 2 +- 8 files changed, 132 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 489a28e..aefc505 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +

# Kong is a command-line parser for Go [![CircleCI](https://circleci.com/gh/alecthomas/kong.svg?style=svg&circle-token=477fecac758383bf281453187269b913130f17d2)](https://circleci.com/gh/alecthomas/kong) @@ -8,17 +9,20 @@ 1. [Help](#help) 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. [Supported tags](#supported-tags) 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. [`Help(HelpFunc)` - customising help](#helphelpfunc---customising-help) 1. [`Hook(&field, HookFunc)` - callback hooks to execute when the command-line is parsed](#hookfield-hookfunc---callback-hooks-to-execute-when-the-command-line-is-parsed) + 1. [Other options](#other-options) - ## Introduction Kong aims to support arbitrarily complex command-line structures with as little developer effort as possible. @@ -27,10 +31,8 @@ To achieve that, command-lines are expressed as Go types, with the structure and For example, the following command-line: -``` -shell rm [-f] [-r] ... -shell ls [ ...] -``` + shell rm [-f] [-r] ... + shell ls [ ...] Can be represented by the following command-line structure: @@ -63,59 +65,78 @@ Help is automatically generated. With no other arguments provided, help will dis eg. -``` -$ shell --help -usage: shell + $ shell --help + usage: shell -A shell-like example app. + A shell-like example app. -Flags: - --help Show context-sensitive help. - --debug Debug mode. + Flags: + --help Show context-sensitive help. + --debug Debug mode. -Commands: - rm ... - Remove files. + Commands: + rm ... + Remove files. - ls [ ...] - List paths. -``` + ls [ ...] + List paths. If a command is provided, the help will show full detail on the command including all available flags. eg. -``` -$ shell --help rm -usage: shell rm ... + $ shell --help rm + usage: shell rm ... -Remove files. + Remove files. -Arguments: - ... Paths to remove. + Arguments: + ... Paths to remove. -Flags: - --debug Debug mode. + Flags: + --debug Debug mode. - -f, --force Force removal. - -r, --recursive Recursively remove files. -``` + -f, --force Force removal. + -r, --recursive Recursively remove files. ## Flags -Any field in the command structure *not* tagged with `cmd` or `arg` will be a flag. Flags are optional by default. +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. + +eg. The command-line `app [--flag="foo"]` can be represented by the following. + +```go +type CLI struct { + Flag string +} +``` ## Commands and sub-commands -Kong supports arbitrarily nested commands and positional arguments. Nested structs tagged with `cmd` will be treated as commands. +Sub-commands are specified by tagging a struct field with `cmd`. Kong supports arbitrarily nested commands. -Arguments can also optionally have children, in order to support commands like the following: +eg. The following struct represents the CLI structure `command [--flag="str"] sub-command`. -``` -app rename to +```go +type CLI struct { + Command struct { + Flag string + + SubCommand struct { + } `cmd` + } `cmd` +} ``` -This is achieved by tagging a nested struct with `arg`, then including a positional argument field inside that struct with the same name. For example: +## Branching positional arguments + +In addition to sub-commands, structs can also be configured as branching positional arguments. + +This is achieved by tagging an [unmapped](#mapper---customising-how-the-command-line-is-mapped-to-go-values) nested struct field with `arg`, then including a positional argument field inside that struct _with the same name_. For example, the following command structure: + + app rename to + +Can be represented with the following: ```go var CLI struct { @@ -131,8 +152,13 @@ var CLI struct { } `cmd` } ``` + 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`, + ## Supported tags Tags can be in two forms: @@ -147,12 +173,12 @@ Both can coexist with standard Tag parsing. | `cmd` | If present, struct is a command. | | `arg` | If present, field is an argument. | | `env:"X"` | Specify envar to use for default value. -| `type:"X"` | Specify named Mapper to use. | +| `name:"X"` | Long name, for overriding field name. | | `help:"X"` | Help text. | +| `type:"X"` | Specify named Mapper to use. | | `placeholder:"X"` | Placeholder text. | | `default:"X"` | Default value. | | `short:"X"` | Short name, if flag. | -| `name:"X"` | Long name, for overriding field name. | | `required` | If present, flag/arg is required. | | `optional` | If present, flag/arg is optional. | | `hidden` | If present, flag is hidden. | @@ -163,7 +189,15 @@ Both can coexist with standard Tag parsing. Each Kong parser can be configured via functional options passed to `New(cli interface{}, options...Option)`. -The full set of options can be found in [options.go](https://github.com/alecthomas/kong/blob/master/options.go). +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. + +The name of the application will default to the binary name, but can be overridden with `Name(name)`. + +As with all help in Kong, text will be wrapped to the terminal. ### `Configuration(loader, paths...)` - load defaults from configuration files @@ -201,8 +235,7 @@ All builtin Go types (as well as a bunch of useful stdlib types like `time.Time` 1. `NamedMapper(string, Mapper)` and using the tag key `type:""`. 2. `KindMapper(reflect.Kind, Mapper)`. 3. `TypeMapper(reflect.Type, Mapper)`. -4. `ValueMapper(interface{}, Mapper)`, passing in a pointer to a field of the grammar. - +4. `ValueMapper(interface{}, Mapper)`, passing in a pointer to a field of the grammar. ### `Help(HelpFunc)` - customising help @@ -230,3 +263,7 @@ if CLI.Debug { ``` But under some circumstances, hooks are the right choice. + +### Other options + +The full set of options can be found [here](https://godoc.org/github.com/alecthomas/kong#Option). diff --git a/context.go b/context.go index 20ef8c5..1f39e74 100755 --- a/context.go +++ b/context.go @@ -55,6 +55,8 @@ func (c *Context) Selected() *Node { // Trace path of "args" through the gammar tree. // // The returned Context will include a Path of all commands, arguments, positionals and flags. +// +// Note that this will not modify the target grammar. Call Apply() to do so. func Trace(k *Kong, args []string) (*Context, error) { c := &Context{ App: k, @@ -62,17 +64,10 @@ func Trace(k *Kong, args []string) (*Context, error) { Path: []*Path{ {App: k.Model, Flags: k.Model.Flags, Value: k.Model.Target}, }, - } - err := c.reset(&c.App.Model.Node) - if err != nil { - return nil, err + scan: Scan(args...), } c.Error = c.trace(&c.App.Model.Node) - err = c.traceResolvers() - if err != nil { - return nil, err - } - return c, nil + return c, c.traceResolvers() } // Validate the current context. @@ -155,7 +150,6 @@ func (c *Context) FlagValue(flag *Flag) reflect.Value { // Recursively reset values to defaults (as specified in the grammar) or the zero value. func (c *Context) reset(node *Node) error { - c.scan = Scan(c.args...) for _, flag := range node.Flags { err := flag.Value.Reset() if err != nil { @@ -310,31 +304,6 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo return nil } -// Apply traced context to the target grammar. -func (c *Context) Apply() (string, error) { - path := []string{} - - for _, trace := range c.Path { - switch { - case trace.App != nil: - case trace.Argument != nil: - path = append(path, "<"+trace.Argument.Name+">") - trace.Argument.Argument.Apply(trace.Value) - case trace.Command != nil: - path = append(path, trace.Command.Name) - case trace.Flag != nil: - trace.Flag.Value.Apply(trace.Value) - case trace.Positional != nil: - path = append(path, "<"+trace.Positional.Name+">") - trace.Positional.Apply(trace.Value) - default: - panic("unsupported path ?!") - } - } - - return strings.Join(path, " "), nil -} - // Walk through flags from existing nodes in the path. func (c *Context) traceResolvers() error { if len(c.App.resolvers) == 0 { @@ -370,6 +339,36 @@ func (c *Context) traceResolvers() error { return nil } +// Apply traced context to the target grammar. +func (c *Context) Apply() (string, error) { + err := c.reset(&c.App.Model.Node) + if err != nil { + return "", err + } + + path := []string{} + + for _, trace := range c.Path { + switch { + case trace.App != nil: + case trace.Argument != nil: + path = append(path, "<"+trace.Argument.Name+">") + trace.Argument.Argument.Apply(trace.Value) + case trace.Command != nil: + path = append(path, trace.Command.Name) + case trace.Flag != nil: + trace.Flag.Value.Apply(trace.Value) + case trace.Positional != nil: + path = append(path, "<"+trace.Positional.Name+">") + trace.Positional.Apply(trace.Value) + default: + panic("unsupported path ?!") + } + } + + return strings.Join(path, " "), nil +} + func (c *Context) matchFlags(flags []*Flag, matcher func(f *Flag) bool) (err error) { defer catch(&err) token := c.scan.Peek() diff --git a/global_test.go b/global_test.go index e371c62..ed0ee83 100644 --- a/global_test.go +++ b/global_test.go @@ -25,7 +25,7 @@ func TestParseHandlingBadBuild(t *testing.T) { } }() - Parse(&cli, ExitFunction(func(_ int) { panic("exiting") })) + Parse(&cli, Exit(func(_ int) { panic("exiting") })) require.Fail(t, "we were expecting a panic") } diff --git a/help_test.go b/help_test.go index b1dfffe..5d5e76d 100644 --- a/help_test.go +++ b/help_test.go @@ -38,7 +38,7 @@ func TestHelp(t *testing.T) { Name("test-app"), Description("A test app."), Writers(w, w), - ExitFunction(func(int) { + Exit(func(int) { exited = true panic(true) // Panic to fake "exit". }), diff --git a/kong.go b/kong.go index dc2fbf3..98ea668 100644 --- a/kong.go +++ b/kong.go @@ -108,9 +108,15 @@ func (k *Kong) extraFlags() []*Flag { return []*Flag{helpFlag} } -// Trace parses the command-line, validating and collecting matching grammar nodes. -func (k *Kong) Trace(args []string) (*Context, error) { - return Trace(k, args) +// Help writes help for the given args to the stdout io.Writer associated with this Kong. +// +// See Help() and Writers() for overriding the help function and stdout, respectively. +func (k *Kong) Help(args []string) error { + ctx, err := Trace(k, args) + if err != nil { + return err + } + return k.help(ctx) } // Parse arguments into target. @@ -119,7 +125,7 @@ func (k *Kong) Trace(args []string) (*Context, error) { // the command name while positional arguments are the argument name surrounded by "". func (k *Kong) Parse(args []string) (command string, err error) { defer catch(&err) - ctx, err := k.Trace(args) + ctx, err := Trace(k, args) if err != nil { return "", err } @@ -165,13 +171,15 @@ func (k *Kong) applyHooks(ctx *Context) error { } // Printf writes a message to Kong.Stdout with the application name prefixed. -func (k *Kong) Printf(format string, args ...interface{}) { +func (k *Kong) Printf(format string, args ...interface{}) *Kong { fmt.Fprintf(k.Stdout, k.Model.Name+": "+format, args...) + return k } // Errorf writes a message to Kong.Stderr with the application name prefixed. -func (k *Kong) Errorf(format string, args ...interface{}) { +func (k *Kong) Errorf(format string, args ...interface{}) *Kong { fmt.Fprintf(k.Stderr, k.Model.Name+": error: "+format, args...) + return k } // FatalIfErrorf terminates with an error message if err != nil. diff --git a/kong_test.go b/kong_test.go index 1835ec4..5a169e4 100644 --- a/kong_test.go +++ b/kong_test.go @@ -10,7 +10,7 @@ import ( func mustNew(t *testing.T, cli interface{}, options ...Option) *Kong { t.Helper() options = append([]Option{ - ExitFunction(func(int) { + Exit(func(int) { t.Helper() t.Fatalf("unexpected exit()") }), @@ -322,7 +322,7 @@ func TestTraceErrorPartiallySucceeds(t *testing.T) { } `kong:"cmd"` } p := mustNew(t, &cli) - ctx, err := p.Trace([]string{"one", "bad"}) + ctx, err := Trace(p, []string{"one", "bad"}) require.NoError(t, err) require.Error(t, ctx.Error) require.Equal(t, []string{"one"}, ctx.Command()) diff --git a/options.go b/options.go index db43d7e..93d00e9 100755 --- a/options.go +++ b/options.go @@ -12,8 +12,8 @@ import ( // An Option applies optional changes to the Kong application. type Option func(k *Kong) -// ExitFunction overrides the function used to terminate. This is useful for testing or interactive use. -func ExitFunction(exit func(int)) Option { +// Exit overrides the function used to terminate. This is useful for testing or interactive use. +func Exit(exit func(int)) Option { return func(k *Kong) { k.Exit = exit } } diff --git a/options_test.go b/options_test.go index fed4a81..ffbc521 100644 --- a/options_test.go +++ b/options_test.go @@ -11,7 +11,7 @@ import ( func TestOptions(t *testing.T) { var cli struct{} - p, err := New(&cli, Name("name"), Description("description"), Writers(nil, nil), ExitFunction(nil)) + p, err := New(&cli, Name("name"), Description("description"), Writers(nil, nil), Exit(nil)) require.NoError(t, err) require.Equal(t, "name", p.Model.Name) require.Equal(t, "description", p.Model.Help)