Start making help slightly configurable.

This commit is contained in:
Alec Thomas
2018-06-20 21:51:56 +10:00
parent 3a2f3eebdd
commit 653531d6bc
9 changed files with 173 additions and 77 deletions
+10 -16
View File
@@ -1,15 +1,10 @@
package main package main
import ( import (
"encoding/json"
"fmt"
"os"
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
) )
// nolint: govet var cli struct {
var CLI struct {
Debug bool `help:"Debug mode."` Debug bool `help:"Debug mode."`
Rm struct { Rm struct {
@@ -17,19 +12,18 @@ 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"`
} `cmd help:"Remove files."` } `cmd:"" help:"Remove files."`
Ls struct { Ls struct {
Paths []string `arg optional help:"Paths to list." type:"path"` Paths []string `arg:"" optional:"" help:"Paths to list." type:"path"`
} `cmd help:"List paths."` } `cmd:"" help:"List paths."`
} }
func main() { func main() {
app := kong.Must(&CLI, kong.Description("A shell-like example app.")) cmd := kong.Parse(&cli, kong.Description("A shell-like example app."), kong.HelpOptions(kong.CompactHelp()))
cmd, err := app.Parse(os.Args[1:]) switch cmd {
app.FatalIfErrorf(err) case "rm":
s, _ := json.Marshal(&CLI) case "ls":
fmt.Println(cmd) }
fmt.Println(string(s))
} }
Executable → Regular
View File
+57 -23
View File
@@ -9,28 +9,49 @@ import (
) )
const ( const (
defaultIndent = 2 defaultIndent = 2
defaultColumnPadding = 4
) )
// PrintHelp is the default help printer. // HelpOption configures the default help.
func PrintHelp(ctx *Context) error { type HelpOption func(options *helpWriterOptions)
w := newHelpWriter(guessWidth(ctx.App.Stdout))
selected := ctx.Selected() // CompactHelp writes help in a more compact form.
if selected == nil { func CompactHelp() HelpOption {
printApp(w, ctx.App.Model) return func(options *helpWriterOptions) {
} else { options.compact = true
printCommand(w, ctx.App.Model, selected) }
}
// 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) { func printApp(w *helpWriter, app *Application) {
w.Printf("usage: %s", app.Summary()) w.Printf("Usage: %s", app.Summary())
printNodeDetail(w, &app.Node) printNodeDetail(w, &app.Node)
cmds := app.Leaves()
if len(cmds) > 0 {
w.Print("")
w.Printf(`Run "%s <command> --help" for more information on a command.`, app.Name)
}
} }
func printCommand(w *helpWriter, app *Application, cmd *Command) { 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) printNodeDetail(w, cmd)
} }
@@ -54,10 +75,18 @@ func printNodeDetail(w *helpWriter, node *Node) {
w.Print("") w.Print("")
w.Print("Commands:") w.Print("Commands:")
iw := w.Indent() iw := w.Indent()
for i, cmd := range cmds { if w.options.compact {
printCommandSummary(iw, cmd) rows := [][2]string{}
if i != len(cmds)-1 { for _, cmd := range cmds {
iw.Print("") 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 { type helpWriter struct {
indent string indent string
width int width int
lines *[]string lines *[]string
options helpWriterOptions
}
type helpWriterOptions struct {
compact bool
} }
func newHelpWriter(width int) *helpWriter { func newHelpWriter(width int) *helpWriter {
@@ -94,7 +128,7 @@ func (h *helpWriter) Print(text string) {
} }
func (h *helpWriter) Indent() *helpWriter { 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 { func (h *helpWriter) String() string {
@@ -113,7 +147,7 @@ func (h *helpWriter) Write(w io.Writer) error {
func (h *helpWriter) Wrap(text string) { func (h *helpWriter) Wrap(text string) {
w := bytes.NewBuffer(nil) 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") { for _, line := range strings.Split(strings.TrimSpace(w.String()), "\n") {
h.Print(line) h.Print(line)
} }
@@ -124,7 +158,7 @@ func writePositionals(w *helpWriter, args []*Positional) {
for _, arg := range args { for _, arg := range args {
rows = append(rows, [2]string{arg.Summary(), arg.Help}) rows = append(rows, [2]string{arg.Summary(), arg.Help})
} }
writeTwoColumns(w, 2, rows) writeTwoColumns(w, defaultColumnPadding, rows)
} }
func writeFlags(w *helpWriter, groups [][]*Flag) { 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) { func writeTwoColumns(w *helpWriter, padding int, rows [][2]string) {
+18 -16
View File
@@ -53,18 +53,18 @@ func TestHelp(t *testing.T) {
}) })
require.True(t, exited) require.True(t, exited)
t.Log(w.String()) t.Log(w.String())
require.Equal(t, `usage: test-app --required <command> require.Equal(t, `Usage: test-app --required <command>
A test app. A test app.
Flags: Flags:
--help Show context-sensitive help. --help Show context-sensitive help.
--string=STRING A string flag. --string=STRING A string flag.
--bool A bool flag with very long help that wraps a lot and is --bool A bool flag with very long help that wraps a lot and is
verbose and is really verbose. verbose and is really verbose.
--slice=STR,... A slice of strings. --slice=STR,... A slice of strings.
--map=KEY=VALUE A map of strings to ints. --map=KEY=VALUE A map of strings to ints.
--required A required flag. --required A required flag.
Commands: Commands:
one --required one --required
@@ -75,6 +75,8 @@ Commands:
two four --required --required-two two four --required --required-two
Sub-sub-command. Sub-sub-command.
Run "test-app <command> --help" for more information on a command.
`, w.String()) `, w.String())
}) })
@@ -87,19 +89,19 @@ Commands:
}) })
require.True(t, exited) require.True(t, exited)
t.Log(w.String()) t.Log(w.String())
require.Equal(t, `usage: test-app two <three> --required --required-two --required-three require.Equal(t, `Usage: test-app two <three> --required --required-two --required-three
Sub-sub-arg. Sub-sub-arg.
Flags: Flags:
--string=STRING A string flag. --string=STRING A string flag.
--bool A bool flag with very long help that wraps a lot and is --bool A bool flag with very long help that wraps a lot and is
verbose and is really verbose. verbose and is really verbose.
--slice=STR,... A slice of strings. --slice=STR,... A slice of strings.
--map=KEY=VALUE A map of strings to ints. --map=KEY=VALUE A map of strings to ints.
--required A required flag. --required A required flag.
--flag=STRING Nested flag under two. --flag=STRING Nested flag under two.
--required-two --required-two
--required-three --required-three
+14 -5
View File
@@ -43,6 +43,7 @@ type Kong struct {
registry *Registry registry *Registry
noDefaultHelp bool noDefaultHelp bool
help func(*Context) error 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
@@ -58,12 +59,17 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
Stderr: os.Stderr, Stderr: os.Stderr,
before: map[reflect.Value]HookFunc{}, before: map[reflect.Value]HookFunc{},
registry: NewRegistry().RegisterDefaults(), registry: NewRegistry().RegisterDefaults(),
help: PrintHelp,
resolvers: []ResolverFunc{Envars()}, resolvers: []ResolverFunc{Envars()},
} }
for _, option := range options { 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) model, err := build(k, grammar)
@@ -74,8 +80,11 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
k.Model = model k.Model = model
for _, option := range k.postBuildOptions { for _, option := range k.postBuildOptions {
option(k) if err := option(k); err != nil {
return nil, err
}
} }
k.postBuildOptions = nil
return k, nil return k, nil
} }
@@ -98,14 +107,14 @@ func (k *Kong) extraFlags() []*Flag {
} }
helpFlag.Flag = helpFlag helpFlag.Flag = helpFlag
hook := Hook(&helpValue, func(ctx *Context, path *Path) error { hook := Hook(&helpValue, func(ctx *Context, path *Path) error {
err := PrintHelp(ctx) err := k.help(ctx)
if err != nil { if err != nil {
return err return err
} }
k.Exit(1) k.Exit(1)
return nil return nil
}) })
hook(k) _ = hook(k)
return []*Flag{helpFlag} return []*Flag{helpFlag}
} }
+23
View File
@@ -43,6 +43,29 @@ type Node struct {
Argument *Value // Populated when Type is ArgumentNode. 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. // AllFlags returns flags from all ancestor branches encountered.
func (n *Node) AllFlags() (out [][]*Flag) { func (n *Node) AllFlags() (out [][]*Flag) {
if n.Parent != nil { if n.Parent != nil {
Executable → Regular
+51 -17
View File
@@ -10,63 +10,84 @@ import (
) )
// An Option applies optional changes to the Kong application. // 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. // Exit overrides the function used to terminate. This is useful for testing or interactive use.
func Exit(exit func(int)) Option { 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. // NoDefaultHelp disables the default help flags.
func NoDefaultHelp() Option { func NoDefaultHelp() Option {
return func(k *Kong) { return func(k *Kong) error {
k.noDefaultHelp = true k.noDefaultHelp = true
return nil
} }
} }
// Name overrides the application name. // Name overrides the application name.
func Name(name string) Option { func Name(name string) Option {
return func(k *Kong) { return func(k *Kong) error {
k.postBuildOptions = append(k.postBuildOptions, func(k *Kong) { k.postBuildOptions = append(k.postBuildOptions, func(k *Kong) error {
k.Model.Name = name k.Model.Name = name
return nil
}) })
return nil
} }
} }
// Description sets the application description. // Description sets the application description.
func Description(description string) Option { func Description(description string) Option {
return func(k *Kong) { return func(k *Kong) error {
k.postBuildOptions = append(k.postBuildOptions, func(k *Kong) { k.postBuildOptions = append(k.postBuildOptions, func(k *Kong) error {
k.Model.Help = description k.Model.Help = description
return nil
}) })
return nil
} }
} }
// TypeMapper registers a mapper to a type. // TypeMapper registers a mapper to a type.
func TypeMapper(typ reflect.Type, mapper Mapper) Option { 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. // KindMapper registers a mapper to a kind.
func KindMapper(kind reflect.Kind, mapper Mapper) Option { 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. // ValueMapper registers a mapper to a field value.
func ValueMapper(ptr interface{}, mapper Mapper) Option { 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. // NamedMapper registers a mapper to a name.
func NamedMapper(name string, mapper Mapper) Option { 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. // Writers overrides the default writers. Useful for testing or interactive use.
func Writers(stdout, stderr io.Writer) Option { func Writers(stdout, stderr io.Writer) Option {
return func(k *Kong) { return func(k *Kong) error {
k.Stdout = stdout k.Stdout = stdout
k.Stderr = stderr k.Stderr = stderr
return nil
} }
} }
@@ -84,8 +105,9 @@ func Hook(ptr interface{}, hook HookFunc) Option {
if key.Kind() != reflect.Ptr { if key.Kind() != reflect.Ptr {
panic("expected a pointer") panic("expected a pointer")
} }
return func(k *Kong) { return func(k *Kong) error {
k.before[key] = hook k.before[key] = hook
return nil
} }
} }
@@ -96,22 +118,33 @@ type HelpFunction func(*Context) error
// //
// Defaults to PrintHelp. // Defaults to PrintHelp.
func Help(help HelpFunction) Option { func Help(help HelpFunction) Option {
return func(k *Kong) { return func(k *Kong) error {
k.help = help 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. // ClearResolvers clears all existing resolvers.
func ClearResolvers() Option { func ClearResolvers() Option {
return func(k *Kong) { return func(k *Kong) error {
k.resolvers = nil k.resolvers = nil
return nil
} }
} }
// Resolver registers flag resolvers. // Resolver registers flag resolvers.
func Resolver(resolvers ...ResolverFunc) Option { func Resolver(resolvers ...ResolverFunc) Option {
return func(k *Kong) { return func(k *Kong) error {
k.resolvers = append(k.resolvers, resolvers...) 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. // ~ expansion will occur on the provided paths.
func Configuration(loader ConfigurationFunc, paths ...string) Option { func Configuration(loader ConfigurationFunc, paths ...string) Option {
return func(k *Kong) { return func(k *Kong) error {
for _, path := range paths { for _, path := range paths {
path = expandPath(path) path = expandPath(path)
r, err := os.Open(path) // nolint: gas r, err := os.Open(path) // nolint: gas
@@ -139,6 +172,7 @@ func Configuration(loader ConfigurationFunc, paths ...string) Option {
} }
_ = r.Close() _ = r.Close()
} }
return nil
} }
} }
Executable → Regular
View File
Executable → Regular
View File