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
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":
}
}
Executable → Regular
View File
+57 -23
View File
@@ -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 <command> --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) {
+18 -16
View File
@@ -53,18 +53,18 @@ func TestHelp(t *testing.T) {
})
require.True(t, exited)
t.Log(w.String())
require.Equal(t, `usage: test-app --required <command>
require.Equal(t, `Usage: test-app --required <command>
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 <command> --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 <three> --required --required-two --required-three
require.Equal(t, `Usage: test-app two <three> --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
+14 -5
View File
@@ -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}
}
+23
View File
@@ -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 {
Executable → Regular
+51 -17
View File
@@ -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
}
}
Executable → Regular
View File
Executable → Regular
View File