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.
This commit is contained in:
committed by
Alec Thomas
parent
fcf5f9bc1a
commit
2a90ca2ad3
@@ -37,6 +37,14 @@ type HelpOptions struct {
|
|||||||
|
|
||||||
// Write help in a more compact, but still fully-specified, form.
|
// Write help in a more compact, but still fully-specified, form.
|
||||||
Compact bool
|
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.
|
// HelpProvider can be implemented by commands/args to provide detailed help.
|
||||||
@@ -45,6 +53,9 @@ type HelpProvider interface {
|
|||||||
Help() string
|
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.
|
// HelpPrinter is used to print context-sensitive help.
|
||||||
type HelpPrinter func(options HelpOptions, ctx *Context) error
|
type HelpPrinter func(options HelpOptions, ctx *Context) error
|
||||||
|
|
||||||
@@ -115,19 +126,32 @@ func printNodeDetail(w *helpWriter, node *Node, hide bool) {
|
|||||||
cmds := node.Leaves(hide)
|
cmds := node.Leaves(hide)
|
||||||
if len(cmds) > 0 {
|
if len(cmds) > 0 {
|
||||||
w.Print("")
|
w.Print("")
|
||||||
w.Print("Commands:")
|
if w.Tree {
|
||||||
iw := w.Indent()
|
w.Print("Command Tree:")
|
||||||
if w.Compact {
|
iw := w.Indent()
|
||||||
rows := [][2]string{}
|
var rows [][2]string
|
||||||
for _, cmd := range cmds {
|
for i, cmd := range node.Children {
|
||||||
rows = append(rows, [2]string{cmd.Path(), cmd.Help})
|
rows = append(rows, w.commandTree(cmd, "")...)
|
||||||
|
if i != len(node.Children)-1 {
|
||||||
|
rows = append(rows, [2]string{"", ""})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
writeTwoColumns(iw, defaultColumnPadding, rows)
|
writeTwoColumns(iw, defaultColumnPadding, rows)
|
||||||
} else {
|
} else {
|
||||||
for i, cmd := range cmds {
|
w.Print("Commands:")
|
||||||
printCommandSummary(iw, cmd)
|
iw := w.Indent()
|
||||||
if i != len(cmds)-1 {
|
if w.Compact {
|
||||||
iw.Print("")
|
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
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -124,3 +124,98 @@ Flags:
|
|||||||
require.Equal(t, expected, w.String())
|
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 <command>
|
||||||
|
|
||||||
|
A test app.
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--help Show context-sensitive help.
|
||||||
|
|
||||||
|
Command Tree:
|
||||||
|
one subcommand one
|
||||||
|
- thing subcommand thing
|
||||||
|
- <arg> argument
|
||||||
|
- <other> subcommand other
|
||||||
|
|
||||||
|
two Another subcommand.
|
||||||
|
- <three> Sub-sub-arg.
|
||||||
|
- four Sub-sub-command.
|
||||||
|
|
||||||
|
Run "test-app <command> --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 <command>
|
||||||
|
|
||||||
|
subcommand one
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--help Show context-sensitive help.
|
||||||
|
|
||||||
|
Command Tree:
|
||||||
|
thing subcommand thing
|
||||||
|
- <arg> argument
|
||||||
|
|
||||||
|
<other> 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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user