Display usage information on error.

This commit is contained in:
Alec Thomas
2018-06-21 10:08:01 +10:00
parent d9c0dd25b1
commit cdcdf49f67
7 changed files with 53 additions and 16 deletions
+2 -2
View File
@@ -19,7 +19,7 @@
1. [`Configuration(loader, paths...)` - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files) 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. [`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. [`*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. [`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) 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)`. 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 ### `HelpOptions(...HelpOption)` and `Help(HelpFunc)` - customising help
The default help output is usually sufficient, but if it's not, there are two solutions. The default help output is usually sufficient, but if it's not, there are two solutions.
+1 -1
View File
@@ -14,7 +14,7 @@ var cli struct {
Force bool `help:"Force removal." short:"f"` Force bool `help:"Force removal." short:"f"`
Recursive bool `help:"Recursively remove files." short:"r"` 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."` } `cmd:"" help:"Remove files."`
Ls struct { Ls struct {
+7 -4
View File
@@ -43,11 +43,11 @@ func (p *Path) Node() *Node {
// Context contains the current parse context. // Context contains the current parse context.
type Context struct { type Context struct {
App *Kong App *Kong
Path []*Path // A trace through parsed nodes. Path []*Path // A trace through parsed nodes.
Error error // Error that occurred during trace, if any. Args []string // Original command-line arguments.
Error error // Error that occurred during trace, if any.
values map[*Value]reflect.Value // Temporary values during tracing. values map[*Value]reflect.Value // Temporary values during tracing.
args []string
scan *Scanner scan *Scanner
} }
@@ -86,7 +86,7 @@ func (c *Context) Selected() *Node {
func Trace(k *Kong, args []string) (*Context, error) { func Trace(k *Kong, args []string) (*Context, error) {
c := &Context{ c := &Context{
App: k, App: k,
args: args, Args: args,
Path: []*Path{ Path: []*Path{
{App: k.Model, Flags: k.Model.Flags}, {App: k.Model, Flags: k.Model.Flags},
}, },
@@ -461,6 +461,9 @@ func checkMissingChildren(node *Node) error {
if len(missing) == 1 { if len(missing) == 1 {
return fmt.Errorf("%q should be followed by %s", node.Path(), missing[0]) 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, ", ")) return fmt.Errorf("%q should be followed by one of %s", node.Path(), strings.Join(missing, ", "))
} }
+12
View File
@@ -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 }
+18 -9
View File
@@ -38,12 +38,13 @@ type Kong struct {
Stdout io.Writer Stdout io.Writer
Stderr io.Writer Stderr io.Writer
before map[reflect.Value]HookFunc before map[reflect.Value]HookFunc
resolvers []ResolverFunc resolvers []ResolverFunc
registry *Registry registry *Registry
noDefaultHelp bool noDefaultHelp bool
help func(*Context) error noUsageOnError bool
helpOptions []HelpOption help func(*Context) error
helpOptions []HelpOption
// Set temporarily by Options. These are applied after build(). // Set temporarily by Options. These are applied after build().
postBuildOptions []Option 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 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 "<argument>". // the command name while positional arguments are the argument name surrounded by "<argument>".
//
// 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) { func (k *Kong) Parse(args []string) (command string, err error) {
defer catch(&err) defer catch(&err)
ctx, err := Trace(k, args) ctx, err := Trace(k, args)
@@ -140,13 +144,13 @@ func (k *Kong) Parse(args []string) (command string, err error) {
return "", err return "", err
} }
if err = k.applyHooks(ctx); err != nil { if err = k.applyHooks(ctx); err != nil {
return "", err return "", &ParseError{error: err, Context: ctx}
} }
if ctx.Error != nil { if ctx.Error != nil {
return "", ctx.Error return "", &ParseError{error: ctx.Error, Context: ctx}
} }
if err = ctx.Validate(); err != nil { if err = ctx.Validate(); err != nil {
return "", err return "", &ParseError{error: err, Context: ctx}
} }
return ctx.Apply() 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() msg = fmt.Sprintf(args[0].(string), args[1:]...) + ": " + err.Error()
} }
k.Errorf("%s", msg) 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) k.Exit(1)
} }
+8
View File
@@ -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. // ClearResolvers clears all existing resolvers.
func ClearResolvers() Option { func ClearResolvers() Option {
return func(k *Kong) error { return func(k *Kong) error {
+5
View File
@@ -23,6 +23,7 @@ type Tag struct {
Short rune Short rune
Hidden bool Hidden bool
Sep rune Sep rune
Enum map[string]bool
// Storage for all tag keys for arbitrary lookups. // Storage for all tag keys for arbitrary lookups.
items map[string]string items map[string]string
@@ -112,6 +113,7 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
s, chars := getTagInfo(ft) s, chars := getTagInfo(ft)
t := &Tag{ t := &Tag{
items: parseTagItems(s, chars), items: parseTagItems(s, chars),
Enum: map[string]bool{},
} }
t.Cmd = t.Has("cmd") t.Cmd = t.Has("cmd")
t.Arg = t.Has("arg") t.Arg = t.Has("arg")
@@ -141,6 +143,9 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
if t.PlaceHolder == "" { if t.PlaceHolder == "" {
t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name())) t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name()))
} }
for _, part := range strings.Split(t.Get("enum"), ",") {
t.Enum[part] = true
}
return t return t
} }