-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathmenu.go
More file actions
185 lines (150 loc) · 4.92 KB
/
Copy pathmenu.go
File metadata and controls
185 lines (150 loc) · 4.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
package exoskeleton
import (
"bytes"
"fmt"
"sort"
"strings"
"text/template"
)
const menuTemplate = "\033[1m" + `USAGE` + "\033[0m" + `
{{.Usage}}
{{- range .Sections}}
` + "\033[1m" + `{{.Heading}}` + "\033[0m" + `
{{- range .MenuItems}}
{{rpad .Name .Width}} {{.Summary}}
{{- end}}
{{- end}}
Run ` + "\033[96m" + `{{.HelpUsage}} <command>` + "\033[0m" + ` to print information on a specific command.`
var templateFuncs = template.FuncMap{
"rpad": func(s string, padding int) string { return fmt.Sprintf("%*s", -padding, s) },
}
// SummaryFunc is a function that is expected to return the heading
type SummaryFunc func(Command) (string, error)
// MenuOptions are the options that control how menus are constructed for modules.
type MenuOptions struct {
// Depth describes how recursively a menu should be constructed. Its default
// value is 0, which indicates that the menu should list only the commands
// that are descendants of the module. A value of 1 would list descendants one
// level deep, a value of 2 would list descendants two levels deep, etc. A value
// -1 lists all descendants.
Depth int
// HeadingFor accepts the parent Command and a subcommand, returning a
// string to use as a section heading for the subcommand.
// The default function returns "COMMANDS".
HeadingFor MenuHeadingForFunc
// SummaryFor accepts a Command and returns its summary and, optionally, an error.
// The default function invokes Summary() on the provided Command.
SummaryFor SummaryFunc
// Template is executed with the constructed exoskeleton.Menu to render
// help content for a Command with subcommands.
Template *template.Template
}
// Menu is the data passed to MenuOptions.Template when it is executed.
type Menu struct {
Usage string
HelpUsage string
Sections MenuSections
}
type MenuSections []MenuSection
type MenuSection struct {
Heading string
MenuItems MenuItems
}
type MenuItems []*MenuItem
// implement sort.Interface so that MenuItems can be sorted by Name
func (m MenuItems) Len() int { return len(m) }
func (m MenuItems) Less(i, j int) bool { return m[i].Name < m[j].Name }
func (m MenuItems) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
func (m MenuItems) MaxWidth() (longestCommand int) {
for _, menuItem := range m {
if len(menuItem.Name) > longestCommand {
longestCommand = len(menuItem.Name)
}
}
return
}
type MenuItem struct {
Name string
Summary string
Heading string
Width int
}
// MenuFor renders a menu of commands for a Command with subcommands.
func MenuFor(cmd Command, opts *MenuOptions) (string, []error) {
if opts.Template == nil {
opts.Template = template.Must(template.New("menu").Funcs(templateFuncs).Parse(menuTemplate))
}
menu, errs := buildMenu(cmd, opts)
b := new(bytes.Buffer)
if err := opts.Template.Execute(b, menu); err != nil {
panic(err)
}
return b.String(), errs
}
// buildMenu constructs a Menu of Commands with their short summary strings for a given Command with subcommands.
func buildMenu(cmd Command, opts *MenuOptions) (*Menu, []error) {
if opts.SummaryFor == nil {
opts.SummaryFor = func(c Command) (string, error) { return c.Summary() }
}
if opts.HeadingFor == nil {
opts.HeadingFor = func(Command, Command) string { return "COMMANDS" }
}
c, err := cmd.Subcommands()
if err != nil {
return &Menu{}, []error{err}
}
c, errs := c.Expand(WithDepth(opts.Depth), WithoutExpandedModules())
allItems, ferrs :=
parallelMap(c, func(subcmd Command) ([]*MenuItem, []error) {
name := UsageRelativeTo(subcmd, cmd)
if HasSubcommands(subcmd) {
name += ":"
}
summary, err := opts.SummaryFor(subcmd)
if err != nil {
return nil, []error{err}
}
if summary == "" {
return nil, nil
}
heading := opts.HeadingFor(nil, subcmd)
return []*MenuItem{{Name: name, Summary: summary, Heading: heading}}, nil
})
errs = append(errs, ferrs...)
// Remove duplicates after parallel processing
seen := make(map[string]bool)
var items MenuItems
for _, item := range allItems {
if item != nil && !seen[item.Name] {
seen[item.Name] = true
items = append(items, item)
}
}
width := items.MaxWidth()
byHeading := make(map[string]MenuItems)
var orderedHeadings []string
for _, menuItem := range items {
menuItem.Width = width
if _, present := byHeading[menuItem.Heading]; !present {
orderedHeadings = append(orderedHeadings, menuItem.Heading)
}
byHeading[menuItem.Heading] = append(byHeading[menuItem.Heading], menuItem)
}
var sections MenuSections
for _, heading := range orderedHeadings {
menuItems := byHeading[heading]
if len(menuItems) > 0 {
sort.Sort(menuItems)
sections = append(sections, MenuSection{heading, menuItems})
}
}
return &Menu{
Usage: Usage(cmd) + " <command> [<args>]",
Sections: sections,
HelpUsage: helpUsage(cmd),
}, errs
}
func helpUsage(cmd Command) string {
args := argsRelativeTo(cmd, nil)
return strings.Join(append([]string{args[0], "help"}, args[1:]...), " ")
}