This commit is contained in:
Alec Thomas
2018-06-04 13:25:12 +10:00
committed by Gerald Kaszuba
parent 48af58cefa
commit 2afd4ba47b
7 changed files with 229 additions and 137 deletions
+14 -4
View File
@@ -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.
+124 -100
View File
@@ -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
View File
@@ -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())
} }
+12 -6
View File
@@ -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
View File
@@ -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
+20 -18
View File
@@ -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
View File
@@ -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
}
}