From 653531d6bca5831eca6b82290c7e07e3337b1bf8 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Wed, 20 Jun 2018 21:51:56 +1000 Subject: [PATCH] Start making help slightly configurable. --- _examples/shell/main.go | 26 ++++++-------- context.go | 0 help.go | 80 +++++++++++++++++++++++++++++------------ help_test.go | 34 +++++++++--------- kong.go | 19 +++++++--- model.go | 23 ++++++++++++ options.go | 68 ++++++++++++++++++++++++++--------- resolver.go | 0 resolver_test.go | 0 9 files changed, 173 insertions(+), 77 deletions(-) mode change 100755 => 100644 context.go mode change 100755 => 100644 options.go mode change 100755 => 100644 resolver.go mode change 100755 => 100644 resolver_test.go diff --git a/_examples/shell/main.go b/_examples/shell/main.go index f760d49..a479d4c 100644 --- a/_examples/shell/main.go +++ b/_examples/shell/main.go @@ -1,15 +1,10 @@ package main import ( - "encoding/json" - "fmt" - "os" - "github.com/alecthomas/kong" ) -// nolint: govet -var CLI struct { +var cli struct { Debug bool `help:"Debug mode."` Rm struct { @@ -17,19 +12,18 @@ 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"` - } `cmd help:"Remove files."` + Paths []string `arg:"" help:"Paths to remove." type:"path"` + } `cmd:"" help:"Remove files."` Ls struct { - Paths []string `arg optional help:"Paths to list." type:"path"` - } `cmd help:"List paths."` + Paths []string `arg:"" optional:"" help:"Paths to list." type:"path"` + } `cmd:"" help:"List paths."` } func main() { - app := kong.Must(&CLI, kong.Description("A shell-like example app.")) - cmd, err := app.Parse(os.Args[1:]) - app.FatalIfErrorf(err) - s, _ := json.Marshal(&CLI) - fmt.Println(cmd) - fmt.Println(string(s)) + cmd := kong.Parse(&cli, kong.Description("A shell-like example app."), kong.HelpOptions(kong.CompactHelp())) + switch cmd { + case "rm": + case "ls": + } } diff --git a/context.go b/context.go old mode 100755 new mode 100644 diff --git a/help.go b/help.go index 8fe9cc7..5c0ad94 100644 --- a/help.go +++ b/help.go @@ -9,28 +9,49 @@ import ( ) const ( - defaultIndent = 2 + defaultIndent = 2 + defaultColumnPadding = 4 ) -// PrintHelp is the default help printer. -func PrintHelp(ctx *Context) error { - w := newHelpWriter(guessWidth(ctx.App.Stdout)) - selected := ctx.Selected() - if selected == nil { - printApp(w, ctx.App.Model) - } else { - printCommand(w, ctx.App.Model, selected) +// HelpOption configures the default help. +type HelpOption func(options *helpWriterOptions) + +// CompactHelp writes help in a more compact form. +func CompactHelp() HelpOption { + return func(options *helpWriterOptions) { + options.compact = true + } +} + +// HelpPrinter returns a HelpFunction configured with the given HelpOptions. +func HelpPrinter(options ...HelpOption) HelpFunction { + return func(ctx *Context) error { + w := newHelpWriter(guessWidth(ctx.App.Stdout)) + for _, option := range options { + option(&w.options) + } + selected := ctx.Selected() + if selected == nil { + printApp(w, ctx.App.Model) + } else { + printCommand(w, ctx.App.Model, selected) + } + return w.Write(ctx.App.Stdout) } - return w.Write(ctx.App.Stdout) } func printApp(w *helpWriter, app *Application) { - w.Printf("usage: %s", app.Summary()) + w.Printf("Usage: %s", app.Summary()) printNodeDetail(w, &app.Node) + cmds := app.Leaves() + if len(cmds) > 0 { + w.Print("") + w.Printf(`Run "%s --help" for more information on a command.`, app.Name) + } } func printCommand(w *helpWriter, app *Application, cmd *Command) { - w.Printf("usage: %s %s", app.Name, cmd.Summary()) + w.Printf("Usage: %s %s", app.Name, cmd.Summary()) printNodeDetail(w, cmd) } @@ -54,10 +75,18 @@ func printNodeDetail(w *helpWriter, node *Node) { w.Print("") w.Print("Commands:") iw := w.Indent() - for i, cmd := range cmds { - printCommandSummary(iw, cmd) - if i != len(cmds)-1 { - iw.Print("") + if w.options.compact { + rows := [][2]string{} + for _, cmd := range cmds { + rows = append(rows, [2]string{cmd.Name, cmd.Help}) + } + writeTwoColumns(iw, defaultColumnPadding, rows) + } else { + for i, cmd := range cmds { + printCommandSummary(iw, cmd) + if i != len(cmds)-1 { + iw.Print("") + } } } } @@ -71,9 +100,14 @@ func printCommandSummary(w *helpWriter, cmd *Command) { } type helpWriter struct { - indent string - width int - lines *[]string + indent string + width int + lines *[]string + options helpWriterOptions +} + +type helpWriterOptions struct { + compact bool } func newHelpWriter(width int) *helpWriter { @@ -94,7 +128,7 @@ func (h *helpWriter) Print(text string) { } func (h *helpWriter) Indent() *helpWriter { - return &helpWriter{indent: h.indent + " ", lines: h.lines, width: h.width - 2} + return &helpWriter{indent: h.indent + " ", lines: h.lines, width: h.width - 2, options: h.options} } func (h *helpWriter) String() string { @@ -113,7 +147,7 @@ func (h *helpWriter) Write(w io.Writer) error { func (h *helpWriter) Wrap(text string) { w := bytes.NewBuffer(nil) - doc.ToText(w, strings.TrimSpace(text), "", "", h.width) + doc.ToText(w, strings.TrimSpace(text), "", " ", h.width) for _, line := range strings.Split(strings.TrimSpace(w.String()), "\n") { h.Print(line) } @@ -124,7 +158,7 @@ func writePositionals(w *helpWriter, args []*Positional) { for _, arg := range args { rows = append(rows, [2]string{arg.Summary(), arg.Help}) } - writeTwoColumns(w, 2, rows) + writeTwoColumns(w, defaultColumnPadding, rows) } func writeFlags(w *helpWriter, groups [][]*Flag) { @@ -148,7 +182,7 @@ func writeFlags(w *helpWriter, groups [][]*Flag) { } } } - writeTwoColumns(w, 2, rows) + writeTwoColumns(w, defaultColumnPadding, rows) } func writeTwoColumns(w *helpWriter, padding int, rows [][2]string) { diff --git a/help_test.go b/help_test.go index 9aa6004..762a95b 100644 --- a/help_test.go +++ b/help_test.go @@ -53,18 +53,18 @@ func TestHelp(t *testing.T) { }) require.True(t, exited) t.Log(w.String()) - require.Equal(t, `usage: test-app --required + require.Equal(t, `Usage: test-app --required A test app. Flags: - --help Show context-sensitive help. - --string=STRING A string flag. - --bool A bool flag with very long help that wraps a lot and is - verbose and is really verbose. - --slice=STR,... A slice of strings. - --map=KEY=VALUE A map of strings to ints. - --required A required flag. + --help Show context-sensitive help. + --string=STRING A string flag. + --bool A bool flag with very long help that wraps a lot and is + verbose and is really verbose. + --slice=STR,... A slice of strings. + --map=KEY=VALUE A map of strings to ints. + --required A required flag. Commands: one --required @@ -75,6 +75,8 @@ Commands: two four --required --required-two Sub-sub-command. + +Run "test-app --help" for more information on a command. `, w.String()) }) @@ -87,19 +89,19 @@ Commands: }) require.True(t, exited) t.Log(w.String()) - require.Equal(t, `usage: test-app two --required --required-two --required-three + require.Equal(t, `Usage: test-app two --required --required-two --required-three Sub-sub-arg. Flags: - --string=STRING A string flag. - --bool A bool flag with very long help that wraps a lot and is - verbose and is really verbose. - --slice=STR,... A slice of strings. - --map=KEY=VALUE A map of strings to ints. - --required A required flag. + --string=STRING A string flag. + --bool A bool flag with very long help that wraps a lot and is + verbose and is really verbose. + --slice=STR,... A slice of strings. + --map=KEY=VALUE A map of strings to ints. + --required A required flag. - --flag=STRING Nested flag under two. + --flag=STRING Nested flag under two. --required-two --required-three diff --git a/kong.go b/kong.go index ec10dfe..a0d750e 100644 --- a/kong.go +++ b/kong.go @@ -43,6 +43,7 @@ type Kong struct { registry *Registry noDefaultHelp bool help func(*Context) error + helpOptions []HelpOption // Set temporarily by Options. These are applied after build(). postBuildOptions []Option @@ -58,12 +59,17 @@ func New(grammar interface{}, options ...Option) (*Kong, error) { Stderr: os.Stderr, before: map[reflect.Value]HookFunc{}, registry: NewRegistry().RegisterDefaults(), - help: PrintHelp, resolvers: []ResolverFunc{Envars()}, } for _, option := range options { - option(k) + if err := option(k); err != nil { + return nil, err + } + } + + if k.help == nil { + k.help = HelpPrinter(k.helpOptions...) } model, err := build(k, grammar) @@ -74,8 +80,11 @@ func New(grammar interface{}, options ...Option) (*Kong, error) { k.Model = model for _, option := range k.postBuildOptions { - option(k) + if err := option(k); err != nil { + return nil, err + } } + k.postBuildOptions = nil return k, nil } @@ -98,14 +107,14 @@ func (k *Kong) extraFlags() []*Flag { } helpFlag.Flag = helpFlag hook := Hook(&helpValue, func(ctx *Context, path *Path) error { - err := PrintHelp(ctx) + err := k.help(ctx) if err != nil { return err } k.Exit(1) return nil }) - hook(k) + _ = hook(k) return []*Flag{helpFlag} } diff --git a/model.go b/model.go index cdf5e50..4da7e19 100644 --- a/model.go +++ b/model.go @@ -43,6 +43,29 @@ type Node struct { Argument *Value // Populated when Type is ArgumentNode. } +// Find a command/argument/flag by pointer to its field. +// +// Returns nil if not found. Panics if ptr is not a pointer. +func (n *Node) Find(ptr interface{}) *Node { + key := reflect.ValueOf(ptr) + if key.Kind() != reflect.Ptr { + panic("expected a pointer") + } + return n.findNode(key) +} + +func (n *Node) findNode(key reflect.Value) *Node { + if n.Target == key { + return n + } + for _, child := range n.Children { + if found := child.findNode(key); found != nil { + return found + } + } + return nil +} + // AllFlags returns flags from all ancestor branches encountered. func (n *Node) AllFlags() (out [][]*Flag) { if n.Parent != nil { diff --git a/options.go b/options.go old mode 100755 new mode 100644 index 93d00e9..eeb9468 --- a/options.go +++ b/options.go @@ -10,63 +10,84 @@ import ( ) // An Option applies optional changes to the Kong application. -type Option func(k *Kong) +type Option func(k *Kong) error // 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 } + return func(k *Kong) error { + k.Exit = exit + return nil + } } // NoDefaultHelp disables the default help flags. func NoDefaultHelp() Option { - return func(k *Kong) { + return func(k *Kong) error { k.noDefaultHelp = true + return nil } } // Name overrides the application name. func Name(name string) Option { - return func(k *Kong) { - k.postBuildOptions = append(k.postBuildOptions, func(k *Kong) { + return func(k *Kong) error { + k.postBuildOptions = append(k.postBuildOptions, func(k *Kong) error { k.Model.Name = name + return nil }) + return nil } } // Description sets the application description. func Description(description string) Option { - return func(k *Kong) { - k.postBuildOptions = append(k.postBuildOptions, func(k *Kong) { + return func(k *Kong) error { + k.postBuildOptions = append(k.postBuildOptions, func(k *Kong) error { k.Model.Help = description + return nil }) + return nil } } // TypeMapper registers a mapper to a type. func TypeMapper(typ reflect.Type, mapper Mapper) Option { - return func(k *Kong) { k.registry.RegisterType(typ, mapper) } + return func(k *Kong) error { + k.registry.RegisterType(typ, mapper) + return nil + } } // KindMapper registers a mapper to a kind. func KindMapper(kind reflect.Kind, mapper Mapper) Option { - return func(k *Kong) { k.registry.RegisterKind(kind, mapper) } + return func(k *Kong) error { + k.registry.RegisterKind(kind, mapper) + return nil + } } // ValueMapper registers a mapper to a field value. func ValueMapper(ptr interface{}, mapper Mapper) Option { - return func(k *Kong) { k.registry.RegisterValue(ptr, mapper) } + return func(k *Kong) error { + k.registry.RegisterValue(ptr, mapper) + return nil + } } // NamedMapper registers a mapper to a name. func NamedMapper(name string, mapper Mapper) Option { - return func(k *Kong) { k.registry.RegisterName(name, mapper) } + return func(k *Kong) error { + k.registry.RegisterName(name, mapper) + return nil + } } // Writers overrides the default writers. Useful for testing or interactive use. func Writers(stdout, stderr io.Writer) Option { - return func(k *Kong) { + return func(k *Kong) error { k.Stdout = stdout k.Stderr = stderr + return nil } } @@ -84,8 +105,9 @@ func Hook(ptr interface{}, hook HookFunc) Option { if key.Kind() != reflect.Ptr { panic("expected a pointer") } - return func(k *Kong) { + return func(k *Kong) error { k.before[key] = hook + return nil } } @@ -96,22 +118,33 @@ type HelpFunction func(*Context) error // // Defaults to PrintHelp. func Help(help HelpFunction) Option { - return func(k *Kong) { + return func(k *Kong) error { k.help = help + return nil + } +} + +// HelpOptions specifies options for the default help printer, if used. +func HelpOptions(options ...HelpOption) Option { + return func(k *Kong) error { + k.helpOptions = options + return nil } } // ClearResolvers clears all existing resolvers. func ClearResolvers() Option { - return func(k *Kong) { + return func(k *Kong) error { k.resolvers = nil + return nil } } // Resolver registers flag resolvers. func Resolver(resolvers ...ResolverFunc) Option { - return func(k *Kong) { + return func(k *Kong) error { k.resolvers = append(k.resolvers, resolvers...) + return nil } } @@ -126,7 +159,7 @@ type ConfigurationFunc func(r io.Reader) (ResolverFunc, error) // // ~ expansion will occur on the provided paths. func Configuration(loader ConfigurationFunc, paths ...string) Option { - return func(k *Kong) { + return func(k *Kong) error { for _, path := range paths { path = expandPath(path) r, err := os.Open(path) // nolint: gas @@ -139,6 +172,7 @@ func Configuration(loader ConfigurationFunc, paths ...string) Option { } _ = r.Close() } + return nil } } diff --git a/resolver.go b/resolver.go old mode 100755 new mode 100644 diff --git a/resolver_test.go b/resolver_test.go old mode 100755 new mode 100644