Add support for groups in the default HelpPrinter (#135)
This commit is contained in:
@@ -589,6 +589,7 @@ The default help output is usually sufficient, but if not there are two solution
|
||||
1. Use `ConfigureHelp(HelpOptions)` to configure how help is formatted (see [HelpOptions](https://godoc.org/github.com/alecthomas/kong#HelpOptions) for details).
|
||||
2. Custom help can be wired into Kong via the `Help(HelpFunc)` option. The `HelpFunc` is passed a `Context`, which contains the parsed context for the current command-line. See the implementation of `PrintHelp` for an example.
|
||||
3. Use `HelpFormatter(HelpValueFormatter)` if you want to just customize the help text that is accompanied by flags and arguments.
|
||||
4. Use `Groups([]Group)` if you want to customize group titles or add a header.
|
||||
|
||||
### `Bind(...)` - bind values for callback hooks and Run() methods
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.S
|
||||
child.Parent = node
|
||||
child.Help = tag.Help
|
||||
child.Hidden = tag.Hidden
|
||||
child.Group = tag.Group
|
||||
child.Group = buildGroupForKey(k, tag.Group)
|
||||
child.Aliases = tag.Aliases
|
||||
|
||||
if provider, ok := fv.Addr().Interface().(HelpProvider); ok {
|
||||
@@ -213,7 +213,7 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
|
||||
Short: tag.Short,
|
||||
PlaceHolder: tag.PlaceHolder,
|
||||
Env: tag.Env,
|
||||
Group: tag.Group,
|
||||
Group: buildGroupForKey(k, tag.Group),
|
||||
Xor: tag.Xor,
|
||||
Hidden: tag.Hidden,
|
||||
}
|
||||
@@ -221,3 +221,20 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
|
||||
node.Flags = append(node.Flags, flag)
|
||||
}
|
||||
}
|
||||
|
||||
func buildGroupForKey(k *Kong, key string) *Group {
|
||||
if key == "" {
|
||||
return nil
|
||||
}
|
||||
for _, group := range k.groups {
|
||||
if group.Key == key {
|
||||
return &group
|
||||
}
|
||||
}
|
||||
|
||||
// No group provided with kong.Groups. We create one ad-hoc for this key.
|
||||
return &Group{
|
||||
Key: key,
|
||||
Title: key,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,22 +144,41 @@ func printNodeDetail(w *helpWriter, node *Node, hide bool) {
|
||||
writePositionals(w.Indent(), node.Positional)
|
||||
}
|
||||
if flags := node.AllFlags(true); len(flags) > 0 {
|
||||
w.Print("")
|
||||
w.Print("Flags:")
|
||||
writeFlags(w.Indent(), flags)
|
||||
groupedFlags := collectFlagGroups(flags)
|
||||
for _, group := range groupedFlags {
|
||||
w.Print("")
|
||||
if group.Metadata.Title != "" {
|
||||
w.Print(group.Metadata.Title)
|
||||
}
|
||||
if group.Metadata.Header != "" {
|
||||
w.Print(group.Metadata.Header)
|
||||
}
|
||||
writeFlags(w.Indent(), group.Flags)
|
||||
}
|
||||
}
|
||||
cmds := node.Leaves(hide)
|
||||
if len(cmds) > 0 {
|
||||
w.Print("")
|
||||
w.Print("Commands:")
|
||||
iw := w.Indent()
|
||||
if w.Tree {
|
||||
writeCommandTree(w, node)
|
||||
w.Print("")
|
||||
w.Print("Commands:")
|
||||
writeCommandTree(iw, node)
|
||||
} else {
|
||||
iw := w.Indent()
|
||||
if w.Compact {
|
||||
writeCompactCommandList(cmds, iw)
|
||||
} else {
|
||||
writeCommandList(cmds, iw)
|
||||
groupedCmds := collectCommandGroups(cmds)
|
||||
for _, group := range groupedCmds {
|
||||
w.Print("")
|
||||
if group.Metadata.Title != "" {
|
||||
w.Print(group.Metadata.Title)
|
||||
}
|
||||
if group.Metadata.Header != "" {
|
||||
w.Print(group.Metadata.Header)
|
||||
}
|
||||
|
||||
if w.Compact {
|
||||
writeCompactCommandList(group.Commands, iw)
|
||||
} else {
|
||||
writeCommandList(group.Commands, iw)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,7 +208,6 @@ func writeCompactCommandList(cmds []*Node, iw *helpWriter) {
|
||||
}
|
||||
|
||||
func writeCommandTree(w *helpWriter, node *Node) {
|
||||
iw := w.Indent()
|
||||
rows := make([][2]string, 0, len(node.Children)*2)
|
||||
for i, cmd := range node.Children {
|
||||
if cmd.Hidden {
|
||||
@@ -200,27 +218,93 @@ func writeCommandTree(w *helpWriter, node *Node) {
|
||||
rows = append(rows, [2]string{"", ""})
|
||||
}
|
||||
}
|
||||
writeTwoColumns(iw, rows)
|
||||
writeTwoColumns(w, rows)
|
||||
}
|
||||
|
||||
type helpFlagGroup struct {
|
||||
Metadata *Group
|
||||
Flags [][]*Flag
|
||||
}
|
||||
|
||||
func collectFlagGroups(flags [][]*Flag) []helpFlagGroup {
|
||||
// Group keys in order of appearance.
|
||||
groups := []*Group{}
|
||||
// Flags grouped by their group key.
|
||||
flagsByGroup := map[string][][]*Flag{}
|
||||
|
||||
for _, levelFlags := range flags {
|
||||
levelFlagsByGroup := map[string][]*Flag{}
|
||||
|
||||
for _, flag := range levelFlags {
|
||||
key := ""
|
||||
if flag.Group != nil {
|
||||
key = flag.Group.Key
|
||||
groupAlreadySeen := false
|
||||
for _, group := range groups {
|
||||
if key == group.Key {
|
||||
groupAlreadySeen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !groupAlreadySeen {
|
||||
groups = append(groups, flag.Group)
|
||||
}
|
||||
}
|
||||
|
||||
levelFlagsByGroup[key] = append(levelFlagsByGroup[key], flag)
|
||||
}
|
||||
|
||||
for key, flags := range levelFlagsByGroup {
|
||||
flagsByGroup[key] = append(flagsByGroup[key], flags)
|
||||
}
|
||||
}
|
||||
|
||||
out := []helpFlagGroup{}
|
||||
// Ungrouped flags are always displayed first.
|
||||
if ungroupedFlags, ok := flagsByGroup[""]; ok {
|
||||
out = append(out, helpFlagGroup{
|
||||
Metadata: &Group{Title: "Flags:"},
|
||||
Flags: ungroupedFlags,
|
||||
})
|
||||
}
|
||||
for _, group := range groups {
|
||||
out = append(out, helpFlagGroup{Metadata: group, Flags: flagsByGroup[group.Key]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// nolint: unused
|
||||
type helpCommandGroup struct {
|
||||
Name string
|
||||
Metadata *Group
|
||||
Commands []*Node
|
||||
}
|
||||
|
||||
// nolint: unused, deadcode
|
||||
func collectCommandGroups(nodes []*Node) []helpCommandGroup {
|
||||
groups := map[string][]*Node{}
|
||||
// Groups in order of appearance.
|
||||
groups := []*Group{}
|
||||
// Nodes grouped by their group key.
|
||||
nodesByGroup := map[string][]*Node{}
|
||||
|
||||
for _, node := range nodes {
|
||||
groups[node.Group] = append(groups[node.Group], node)
|
||||
}
|
||||
out := []helpCommandGroup{}
|
||||
for name, nodes := range groups {
|
||||
if name == "" {
|
||||
name = "Commands"
|
||||
key := ""
|
||||
if group := node.ClosestGroup(); group != nil {
|
||||
key = group.Key
|
||||
if _, ok := nodesByGroup[key]; !ok {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
}
|
||||
out = append(out, helpCommandGroup{Name: name, Commands: nodes})
|
||||
nodesByGroup[key] = append(nodesByGroup[key], node)
|
||||
}
|
||||
|
||||
out := []helpCommandGroup{}
|
||||
// Ungrouped nodes are always displayed first.
|
||||
if ungroupedNodes, ok := nodesByGroup[""]; ok {
|
||||
out = append(out, helpCommandGroup{
|
||||
Metadata: &Group{Title: "Commands:"},
|
||||
Commands: ungroupedNodes,
|
||||
})
|
||||
}
|
||||
for _, group := range groups {
|
||||
out = append(out, helpCommandGroup{Metadata: group, Commands: nodesByGroup[group.Key]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
+162
-1
@@ -136,7 +136,7 @@ func TestHelpTree(t *testing.T) {
|
||||
Other struct {
|
||||
Other string `arg help:"other arg"`
|
||||
} `arg help:"subcommand other"`
|
||||
} `cmd help:"subcommand one"`
|
||||
} `cmd help:"subcommand one" group:"Group A"` // Groups are ignored in trees
|
||||
|
||||
Two struct {
|
||||
Three threeArg `arg help:"Sub-sub-arg."`
|
||||
@@ -248,3 +248,164 @@ func TestCustomHelpFormatter(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, w.String(), "A flag.")
|
||||
}
|
||||
|
||||
func TestHelpGrouping(t *testing.T) {
|
||||
// nolint: govet
|
||||
var cli struct {
|
||||
GroupedAString string `help:"A string flag grouped in A." group:"Group A"`
|
||||
FreeString string `help:"A non grouped string flag."`
|
||||
GroupedBString string `help:"A string flag grouped in B." group:"Group B"`
|
||||
FreeBool bool `help:"A non grouped bool flag."`
|
||||
GroupedABool bool `help:"A bool flag grouped in A." group:"Group A"`
|
||||
|
||||
One struct {
|
||||
Flag string `help:"Nested flag."`
|
||||
// Group is inherited from the parent command
|
||||
Thing struct {
|
||||
Arg string `arg help:"argument"`
|
||||
} `cmd help:"subcommand thing"`
|
||||
Other struct {
|
||||
Other string `arg help:"other arg"`
|
||||
} `arg help:"subcommand other"`
|
||||
// ... but a subcommand can override it
|
||||
Stuff struct {
|
||||
Stuff string `arg help:"argument"`
|
||||
} `arg help:"subcommand stuff" group:"Group B"`
|
||||
} `cmd help:"A subcommand grouped in A." group:"Group A"`
|
||||
|
||||
Two struct {
|
||||
Grouped1String string `help:"A string flag grouped in 1." group:"Group 1"`
|
||||
AFreeString string `help:"A non grouped string flag."`
|
||||
Grouped2String string `help:"A string flag grouped in 2." group:"Group 2"`
|
||||
AGroupedAString bool `help:"A string flag grouped in A." group:"Group A"`
|
||||
Grouped1Bool bool `help:"A bool flag grouped in 1." group:"Group 1"`
|
||||
} `cmd help:"A non grouped subcommand."`
|
||||
|
||||
Four struct {
|
||||
Flag string `help:"Nested flag."`
|
||||
} `cmd help:"Another subcommand grouped in B." group:"Group B"`
|
||||
|
||||
Three struct {
|
||||
Flag string `help:"Nested flag."`
|
||||
} `cmd help:"Another subcommand grouped in A." group:"Group A"`
|
||||
}
|
||||
|
||||
w := bytes.NewBuffer(nil)
|
||||
exited := false
|
||||
app := mustNew(t, &cli,
|
||||
kong.Name("test-app"),
|
||||
kong.Description("A test app."),
|
||||
kong.Groups([]kong.Group{
|
||||
{
|
||||
Key: "Group A",
|
||||
Title: "Group title taken from the kong.Groups option",
|
||||
Header: "A group header",
|
||||
},
|
||||
{
|
||||
Key: "Group 1",
|
||||
Title: "Another group title, this time without header",
|
||||
},
|
||||
{
|
||||
Key: "Unknown key",
|
||||
},
|
||||
}),
|
||||
kong.Writers(w, w),
|
||||
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.True(t, exited)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
expected := `Usage: test-app <command>
|
||||
|
||||
A test app.
|
||||
|
||||
Flags:
|
||||
-h, --help Show context-sensitive help.
|
||||
--free-string=STRING A non grouped string flag.
|
||||
--free-bool A non grouped bool flag.
|
||||
|
||||
Group title taken from the kong.Groups option
|
||||
A group header
|
||||
--grouped-a-string=STRING A string flag grouped in A.
|
||||
--grouped-a-bool A bool flag grouped in A.
|
||||
|
||||
Group B
|
||||
--grouped-b-string=STRING A string flag grouped in B.
|
||||
|
||||
Commands:
|
||||
two
|
||||
A non grouped subcommand.
|
||||
|
||||
Group title taken from the kong.Groups option
|
||||
A group header
|
||||
one thing <arg>
|
||||
subcommand thing
|
||||
|
||||
one <other>
|
||||
subcommand other
|
||||
|
||||
three
|
||||
Another subcommand grouped in A.
|
||||
|
||||
Group B
|
||||
one <stuff>
|
||||
subcommand stuff
|
||||
|
||||
four
|
||||
Another subcommand grouped in B.
|
||||
|
||||
Run "test-app <command> --help" for more information on a command.
|
||||
`
|
||||
t.Log(w.String())
|
||||
t.Log(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{"two", "--help"})
|
||||
require.NoError(t, err)
|
||||
require.True(t, exited)
|
||||
})
|
||||
expected := `Usage: test-app two
|
||||
|
||||
A non grouped subcommand.
|
||||
|
||||
Flags:
|
||||
-h, --help Show context-sensitive help.
|
||||
--free-string=STRING A non grouped string flag.
|
||||
--free-bool A non grouped bool flag.
|
||||
|
||||
--a-free-string=STRING A non grouped string flag.
|
||||
|
||||
Group title taken from the kong.Groups option
|
||||
A group header
|
||||
--grouped-a-string=STRING A string flag grouped in A.
|
||||
--grouped-a-bool A bool flag grouped in A.
|
||||
|
||||
--a-grouped-a-string A string flag grouped in A.
|
||||
|
||||
Group B
|
||||
--grouped-b-string=STRING A string flag grouped in B.
|
||||
|
||||
Another group title, this time without header
|
||||
--grouped-1-string=STRING A string flag grouped in 1.
|
||||
--grouped-1-bool A bool flag grouped in 1.
|
||||
|
||||
Group 2
|
||||
--grouped-2-string=STRING A string flag grouped in 2.
|
||||
`
|
||||
t.Log(expected)
|
||||
t.Log(w.String())
|
||||
require.Equal(t, expected, w.String())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ type Kong struct {
|
||||
helpFormatter HelpValueFormatter
|
||||
helpOptions HelpOptions
|
||||
helpFlag *Flag
|
||||
groups []Group
|
||||
vars Vars
|
||||
|
||||
// Set temporarily by Options. These are applied after build().
|
||||
|
||||
@@ -46,7 +46,7 @@ type Node struct {
|
||||
Name string
|
||||
Help string // Short help displayed in summaries.
|
||||
Detail string // Detailed help displayed when describing command/arg alone.
|
||||
Group string
|
||||
Group *Group
|
||||
Hidden bool
|
||||
Flags []*Flag
|
||||
Positional []*Positional
|
||||
@@ -203,6 +203,18 @@ func (n *Node) Path() (out string) {
|
||||
return strings.TrimSpace(out)
|
||||
}
|
||||
|
||||
// ClosestGroup finds the first non-nil group in this node and its ancestors.
|
||||
func (n *Node) ClosestGroup() *Group {
|
||||
switch {
|
||||
case n.Group != nil:
|
||||
return n.Group
|
||||
case n.Parent != nil:
|
||||
return n.Parent.ClosestGroup()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// A Value is either a flag or a variable positional argument.
|
||||
type Value struct {
|
||||
Flag *Flag // Nil if positional argument.
|
||||
@@ -351,7 +363,7 @@ type Positional = Value
|
||||
// A Flag represents a command-line flag.
|
||||
type Flag struct {
|
||||
*Value
|
||||
Group string // Logical grouping when displaying. May also be used by configuration loaders to group options logically.
|
||||
Group *Group // Logical grouping when displaying. May also be used by configuration loaders to group options logically.
|
||||
Xor string
|
||||
PlaceHolder string
|
||||
Env string
|
||||
@@ -394,6 +406,17 @@ func (f *Flag) FormatPlaceHolder() string {
|
||||
return strings.ToUpper(f.Name) + tail
|
||||
}
|
||||
|
||||
// Group holds metadata about a command or flag group used when printing help.
|
||||
type Group struct {
|
||||
// Key is the `group` field tag value used to identify this group.
|
||||
Key string
|
||||
// Title is displayed above the grouped items.
|
||||
Title string
|
||||
// Header is optional and displayed under the Title when non empty.
|
||||
// It can be used to introduce the group's purpose to the user.
|
||||
Header string
|
||||
}
|
||||
|
||||
// This is directly from the Go 1.13 source code.
|
||||
func reflectValueIsZero(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
|
||||
+10
@@ -208,6 +208,16 @@ func ConfigureHelp(options HelpOptions) Option {
|
||||
})
|
||||
}
|
||||
|
||||
// Groups associates `group` field tags with their metadata.
|
||||
//
|
||||
// It can be used to provide a title or header to a command or flag group.
|
||||
func Groups(groups []Group) Option {
|
||||
return OptionFunc(func(k *Kong) error {
|
||||
k.groups = groups
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// UsageOnError configures Kong to display context-sensitive usage if FatalIfErrorf is called with an error.
|
||||
func UsageOnError() Option {
|
||||
return OptionFunc(func(k *Kong) error {
|
||||
|
||||
Reference in New Issue
Block a user