diff --git a/context.go b/context.go index 3ff7f4e..6ad15c3 100644 --- a/context.go +++ b/context.go @@ -2,7 +2,6 @@ package kong import ( "fmt" - "io" "reflect" "strconv" "strings" @@ -31,13 +30,24 @@ type Context struct { Path []*Path // A trace through parsed nodes. Error error // Error that occurred during trace, if any. - Stdout io.Writer - Stderr io.Writer - args []string 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. // // The returned Context will include a Path of all commands, arguments, positionals and flags. diff --git a/help.go b/help.go index 5c54764..0d6d937 100644 --- a/help.go +++ b/help.go @@ -7,130 +7,154 @@ import ( "io" "reflect" "strings" - - "github.com/aymerick/raymond" ) const ( - 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}} -` + defaultIndent = 2 ) -var defaultHelpTemplate = raymond.MustParse(strings.TrimSpace(defaultTemplate)) - -func init() { - defaultHelpTemplate.RegisterHelpers(map[string]interface{}{ - "indent": func(options *raymond.Options) string { - indent, ok := options.HashProp("depth").(int) - if !ok { - indent = 2 - } - 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() - }, - }) +func PrintHelp(ctx *Context) error { + w := newHelpWriter(guessWidth(ctx.App.Stdout)) + selected := ctx.Selected() + if selected == nil { + printApp(w, ctx.App.Application) + } else { + printCommand(w, ctx.App.Application, selected) + } + return w.Write(ctx.App.Stdout) } -// Help returns a Before hook that will display help and exit. -// -// tmpl receives a context with several top-level values, in addition to those passed through tmplctx: -// .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 { - merged := map[string]interface{}{ - "App": ctx.App, - "Context": ctx, - "Path": path, +func printApp(w *helpWriter, app *Application) { + w.Printf("usage: %s", app.Summary()) + printNodeDetail(w, &app.Node) +} + +func printCommand(w *helpWriter, app *Application, cmd *Command) { + w.Printf("usage: %s %s", app.Name, cmd.Summary()) + printNodeDetail(w, cmd) +} + +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() - frame.Set("width", guessWidth(ctx.App.Stdout)) - output, err := tmpl.ExecWith(merged, frame) + } +} + +func printCommandSummary(w *helpWriter, cmd *Command) { + w.Print(cmd.Summary()) + 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 { 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. - s := 0 + leftSize := 0 for _, row := range rows { - if c := len(row[0]); c > s && c < 30 { - s = c + if c := len(row[0]); c > leftSize && c < 30 { + leftSize = c } } - indentStr := strings.Repeat(" ", indent) - offsetStr := strings.Repeat(" ", s+padding) + offsetStr := strings.Repeat(" ", leftSize+padding) for _, row := range rows { 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") - fmt.Fprintf(w, "%s%-*s%*s", indentStr, s, row[0], padding, "") - if len(row[0]) >= 30 { - fmt.Fprintf(w, "\n%s%s", indentStr, offsetStr) + + line := fmt.Sprintf("%-*s", leftSize, row[0]) + if len(row[0]) < 30 { + line += fmt.Sprintf("%*s%s", padding, "", lines[0]) + lines = lines[1:] } - fmt.Fprintf(w, "%s\n", lines[0]) - for _, line := range lines[1:] { - fmt.Fprintf(w, "%s%s%s\n", indentStr, offsetStr, line) + w.Print(line) + for _, line := range lines { + w.Printf("%s%s", offsetStr, line) } } } diff --git a/help_test.go b/help_test.go index 2008855..c359536 100644 --- a/help_test.go +++ b/help_test.go @@ -2,7 +2,6 @@ package kong import ( "bytes" - "fmt" "testing" "github.com/stretchr/testify/require" @@ -10,11 +9,16 @@ import ( func TestHelp(t *testing.T) { var cli struct { - String string `kong:"help='A string flag.'"` - Bool bool `kong:"help='A bool flag.'"` + String string `help:"A string flag."` + Bool bool `help:"A bool flag with very long help that wraps a lot and is verbose and is really verbose."` 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) exited := false @@ -27,5 +31,34 @@ func TestHelp(t *testing.T) { _, err := app.Parse([]string{"--help"}) require.NoError(t, err) require.True(t, exited) - fmt.Println(w.String()) + require.Equal(t, `usage: test-app [] + +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 [] + A subcommand. + + two [] + 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 [] + +A subcommand. + +Flags: + --flag=STRING Nested flag. +`, w.String()) } diff --git a/kong.go b/kong.go index f27c463..c16adc3 100644 --- a/kong.go +++ b/kong.go @@ -8,9 +8,6 @@ import ( "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. type Error struct{ msg string } @@ -39,9 +36,10 @@ type Kong struct { Stdout io.Writer Stderr io.Writer - before map[reflect.Value]Before + before map[reflect.Value]HookFunction registry *Registry noDefaultHelp bool + help func(*Context) error } // New creates a new Kong parser on grammar. @@ -52,8 +50,9 @@ func New(grammar interface{}, options ...Option) (*Kong, error) { Exit: os.Exit, Stdout: os.Stdout, Stderr: os.Stderr, - before: map[reflect.Value]Before{}, + before: map[reflect.Value]HookFunction{}, registry: NewRegistry().RegisterDefaults(), + help: PrintHelp, } for _, option := range options { @@ -89,7 +88,14 @@ func (k *Kong) extraFlags() []*Flag { 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) return []*Flag{helpFlag} } diff --git a/kong_test.go b/kong_test.go index 201615c..0c0badf 100644 --- a/kong_test.go +++ b/kong_test.go @@ -9,9 +9,12 @@ import ( func mustNew(t *testing.T, cli interface{}, options ...Option) *Kong { t.Helper() - options = append([]Option{ExitFunction(func(int) { - t.Fatalf("unexpected exit()") - })}, options...) + options = append([]Option{ + ExitFunction(func(int) { + t.Helper() + t.Fatalf("unexpected exit()") + }), + }, options...) parser, err := New(cli, options...) require.NoError(t, err) return parser diff --git a/model.go b/model.go index 82bcac2..5f15e12 100644 --- a/model.go +++ b/model.go @@ -12,23 +12,6 @@ type Application struct { 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 Command = Node @@ -54,6 +37,25 @@ type Node struct { 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. func (n *Node) Depth() int { depth := 0 @@ -67,7 +69,7 @@ func (n *Node) Depth() int { // Summary help string for the node. func (n *Node) Summary() string { - summary := n.Name + summary := n.Path() if n.Type == ArgumentNode { summary = "<" + summary + ">" } diff --git a/options.go b/options.go index 481b06f..99bfe5b 100644 --- a/options.go +++ b/options.go @@ -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. // // "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) if key.Kind() != reflect.Ptr { panic("expected a pointer") @@ -81,3 +84,14 @@ func Hook(ptr interface{}, hook Before) Option { 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 + } +}