-
Notifications
You must be signed in to change notification settings - Fork 167
/
cmd_help.go
286 lines (247 loc) · 9.01 KB
/
cmd_help.go
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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package cmd
import (
"fmt"
"slices"
"strings"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
const (
// this is used for aligning titles in the console.
endOfTitleSentinel string = "\x00"
)
// cmdHelpGenerator defines the required signature to implement and produce help description for commands.
type cmdHelpGenerator func(cmd *cobra.Command) string
// generateCmdHelpOptions defines settings to control the text description for displaying the commands' help.
type generateCmdHelpOptions struct {
Description cmdHelpGenerator
Usage cmdHelpGenerator
Commands cmdHelpGenerator
Flags cmdHelpGenerator
Footer cmdHelpGenerator
}
/*
generateCmdHelp sets the base structure for displaying help documentation for a command in the console.
The base structure is on the form of:
**********************
<description>
<usage>
<commands>
<flags>
<footer>
**********************
Where:
- description: Main information for the command. Default to cobra's `Short` field.
- usage: Demonstrate how to call the command. Default to cobra's `Use` filed.
- commands: The list of sub-commands supported. Default to list cobra's sub-commands in the form of `cmd : short-notes`.
- flags: List of supported flags. Default to `Flags + Global flags` and each flag as `-F, --flag [type] : description`.
- footer: The last section is where commands can define quick-start, examples or extra notes. Default to display notes
about how to report bugs or comments.
*/
func generateCmdHelp(
cmd *cobra.Command,
options generateCmdHelpOptions) string {
getGeneratorOrDefault := func(option, defaultOption cmdHelpGenerator) cmdHelpGenerator {
if option != nil {
return option
}
return defaultOption
}
return fmt.Sprintf("\n%s%s%s%s%s%s\n",
getGeneratorOrDefault(options.Description, getCmdHelpDefaultDescription)(cmd),
getGeneratorOrDefault(options.Usage, getCmdHelpDefaultUsage)(cmd),
getGeneratorOrDefault(options.Commands, getCmdHelpDefaultCommands)(cmd),
getGeneratorOrDefault(options.Flags, getCmdHelpDefaultFlags)(cmd),
getPreFooter(cmd),
getGeneratorOrDefault(options.Footer, getCmdHelpDefaultFooter)(cmd),
)
}
// getCmdHelpDefaultDescription provides the default implementation for displaying the help description section.
func getCmdHelpDefaultDescription(cmd *cobra.Command) string {
return generateCmdHelpDescription(cmd.Short, nil)
}
// getCmdHelpDefaultUsage provides the default implementation for displaying the help usage section.
func getCmdHelpDefaultUsage(cmd *cobra.Command) string {
return fmt.Sprintf("%s\n %s\n\n",
output.WithBold(output.WithUnderline("Usage")),
"{{if .Runnable}}{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}{{.CommandPath}} [command]{{end}}",
)
}
// getCmdHelpDefaultCommands provides the default implementation for displaying the help commands section.
func getCmdHelpDefaultCommands(cmd *cobra.Command) string {
return getCmdHelpAvailableCommands(getCommandsDetails(cmd))
}
// getCmdHelpDefaultFlags provides the default implementation for displaying the help flags section.
func getCmdHelpDefaultFlags(cmd *cobra.Command) (result string) {
if cmd.HasAvailableLocalFlags() {
flags := getFlagsDetails(cmd.LocalFlags())
result = fmt.Sprintf("%s\n%s\n",
output.WithBold(output.WithUnderline("Flags")),
flags)
}
if cmd.HasAvailableInheritedFlags() {
globalFlags := getFlagsDetails(cmd.InheritedFlags())
result += fmt.Sprintf("%s\n%s\n",
output.WithBold(output.WithUnderline("Global Flags")),
globalFlags)
}
return result
}
// getCmdHelpDefaultFooter provides the default implementation for displaying the help footer section.
func getCmdHelpDefaultFooter(*cobra.Command) string {
return fmt.Sprintf("Find a bug? Want to let us know how we're doing? Fill out this brief survey: %s.\n",
output.WithLinkFormat("https://aka.ms/azure-dev/hats"))
}
/*
getCmdHelpCommands defines the base structure for the commands section within the help as:
*******************
Commands:
{{ commands - description }}
*******************
*/
func getCmdHelpCommands(title string, commands string) string {
if commands == "" {
return commands
}
return fmt.Sprintf("%s\n%s\n", output.WithBold(output.WithUnderline(title)), commands)
}
// getCmdHelpGroupedCommands generates {{ commands - description }} where sub-commands are grouped.
func getCmdHelpGroupedCommands(commands string) string {
return getCmdHelpCommands("Commands", commands)
}
// getCmdHelpAvailableCommands generates {{ commands - description }} for all sub-commands.
func getCmdHelpAvailableCommands(commands string) string {
return getCmdHelpCommands("Available Commands", commands)
}
// getFlagsDetails produces the command - flags - details in the form of `-F, --flag [type] : description`
func getFlagsDetails(flagSet *pflag.FlagSet) (result string) {
var lines []string
max := 0
flagSet.VisitAll(func(flag *pflag.Flag) {
if flag.Hidden {
return
}
line := ""
if flag.Shorthand != "" && flag.ShorthandDeprecated == "" {
line = fmt.Sprintf(" -%s, --%s", flag.Shorthand, flag.Name)
} else {
line = fmt.Sprintf(" --%s", flag.Name)
}
varName, usage := pflag.UnquoteUsage(flag)
if varName != "" {
line += " " + varName
}
// insert a sentinel for the end of the flag titles. Lines are aligned based on the longest line.
line += endOfTitleSentinel
lineLen := len(line)
if lineLen > max {
// the max value is used later to fill all lines with same size
max = lineLen
}
line += usage
if len(flag.Deprecated) != 0 {
line += fmt.Sprintf(" (DEPRECATED: %s)", flag.Deprecated)
}
lines = append(lines, line)
})
// no flags
if max == 0 {
return result
}
alignTitles(lines, max)
return fmt.Sprintf(" %s\n", strings.Join(lines, "\n "))
}
// alignTitles update all the input lines to be the same len by adding white spaces. Then it produces the `title : note`
// output.
// Note: alignTitles depends on all lines containing the `endOfTitleSentinel` which indicates the end of the title and where
// the colon is expected to be added after the aligning.
// Example:
/*
input: [
"title:foo",
"titleTwo:foo",
"titleTree:foo",
]
result: [
"title : foo",
"titleTwo : foo",
"titleTree : foo",
]
*/
func alignTitles(lines []string, longestLineLen int) {
for i, line := range lines {
sentinelIndex := strings.Index(line, endOfTitleSentinel)
// calculate the difference between the longest line to each line ending. It's 0 for the longest
gapToFill := strings.Repeat(" ", longestLineLen-sentinelIndex)
lines[i] = fmt.Sprintf("%s%s\t: %s", line[:sentinelIndex], gapToFill, line[sentinelIndex+1:])
}
}
// getCommandsDetails produces the default help - commands - description for any command in the form of `cmd : notes`.
func getCommandsDetails(cmd *cobra.Command) (result string) {
childrenCommands := cmd.Commands()
if len(childrenCommands) == 0 {
return ""
}
// stores the longes line len
max := 0
var lines []string
for _, childCommand := range childrenCommands {
if !childCommand.IsAvailableCommand() {
continue
}
commandName := fmt.Sprintf(" %s", childCommand.Name())
commandNameLen := len(commandName)
if commandNameLen > max {
max = commandNameLen
}
lines = append(lines,
fmt.Sprintf("%s%s%s", commandName, endOfTitleSentinel, childCommand.Short))
}
alignTitles(lines, max)
return fmt.Sprintf("%s\n", strings.Join(lines, "\n"))
}
// formatHelpNote provides the expected format in description notes using `•`.
func formatHelpNote(note string) string {
return fmt.Sprintf(" • %s", note)
}
// getPreFooter automatically adds a message to any command containing sub-commands about how to get help for subcommands.
func getPreFooter(c *cobra.Command) string {
if !c.HasSubCommands() {
return ""
}
return fmt.Sprintf("Use %s to view examples and more information about a specific command.\n\n",
fmt.Sprintf("%s %s %s",
output.WithHighLightFormat("%s", c.CommandPath()),
output.WithWarningFormat("[command]"),
output.WithHighLightFormat("--help"),
))
}
// generateCmdHelpDescription construct a help text block from a title and description notes.
func generateCmdHelpDescription(title string, notes []string) string {
var note string
if len(notes) > 0 {
note = fmt.Sprintf("%s\n\n", strings.Join(notes, "\n"))
}
return fmt.Sprintf("%s\n\n%s", title, note)
}
// generateCmdHelpSamplesBlock converts the samples within the input `samples` to a help text block describing each sample
// title and the command to run it.
func generateCmdHelpSamplesBlock(samples map[string]string) string {
SamplesCount := len(samples)
if SamplesCount == 0 {
return ""
}
var lines []string
for title, command := range samples {
lines = append(lines, fmt.Sprintf(" %s\n %s", title, command))
}
// sorting lines to keep a deterministic output, as map[string]string is not ordered
slices.Sort(lines)
return fmt.Sprintf("%s\n%s\n",
output.WithBold(output.WithUnderline("Examples")),
strings.Join(lines, "\n\n"),
)
}