From 2a90ca2ad300aa9820c9cf299ae21fb0385f964c Mon Sep 17 00:00:00 2001 From: Matthias Fax Date: Mon, 21 Jan 2019 14:14:26 +0100 Subject: [PATCH] feat: add a command tree help view (#32) Add a command tree help view. Following up #29 and #30, I noticed that the default help looks quite duplicative since only the help text of the last command in chain is viewed. I needed an option to also show the help text of the command nodes in an appropriate way. This is what I came up with. --- help.go | 91 +++++++++++++++++++++++++++++++++++++++++++------ help_test.go | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 10 deletions(-) diff --git a/help.go b/help.go index d640efb..bc4aeba 100644 --- a/help.go +++ b/help.go @@ -37,6 +37,14 @@ type HelpOptions struct { // Write help in a more compact, but still fully-specified, form. Compact bool + + // Tree writes command chains in a tree structure instead of listing them separately. + Tree bool + + // Indenter modulates the given prefix for the next layer in the tree view. + // The following exported templates can be used: kong.SpaceIndenter, kong.LineIndenter, kong.TreeIndenter + // The kong.SpaceIndenter will be used by default. + Indenter HelpIndenter } // HelpProvider can be implemented by commands/args to provide detailed help. @@ -45,6 +53,9 @@ type HelpProvider interface { Help() string } +// HelpIndenter is used to indent new layers in the help tree. +type HelpIndenter func(prefix string) string + // HelpPrinter is used to print context-sensitive help. type HelpPrinter func(options HelpOptions, ctx *Context) error @@ -115,19 +126,32 @@ func printNodeDetail(w *helpWriter, node *Node, hide bool) { cmds := node.Leaves(hide) if len(cmds) > 0 { w.Print("") - w.Print("Commands:") - iw := w.Indent() - if w.Compact { - rows := [][2]string{} - for _, cmd := range cmds { - rows = append(rows, [2]string{cmd.Path(), cmd.Help}) + if w.Tree { + w.Print("Command Tree:") + iw := w.Indent() + var rows [][2]string + for i, cmd := range node.Children { + rows = append(rows, w.commandTree(cmd, "")...) + if i != len(node.Children)-1 { + rows = append(rows, [2]string{"", ""}) + } } writeTwoColumns(iw, defaultColumnPadding, rows) } else { - for i, cmd := range cmds { - printCommandSummary(iw, cmd) - if i != len(cmds)-1 { - iw.Print("") + w.Print("Commands:") + iw := w.Indent() + if w.Compact { + rows := [][2]string{} + for _, cmd := range cmds { + rows = append(rows, [2]string{cmd.Path(), cmd.Help}) + } + writeTwoColumns(iw, defaultColumnPadding, rows) + } else { + for i, cmd := range cmds { + printCommandSummary(iw, cmd) + if i != len(cmds)-1 { + iw.Print("") + } } } } @@ -276,3 +300,50 @@ func formatFlag(haveShort bool, flag *Flag) string { } return flagString } + +// commandTree creates a tree with the given node name as root and its children's arguments and sub commands as leaves. +func (w *HelpOptions) commandTree(node *Node, prefix string) (rows [][2]string) { + var nodeName string + switch node.Type { + default: + nodeName += prefix + node.Name + case ArgumentNode: + nodeName += prefix + "<" + node.Name + ">" + } + rows = append(rows, [2]string{nodeName, node.Help}) + if w.Indenter == nil { + prefix = SpaceIndenter(prefix) + } else { + prefix = w.Indenter(prefix) + } + for _, arg := range node.Positional { + rows = append(rows, [2]string{prefix + arg.Summary(), arg.Help}) + } + for _, subCmd := range node.Children { + rows = append(rows, w.commandTree(subCmd, prefix)...) + } + return +} + +// SpaceIndenter adds a space indent to the given prefix. +func SpaceIndenter(prefix string) string { + return prefix + strings.Repeat(" ", defaultIndent) +} + +// SpaceIndenter adds line points to every new indent. +func LineIndenter(prefix string) string { + if prefix == "" { + return "- " + } else { + return strings.Repeat(" ", defaultIndent) + prefix + } +} + +// TreeIndenter adds line points to every new indent and vertical lines to every layer. +func TreeIndenter(prefix string) string { + if prefix == "" { + return "|- " + } else { + return "|" + strings.Repeat(" ", defaultIndent) + prefix + } +} diff --git a/help_test.go b/help_test.go index 91068c5..a985bba 100644 --- a/help_test.go +++ b/help_test.go @@ -124,3 +124,98 @@ Flags: require.Equal(t, expected, w.String()) }) } + +func TestHelpTree(t *testing.T) { + // nolint: govet + var cli struct { + One struct { + Thing struct { + Arg string `arg help:"argument"` + } `cmd help:"subcommand thing"` + Other struct { + Other string `arg help:"other arg"` + } `arg help:"subcommand other"` + } `cmd help:"subcommand one"` + + Two struct { + Three threeArg `arg help:"Sub-sub-arg."` + + Four struct { + } `cmd help:"Sub-sub-command."` + } `cmd help:"Another subcommand."` + } + + w := bytes.NewBuffer(nil) + exited := false + app := mustNew(t, &cli, + kong.Name("test-app"), + kong.Description("A test app."), + kong.Writers(w, w), + kong.ConfigureHelp(kong.HelpOptions{ + Tree: true, + Indenter: kong.LineIndenter, + }), + kong.Exit(func(int) { + exited = true + panic(true) // Panic to fake "exit". + }), + ) + + t.Run("Full", func(t *testing.T) { + require.PanicsWithValue(t, true, func() { + _, err := app.Parse([]string{"--help"}) + require.NoError(t, err) + }) + require.True(t, exited) + expected := `Usage: test-app + +A test app. + +Flags: + --help Show context-sensitive help. + +Command Tree: + one subcommand one + - thing subcommand thing + - argument + - subcommand other + + two Another subcommand. + - Sub-sub-arg. + - four Sub-sub-command. + +Run "test-app --help" for more information on a command. +` + if expected != w.String() { + t.Errorf("help command returned:\n%v\n\nwant:\n%v", w.String(), expected) + } + require.Equal(t, expected, w.String()) + }) + + t.Run("Selected", func(t *testing.T) { + exited = false + w.Truncate(0) + require.PanicsWithValue(t, true, func() { + _, err := app.Parse([]string{"one", "--help"}) + require.NoError(t, err) + }) + require.True(t, exited) + expected := `Usage: test-app one + +subcommand one + +Flags: + --help Show context-sensitive help. + +Command Tree: + thing subcommand thing + - argument + + subcommand other +` + if expected != w.String() { + t.Errorf("help command returned:\n%v\n\nwant:\n%v", w.String(), expected) + } + require.Equal(t, expected, w.String()) + }) +}