Start making help slightly configurable.
This commit is contained in:
+10
-16
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
Executable → Regular
Reference in New Issue
Block a user