Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 19 additions & 2 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -213,11 +213,28 @@ 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,
}
value.Flag = flag
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,
}
}
130 changes: 107 additions & 23 deletions help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
key := ""
if group := node.ClosestGroup(); group != nil {
key = group.Key
if _, ok := nodesByGroup[key]; !ok {
groups = append(groups, group)
}
}
nodesByGroup[key] = append(nodesByGroup[key], node)
}

out := []helpCommandGroup{}
for name, nodes := range groups {
if name == "" {
name = "Commands"
}
out = append(out, helpCommandGroup{Name: name, Commands: nodes})
// 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
}
Expand Down
163 changes: 162 additions & 1 deletion help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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."`
Expand Down Expand Up @@ -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.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting looks good!


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())
})
}
1 change: 1 addition & 0 deletions kong.go
Original file line number Diff line number Diff line change
Expand Up @@ -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().
Expand Down
Loading