Help!
This commit is contained in:
committed by
Gerald Kaszuba
parent
48af58cefa
commit
2afd4ba47b
+14
-4
@@ -2,7 +2,6 @@ package kong
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -31,13 +30,24 @@ type Context struct {
|
|||||||
Path []*Path // A trace through parsed nodes.
|
Path []*Path // A trace through parsed nodes.
|
||||||
Error error // Error that occurred during trace, if any.
|
Error error // Error that occurred during trace, if any.
|
||||||
|
|
||||||
Stdout io.Writer
|
|
||||||
Stderr io.Writer
|
|
||||||
|
|
||||||
args []string
|
args []string
|
||||||
scan *Scanner
|
scan *Scanner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Selected command or argument.
|
||||||
|
func (c *Context) Selected() *Node {
|
||||||
|
var selected *Node
|
||||||
|
for _, path := range c.Path {
|
||||||
|
switch {
|
||||||
|
case path.Command != nil:
|
||||||
|
selected = path.Command
|
||||||
|
case path.Argument != nil:
|
||||||
|
selected = path.Argument
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
|
||||||
// Trace path of "args" through the gammar tree.
|
// Trace path of "args" through the gammar tree.
|
||||||
//
|
//
|
||||||
// The returned Context will include a Path of all commands, arguments, positionals and flags.
|
// The returned Context will include a Path of all commands, arguments, positionals and flags.
|
||||||
|
|||||||
@@ -7,130 +7,154 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/aymerick/raymond"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultIndent = 2
|
defaultIndent = 2
|
||||||
defaultTemplate = `
|
|
||||||
{{#with App}}
|
|
||||||
usage: {{Name}}
|
|
||||||
|
|
||||||
{{#wrap}}
|
|
||||||
{{Help}}
|
|
||||||
{{/wrap}}
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
{{#indent}}
|
|
||||||
{{formatFlags Flags}}
|
|
||||||
{{/indent}}
|
|
||||||
|
|
||||||
{{#if Children}}
|
|
||||||
{{#indent}}
|
|
||||||
{{#each Children}}
|
|
||||||
{{Name}}
|
|
||||||
{{/each}}
|
|
||||||
{{/indent}}
|
|
||||||
{{/if}}
|
|
||||||
{{/with}}
|
|
||||||
`
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultHelpTemplate = raymond.MustParse(strings.TrimSpace(defaultTemplate))
|
func PrintHelp(ctx *Context) error {
|
||||||
|
w := newHelpWriter(guessWidth(ctx.App.Stdout))
|
||||||
func init() {
|
selected := ctx.Selected()
|
||||||
defaultHelpTemplate.RegisterHelpers(map[string]interface{}{
|
if selected == nil {
|
||||||
"indent": func(options *raymond.Options) string {
|
printApp(w, ctx.App.Application)
|
||||||
indent, ok := options.HashProp("depth").(int)
|
} else {
|
||||||
if !ok {
|
printCommand(w, ctx.App.Application, selected)
|
||||||
indent = 2
|
}
|
||||||
}
|
return w.Write(ctx.App.Stdout)
|
||||||
width := options.Data("width").(int)
|
|
||||||
frame := options.NewDataFrame()
|
|
||||||
frame.Set("width", width-indent)
|
|
||||||
indentStr := strings.Repeat(" ", indent)
|
|
||||||
lines := strings.Split(options.FnData(frame), "\n")
|
|
||||||
for i, line := range lines {
|
|
||||||
lines[i] = indentStr + line
|
|
||||||
}
|
|
||||||
return strings.Join(lines, "\n")
|
|
||||||
},
|
|
||||||
"formatFlags": func(flags []*Flag, options *raymond.Options) string {
|
|
||||||
rows := [][2]string{}
|
|
||||||
haveShort := false
|
|
||||||
for _, flag := range flags {
|
|
||||||
if flag.Short != 0 {
|
|
||||||
haveShort = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, flag := range flags {
|
|
||||||
if !flag.Hidden {
|
|
||||||
rows = append(rows, [2]string{formatFlag(haveShort, flag), flag.Help})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w := bytes.NewBuffer(nil)
|
|
||||||
formatTwoColumns(w, 0, 2, options.Data("width").(int), rows)
|
|
||||||
return w.String()
|
|
||||||
},
|
|
||||||
"wrap": func(options *raymond.Options) string {
|
|
||||||
w := bytes.NewBuffer(nil)
|
|
||||||
doc.ToText(w, options.Fn(), "", " ", options.Data("width").(int))
|
|
||||||
return w.String()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Help returns a Before hook that will display help and exit.
|
func printApp(w *helpWriter, app *Application) {
|
||||||
//
|
w.Printf("usage: %s", app.Summary())
|
||||||
// tmpl receives a context with several top-level values, in addition to those passed through tmplctx:
|
printNodeDetail(w, &app.Node)
|
||||||
// .Context which is of type *Context and .Path which is of type *Path.
|
}
|
||||||
func Help(tmpl *raymond.Template, tmplctx map[string]interface{}) Before {
|
|
||||||
return func(ctx *Context, path *Path) error {
|
func printCommand(w *helpWriter, app *Application, cmd *Command) {
|
||||||
merged := map[string]interface{}{
|
w.Printf("usage: %s %s", app.Name, cmd.Summary())
|
||||||
"App": ctx.App,
|
printNodeDetail(w, cmd)
|
||||||
"Context": ctx,
|
}
|
||||||
"Path": path,
|
|
||||||
|
func printNodeDetail(w *helpWriter, node *Node) {
|
||||||
|
if node.Help != "" {
|
||||||
|
w.Print("")
|
||||||
|
w.Wrap(node.Help)
|
||||||
|
}
|
||||||
|
if len(node.Flags) > 0 {
|
||||||
|
w.Printf("")
|
||||||
|
w.Printf("Flags:")
|
||||||
|
writeFlags(w.Indent(), node.Flags)
|
||||||
|
}
|
||||||
|
cmds := node.Leaves()
|
||||||
|
if len(cmds) > 0 {
|
||||||
|
w.Print("")
|
||||||
|
w.Print("Commands:")
|
||||||
|
iw := w.Indent()
|
||||||
|
for i, cmd := range cmds {
|
||||||
|
printCommandSummary(iw, cmd)
|
||||||
|
if i != len(cmds)-1 {
|
||||||
|
iw.Print("")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for k, v := range tmplctx {
|
}
|
||||||
merged[k] = v
|
}
|
||||||
}
|
|
||||||
frame := raymond.NewDataFrame()
|
func printCommandSummary(w *helpWriter, cmd *Command) {
|
||||||
frame.Set("width", guessWidth(ctx.App.Stdout))
|
w.Print(cmd.Summary())
|
||||||
output, err := tmpl.ExecWith(merged, frame)
|
if cmd.Help != "" {
|
||||||
|
w.Indent().Wrap(cmd.Help)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type helpWriter struct {
|
||||||
|
indent string
|
||||||
|
width int
|
||||||
|
lines *[]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHelpWriter(width int) *helpWriter {
|
||||||
|
lines := []string{}
|
||||||
|
return &helpWriter{
|
||||||
|
indent: "",
|
||||||
|
width: width,
|
||||||
|
lines: &lines,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *helpWriter) Printf(format string, args ...interface{}) {
|
||||||
|
h.Print(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *helpWriter) Print(text string) {
|
||||||
|
*h.lines = append(*h.lines, strings.TrimRight(h.indent+text, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *helpWriter) Indent() *helpWriter {
|
||||||
|
return &helpWriter{indent: h.indent + " ", lines: h.lines, width: h.width - 2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *helpWriter) String() string {
|
||||||
|
return strings.Join(*h.lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *helpWriter) Write(w io.Writer) error {
|
||||||
|
for _, line := range *h.lines {
|
||||||
|
_, err := io.WriteString(w, line+"\n")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
io.WriteString(ctx.App.Stdout, output)
|
}
|
||||||
ctx.App.Exit(0)
|
return nil
|
||||||
return nil
|
}
|
||||||
|
|
||||||
|
func (h *helpWriter) Wrap(text string) {
|
||||||
|
w := bytes.NewBuffer(nil)
|
||||||
|
doc.ToText(w, strings.TrimSpace(text), "", "", h.width)
|
||||||
|
for _, line := range strings.Split(strings.TrimSpace(w.String()), "\n") {
|
||||||
|
h.Print(line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatTwoColumns(w io.Writer, indent, padding, width int, rows [][2]string) {
|
func writeFlags(w *helpWriter, flags []*Flag) {
|
||||||
|
rows := [][2]string{}
|
||||||
|
haveShort := false
|
||||||
|
for _, flag := range flags {
|
||||||
|
if flag.Short != 0 {
|
||||||
|
haveShort = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, flag := range flags {
|
||||||
|
if !flag.Hidden {
|
||||||
|
rows = append(rows, [2]string{formatFlag(haveShort, flag), flag.Help})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeTwoColumns(w, 2, rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeTwoColumns(w *helpWriter, padding int, rows [][2]string) {
|
||||||
// Find size of first column.
|
// Find size of first column.
|
||||||
s := 0
|
leftSize := 0
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
if c := len(row[0]); c > s && c < 30 {
|
if c := len(row[0]); c > leftSize && c < 30 {
|
||||||
s = c
|
leftSize = c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
indentStr := strings.Repeat(" ", indent)
|
offsetStr := strings.Repeat(" ", leftSize+padding)
|
||||||
offsetStr := strings.Repeat(" ", s+padding)
|
|
||||||
|
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
doc.ToText(buf, row[1], "", strings.Repeat(" ", defaultIndent), width-s-padding-indent)
|
doc.ToText(buf, row[1], "", strings.Repeat(" ", defaultIndent), w.width-leftSize-padding)
|
||||||
lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
|
lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
|
||||||
fmt.Fprintf(w, "%s%-*s%*s", indentStr, s, row[0], padding, "")
|
|
||||||
if len(row[0]) >= 30 {
|
line := fmt.Sprintf("%-*s", leftSize, row[0])
|
||||||
fmt.Fprintf(w, "\n%s%s", indentStr, offsetStr)
|
if len(row[0]) < 30 {
|
||||||
|
line += fmt.Sprintf("%*s%s", padding, "", lines[0])
|
||||||
|
lines = lines[1:]
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "%s\n", lines[0])
|
w.Print(line)
|
||||||
for _, line := range lines[1:] {
|
for _, line := range lines {
|
||||||
fmt.Fprintf(w, "%s%s%s\n", indentStr, offsetStr, line)
|
w.Printf("%s%s", offsetStr, line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-5
@@ -2,7 +2,6 @@ package kong
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -10,11 +9,16 @@ import (
|
|||||||
|
|
||||||
func TestHelp(t *testing.T) {
|
func TestHelp(t *testing.T) {
|
||||||
var cli struct {
|
var cli struct {
|
||||||
String string `kong:"help='A string flag.'"`
|
String string `help:"A string flag."`
|
||||||
Bool bool `kong:"help='A bool flag.'"`
|
Bool bool `help:"A bool flag with very long help that wraps a lot and is verbose and is really verbose."`
|
||||||
|
|
||||||
One struct {
|
One struct {
|
||||||
} `kong:"cmd"`
|
Flag string `help:"Nested flag."`
|
||||||
|
} `cmd help:"A subcommand."`
|
||||||
|
|
||||||
|
Two struct {
|
||||||
|
Flag string `help:"Nested flag under two."`
|
||||||
|
} `cmd help:"Another subcommand."`
|
||||||
}
|
}
|
||||||
w := bytes.NewBuffer(nil)
|
w := bytes.NewBuffer(nil)
|
||||||
exited := false
|
exited := false
|
||||||
@@ -27,5 +31,34 @@ func TestHelp(t *testing.T) {
|
|||||||
_, err := app.Parse([]string{"--help"})
|
_, err := app.Parse([]string{"--help"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, exited)
|
require.True(t, exited)
|
||||||
fmt.Println(w.String())
|
require.Equal(t, `usage: test-app [<flags>]
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
one [<flags>]
|
||||||
|
A subcommand.
|
||||||
|
|
||||||
|
two [<flags>]
|
||||||
|
Another subcommand.
|
||||||
|
`, w.String())
|
||||||
|
|
||||||
|
exited = false
|
||||||
|
w.Truncate(0)
|
||||||
|
_, err = app.Parse([]string{"one", "--help"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, exited)
|
||||||
|
require.Equal(t, `usage: test-app one [<flags>]
|
||||||
|
|
||||||
|
A subcommand.
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--flag=STRING Nested flag.
|
||||||
|
`, w.String())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Before is a callback tied to a field of the grammar, called before a value is applied.
|
|
||||||
type Before func(ctx *Context, path *Path) error
|
|
||||||
|
|
||||||
// Error reported by Kong.
|
// Error reported by Kong.
|
||||||
type Error struct{ msg string }
|
type Error struct{ msg string }
|
||||||
|
|
||||||
@@ -39,9 +36,10 @@ type Kong struct {
|
|||||||
Stdout io.Writer
|
Stdout io.Writer
|
||||||
Stderr io.Writer
|
Stderr io.Writer
|
||||||
|
|
||||||
before map[reflect.Value]Before
|
before map[reflect.Value]HookFunction
|
||||||
registry *Registry
|
registry *Registry
|
||||||
noDefaultHelp bool
|
noDefaultHelp bool
|
||||||
|
help func(*Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Kong parser on grammar.
|
// New creates a new Kong parser on grammar.
|
||||||
@@ -52,8 +50,9 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
|
|||||||
Exit: os.Exit,
|
Exit: os.Exit,
|
||||||
Stdout: os.Stdout,
|
Stdout: os.Stdout,
|
||||||
Stderr: os.Stderr,
|
Stderr: os.Stderr,
|
||||||
before: map[reflect.Value]Before{},
|
before: map[reflect.Value]HookFunction{},
|
||||||
registry: NewRegistry().RegisterDefaults(),
|
registry: NewRegistry().RegisterDefaults(),
|
||||||
|
help: PrintHelp,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
@@ -89,7 +88,14 @@ func (k *Kong) extraFlags() []*Flag {
|
|||||||
Mapper: k.registry.ForValue(value),
|
Mapper: k.registry.ForValue(value),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
hook := Hook(&helpValue, Help(defaultHelpTemplate, nil))
|
hook := Hook(&helpValue, func(ctx *Context, path *Path) error {
|
||||||
|
err := PrintHelp(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
k.Exit(1)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
hook(k)
|
hook(k)
|
||||||
return []*Flag{helpFlag}
|
return []*Flag{helpFlag}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-3
@@ -9,9 +9,12 @@ import (
|
|||||||
|
|
||||||
func mustNew(t *testing.T, cli interface{}, options ...Option) *Kong {
|
func mustNew(t *testing.T, cli interface{}, options ...Option) *Kong {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
options = append([]Option{ExitFunction(func(int) {
|
options = append([]Option{
|
||||||
t.Fatalf("unexpected exit()")
|
ExitFunction(func(int) {
|
||||||
})}, options...)
|
t.Helper()
|
||||||
|
t.Fatalf("unexpected exit()")
|
||||||
|
}),
|
||||||
|
}, options...)
|
||||||
parser, err := New(cli, options...)
|
parser, err := New(cli, options...)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return parser
|
return parser
|
||||||
|
|||||||
@@ -12,23 +12,6 @@ type Application struct {
|
|||||||
HelpFlag *Flag
|
HelpFlag *Flag
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leaves returns the leaf commands/arguments in the command-line grammar.
|
|
||||||
func (a *Application) Leaves() (out []*Node) {
|
|
||||||
var walk func(n *Node)
|
|
||||||
walk = func(n *Node) {
|
|
||||||
if len(n.Children) == 0 && n.Type != ApplicationNode {
|
|
||||||
out = append(out, n)
|
|
||||||
}
|
|
||||||
for _, child := range n.Children {
|
|
||||||
if child.Type == CommandNode || child.Type == ArgumentNode {
|
|
||||||
walk(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
walk(&a.Node)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type Argument = Node
|
type Argument = Node
|
||||||
|
|
||||||
type Command = Node
|
type Command = Node
|
||||||
@@ -54,6 +37,25 @@ type Node struct {
|
|||||||
Argument *Value // Populated when Type is ArgumentNode.
|
Argument *Value // Populated when Type is ArgumentNode.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Leaves returns the leaf commands/arguments under Node.
|
||||||
|
func (n *Node) Leaves() (out []*Node) {
|
||||||
|
var walk func(n *Node)
|
||||||
|
walk = func(n *Node) {
|
||||||
|
if len(n.Children) == 0 && n.Type != ApplicationNode {
|
||||||
|
out = append(out, n)
|
||||||
|
}
|
||||||
|
for _, child := range n.Children {
|
||||||
|
if child.Type == CommandNode || child.Type == ArgumentNode {
|
||||||
|
walk(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, child := range n.Children {
|
||||||
|
walk(child)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Depth of the command from the application root.
|
// Depth of the command from the application root.
|
||||||
func (n *Node) Depth() int {
|
func (n *Node) Depth() int {
|
||||||
depth := 0
|
depth := 0
|
||||||
@@ -67,7 +69,7 @@ func (n *Node) Depth() int {
|
|||||||
|
|
||||||
// Summary help string for the node.
|
// Summary help string for the node.
|
||||||
func (n *Node) Summary() string {
|
func (n *Node) Summary() string {
|
||||||
summary := n.Name
|
summary := n.Path()
|
||||||
if n.Type == ArgumentNode {
|
if n.Type == ArgumentNode {
|
||||||
summary = "<" + summary + ">"
|
summary = "<" + summary + ">"
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-1
@@ -69,10 +69,13 @@ func Writers(stdout, stderr io.Writer) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HookFunction is a callback tied to a field of the grammar, called before a value is applied.
|
||||||
|
type HookFunction func(ctx *Context, path *Path) error
|
||||||
|
|
||||||
// Hook to aply before a command, flag or positional argument is encountered.
|
// Hook to aply before a command, flag or positional argument is encountered.
|
||||||
//
|
//
|
||||||
// "ptr" is a pointer to a field of the grammar.
|
// "ptr" is a pointer to a field of the grammar.
|
||||||
func Hook(ptr interface{}, hook Before) Option {
|
func Hook(ptr interface{}, hook HookFunction) Option {
|
||||||
key := reflect.ValueOf(ptr)
|
key := reflect.ValueOf(ptr)
|
||||||
if key.Kind() != reflect.Ptr {
|
if key.Kind() != reflect.Ptr {
|
||||||
panic("expected a pointer")
|
panic("expected a pointer")
|
||||||
@@ -81,3 +84,14 @@ func Hook(ptr interface{}, hook Before) Option {
|
|||||||
k.before[key] = hook
|
k.before[key] = hook
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HelpFunction func(*Context) error
|
||||||
|
|
||||||
|
// Help function to use.
|
||||||
|
//
|
||||||
|
// Defaults to PrintHelp.
|
||||||
|
func Help(help func(*Context) error) Option {
|
||||||
|
return func(k *Kong) {
|
||||||
|
k.help = help
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user