From cdcdf49f678926ce07bbe0f94a6b9a176054d4e2 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 21 Jun 2018 10:08:01 +1000 Subject: [PATCH] Display usage information on error. --- README.md | 4 ++-- _examples/shell/main.go | 2 +- context.go | 11 +++++++---- error.go | 12 ++++++++++++ kong.go | 27 ++++++++++++++++++--------- options.go | 8 ++++++++ tag.go | 5 +++++ 7 files changed, 53 insertions(+), 16 deletions(-) create mode 100644 error.go diff --git a/README.md b/README.md index 6aeb048..6758dca 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ 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. [`HelpOptions(...HelpOption)` and `Help(HelpFunc)` - customising help](#helpoptionshelpoption-and-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) @@ -279,7 +279,7 @@ 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. -### `Help(HelpFunc)` - customising help +### `HelpOptions(...HelpOption)` and `Help(HelpFunc)` - customising help The default help output is usually sufficient, but if it's not, there are two solutions. diff --git a/_examples/shell/main.go b/_examples/shell/main.go index 36813ed..46e07f5 100644 --- a/_examples/shell/main.go +++ b/_examples/shell/main.go @@ -14,7 +14,7 @@ var cli struct { Force bool `help:"Force removal." short:"f"` Recursive bool `help:"Recursively remove files." short:"r"` - Paths []string `arg:"" help:"Paths to remove." type:"path"` + Paths []string `arg:"" help:"Paths to remove." type:"path" name:"path"` } `cmd:"" help:"Remove files."` Ls struct { diff --git a/context.go b/context.go index 3f22df7..5a56565 100644 --- a/context.go +++ b/context.go @@ -43,11 +43,11 @@ func (p *Path) Node() *Node { // Context contains the current parse context. type Context struct { App *Kong - Path []*Path // A trace through parsed nodes. - Error error // Error that occurred during trace, if any. + Path []*Path // A trace through parsed nodes. + Args []string // Original command-line arguments. + Error error // Error that occurred during trace, if any. values map[*Value]reflect.Value // Temporary values during tracing. - args []string scan *Scanner } @@ -86,7 +86,7 @@ func (c *Context) Selected() *Node { func Trace(k *Kong, args []string) (*Context, error) { c := &Context{ App: k, - args: args, + Args: args, Path: []*Path{ {App: k.Model, Flags: k.Model.Flags}, }, @@ -461,6 +461,9 @@ func checkMissingChildren(node *Node) error { if len(missing) == 1 { return fmt.Errorf("%q should be followed by %s", node.Path(), missing[0]) } + if len(missing) > 5 { + missing = append(missing[:5], "...") + } return fmt.Errorf("%q should be followed by one of %s", node.Path(), strings.Join(missing, ", ")) } diff --git a/error.go b/error.go new file mode 100644 index 0000000..30b8858 --- /dev/null +++ b/error.go @@ -0,0 +1,12 @@ +package kong + +// ParseError is the error type returned by Kong.Parse(). +// +// It contains the parse Context that triggered the error. +type ParseError struct { + error + Context *Context +} + +// Cause returns the original cause of the error. +func (p *ParseError) Cause() error { return p.error } diff --git a/kong.go b/kong.go index a0d750e..ed7ff80 100644 --- a/kong.go +++ b/kong.go @@ -38,12 +38,13 @@ type Kong struct { Stdout io.Writer Stderr io.Writer - before map[reflect.Value]HookFunc - resolvers []ResolverFunc - registry *Registry - noDefaultHelp bool - help func(*Context) error - helpOptions []HelpOption + before map[reflect.Value]HookFunc + resolvers []ResolverFunc + registry *Registry + noDefaultHelp bool + noUsageOnError bool + help func(*Context) error + helpOptions []HelpOption // Set temporarily by Options. These are applied after build(). postBuildOptions []Option @@ -133,6 +134,9 @@ func (k *Kong) Help(args []string) error { // // The returned "command" is a space separated path to the final selected command, if any. Commands appear as // the command name while positional arguments are the argument name surrounded by "". +// +// Will return a ParseError if a *semantically* invalid command-line is encountered (as opposed to a syntactically +// invalid one, which will report a normal error). func (k *Kong) Parse(args []string) (command string, err error) { defer catch(&err) ctx, err := Trace(k, args) @@ -140,13 +144,13 @@ func (k *Kong) Parse(args []string) (command string, err error) { return "", err } if err = k.applyHooks(ctx); err != nil { - return "", err + return "", &ParseError{error: err, Context: ctx} } if ctx.Error != nil { - return "", ctx.Error + return "", &ParseError{error: ctx.Error, Context: ctx} } if err = ctx.Validate(); err != nil { - return "", err + return "", &ParseError{error: err, Context: ctx} } return ctx.Apply() } @@ -210,6 +214,11 @@ func (k *Kong) FatalIfErrorf(err error, args ...interface{}) { msg = fmt.Sprintf(args[0].(string), args[1:]...) + ": " + err.Error() } k.Errorf("%s", msg) + // Maybe display usage information. + if err, ok := err.(*ParseError); ok && !k.noUsageOnError { + fmt.Fprintln(k.Stdout) + _ = k.help(err.Context) + } k.Exit(1) } diff --git a/options.go b/options.go index eeb9468..2a4f3ef 100644 --- a/options.go +++ b/options.go @@ -132,6 +132,14 @@ func HelpOptions(options ...HelpOption) Option { } } +// NoUsageOnError configures Kong to NOT display context-sensitive usage if FatalIfErrorf is called with an error. +func NoUsageOnError() Option { + return func(k *Kong) error { + k.noUsageOnError = true + return nil + } +} + // ClearResolvers clears all existing resolvers. func ClearResolvers() Option { return func(k *Kong) error { diff --git a/tag.go b/tag.go index 594c75a..d07fd2f 100644 --- a/tag.go +++ b/tag.go @@ -23,6 +23,7 @@ type Tag struct { Short rune Hidden bool Sep rune + Enum map[string]bool // Storage for all tag keys for arbitrary lookups. items map[string]string @@ -112,6 +113,7 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag { s, chars := getTagInfo(ft) t := &Tag{ items: parseTagItems(s, chars), + Enum: map[string]bool{}, } t.Cmd = t.Has("cmd") t.Arg = t.Has("arg") @@ -141,6 +143,9 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag { if t.PlaceHolder == "" { t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name())) } + for _, part := range strings.Split(t.Get("enum"), ",") { + t.Enum[part] = true + } return t }