blob: d38030500e8d722f8950b38766c683c6c8b0781d [file] [log] [blame]
Scott Bakerbdb962b2020-04-03 10:53:36 -07001// Copyright 2012 Jesse van den Kieboom. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package flags
6
7import (
8 "bufio"
9 "bytes"
10 "fmt"
11 "io"
12 "runtime"
13 "strings"
14 "unicode/utf8"
15)
16
17type alignmentInfo struct {
18 maxLongLen int
19 hasShort bool
20 hasValueName bool
21 terminalColumns int
22 indent bool
23}
24
25const (
26 paddingBeforeOption = 2
27 distanceBetweenOptionAndDescription = 2
28)
29
30func (a *alignmentInfo) descriptionStart() int {
31 ret := a.maxLongLen + distanceBetweenOptionAndDescription
32
33 if a.hasShort {
34 ret += 2
35 }
36
37 if a.maxLongLen > 0 {
38 ret += 4
39 }
40
41 if a.hasValueName {
42 ret += 3
43 }
44
45 return ret
46}
47
48func (a *alignmentInfo) updateLen(name string, indent bool) {
49 l := utf8.RuneCountInString(name)
50
51 if indent {
52 l = l + 4
53 }
54
55 if l > a.maxLongLen {
56 a.maxLongLen = l
57 }
58}
59
60func (p *Parser) getAlignmentInfo() alignmentInfo {
61 ret := alignmentInfo{
62 maxLongLen: 0,
63 hasShort: false,
64 hasValueName: false,
65 terminalColumns: getTerminalColumns(),
66 }
67
68 if ret.terminalColumns <= 0 {
69 ret.terminalColumns = 80
70 }
71
72 var prevcmd *Command
73
74 p.eachActiveGroup(func(c *Command, grp *Group) {
75 if c != prevcmd {
76 for _, arg := range c.args {
77 ret.updateLen(arg.Name, c != p.Command)
78 }
79 }
80
81 for _, info := range grp.options {
82 if !info.canCli() {
83 continue
84 }
85
86 if info.ShortName != 0 {
87 ret.hasShort = true
88 }
89
90 if len(info.ValueName) > 0 {
91 ret.hasValueName = true
92 }
93
94 l := info.LongNameWithNamespace() + info.ValueName
95
96 if len(info.Choices) != 0 {
97 l += "[" + strings.Join(info.Choices, "|") + "]"
98 }
99
100 ret.updateLen(l, c != p.Command)
101 }
102 })
103
104 return ret
105}
106
107func wrapText(s string, l int, prefix string) string {
108 var ret string
109
110 if l < 10 {
111 l = 10
112 }
113
114 // Basic text wrapping of s at spaces to fit in l
115 lines := strings.Split(s, "\n")
116
117 for _, line := range lines {
118 var retline string
119
120 line = strings.TrimSpace(line)
121
122 for len(line) > l {
123 // Try to split on space
124 suffix := ""
125
126 pos := strings.LastIndex(line[:l], " ")
127
128 if pos < 0 {
129 pos = l - 1
130 suffix = "-\n"
131 }
132
133 if len(retline) != 0 {
134 retline += "\n" + prefix
135 }
136
137 retline += strings.TrimSpace(line[:pos]) + suffix
138 line = strings.TrimSpace(line[pos:])
139 }
140
141 if len(line) > 0 {
142 if len(retline) != 0 {
143 retline += "\n" + prefix
144 }
145
146 retline += line
147 }
148
149 if len(ret) > 0 {
150 ret += "\n"
151
152 if len(retline) > 0 {
153 ret += prefix
154 }
155 }
156
157 ret += retline
158 }
159
160 return ret
161}
162
163func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alignmentInfo) {
164 line := &bytes.Buffer{}
165
166 prefix := paddingBeforeOption
167
168 if info.indent {
169 prefix += 4
170 }
171
172 if option.Hidden {
173 return
174 }
175
176 line.WriteString(strings.Repeat(" ", prefix))
177
178 if option.ShortName != 0 {
179 line.WriteRune(defaultShortOptDelimiter)
180 line.WriteRune(option.ShortName)
181 } else if info.hasShort {
182 line.WriteString(" ")
183 }
184
185 descstart := info.descriptionStart() + paddingBeforeOption
186
187 if len(option.LongName) > 0 {
188 if option.ShortName != 0 {
189 line.WriteString(", ")
190 } else if info.hasShort {
191 line.WriteString(" ")
192 }
193
194 line.WriteString(defaultLongOptDelimiter)
195 line.WriteString(option.LongNameWithNamespace())
196 }
197
198 if option.canArgument() {
199 line.WriteRune(defaultNameArgDelimiter)
200
201 if len(option.ValueName) > 0 {
202 line.WriteString(option.ValueName)
203 }
204
205 if len(option.Choices) > 0 {
206 line.WriteString("[" + strings.Join(option.Choices, "|") + "]")
207 }
208 }
209
210 written := line.Len()
211 line.WriteTo(writer)
212
213 if option.Description != "" {
214 dw := descstart - written
215 writer.WriteString(strings.Repeat(" ", dw))
216
217 var def string
218
219 if len(option.DefaultMask) != 0 {
220 if option.DefaultMask != "-" {
221 def = option.DefaultMask
222 }
223 } else {
224 def = option.defaultLiteral
225 }
226
227 var envDef string
228 if option.EnvDefaultKey != "" {
229 var envPrintable string
230 if runtime.GOOS == "windows" {
231 envPrintable = "%" + option.EnvDefaultKey + "%"
232 } else {
233 envPrintable = "$" + option.EnvDefaultKey
234 }
235 envDef = fmt.Sprintf(" [%s]", envPrintable)
236 }
237
238 var desc string
239
240 if def != "" {
241 desc = fmt.Sprintf("%s (default: %v)%s", option.Description, def, envDef)
242 } else {
243 desc = option.Description + envDef
244 }
245
246 writer.WriteString(wrapText(desc,
247 info.terminalColumns-descstart,
248 strings.Repeat(" ", descstart)))
249 }
250
251 writer.WriteString("\n")
252}
253
254func maxCommandLength(s []*Command) int {
255 if len(s) == 0 {
256 return 0
257 }
258
259 ret := len(s[0].Name)
260
261 for _, v := range s[1:] {
262 l := len(v.Name)
263
264 if l > ret {
265 ret = l
266 }
267 }
268
269 return ret
270}
271
272// WriteHelp writes a help message containing all the possible options and
273// their descriptions to the provided writer. Note that the HelpFlag parser
274// option provides a convenient way to add a -h/--help option group to the
275// command line parser which will automatically show the help messages using
276// this method.
277func (p *Parser) WriteHelp(writer io.Writer) {
278 if writer == nil {
279 return
280 }
281
282 wr := bufio.NewWriter(writer)
283 aligninfo := p.getAlignmentInfo()
284
285 cmd := p.Command
286
287 for cmd.Active != nil {
288 cmd = cmd.Active
289 }
290
291 if p.Name != "" {
292 wr.WriteString("Usage:\n")
293 wr.WriteString(" ")
294
295 allcmd := p.Command
296
297 for allcmd != nil {
298 var usage string
299
300 if allcmd == p.Command {
301 if len(p.Usage) != 0 {
302 usage = p.Usage
303 } else if p.Options&HelpFlag != 0 {
304 usage = "[OPTIONS]"
305 }
306 } else if us, ok := allcmd.data.(Usage); ok {
307 usage = us.Usage()
308 } else if allcmd.hasCliOptions() {
309 usage = fmt.Sprintf("[%s-OPTIONS]", allcmd.Name)
310 }
311
312 if len(usage) != 0 {
313 fmt.Fprintf(wr, " %s %s", allcmd.Name, usage)
314 } else {
315 fmt.Fprintf(wr, " %s", allcmd.Name)
316 }
317
318 if len(allcmd.args) > 0 {
319 fmt.Fprintf(wr, " ")
320 }
321
322 for i, arg := range allcmd.args {
323 if i != 0 {
324 fmt.Fprintf(wr, " ")
325 }
326
327 name := arg.Name
328
329 if arg.isRemaining() {
330 name = name + "..."
331 }
332
333 if !allcmd.ArgsRequired {
334 fmt.Fprintf(wr, "[%s]", name)
335 } else {
336 fmt.Fprintf(wr, "%s", name)
337 }
338 }
339
340 if allcmd.Active == nil && len(allcmd.commands) > 0 {
341 var co, cc string
342
343 if allcmd.SubcommandsOptional {
344 co, cc = "[", "]"
345 } else {
346 co, cc = "<", ">"
347 }
348
349 visibleCommands := allcmd.visibleCommands()
350
351 if len(visibleCommands) > 3 {
352 fmt.Fprintf(wr, " %scommand%s", co, cc)
353 } else {
354 subcommands := allcmd.sortedVisibleCommands()
355 names := make([]string, len(subcommands))
356
357 for i, subc := range subcommands {
358 names[i] = subc.Name
359 }
360
361 fmt.Fprintf(wr, " %s%s%s", co, strings.Join(names, " | "), cc)
362 }
363 }
364
365 allcmd = allcmd.Active
366 }
367
368 fmt.Fprintln(wr)
369
370 if len(cmd.LongDescription) != 0 {
371 fmt.Fprintln(wr)
372
373 t := wrapText(cmd.LongDescription,
374 aligninfo.terminalColumns,
375 "")
376
377 fmt.Fprintln(wr, t)
378 }
379 }
380
381 c := p.Command
382
383 for c != nil {
384 printcmd := c != p.Command
385
386 c.eachGroup(func(grp *Group) {
387 first := true
388
389 // Skip built-in help group for all commands except the top-level
390 // parser
391 if grp.Hidden || (grp.isBuiltinHelp && c != p.Command) {
392 return
393 }
394
395 for _, info := range grp.options {
396 if !info.canCli() || info.Hidden {
397 continue
398 }
399
400 if printcmd {
401 fmt.Fprintf(wr, "\n[%s command options]\n", c.Name)
402 aligninfo.indent = true
403 printcmd = false
404 }
405
406 if first && cmd.Group != grp {
407 fmt.Fprintln(wr)
408
409 if aligninfo.indent {
410 wr.WriteString(" ")
411 }
412
413 fmt.Fprintf(wr, "%s:\n", grp.ShortDescription)
414 first = false
415 }
416
417 p.writeHelpOption(wr, info, aligninfo)
418 }
419 })
420
421 var args []*Arg
422 for _, arg := range c.args {
423 if arg.Description != "" {
424 args = append(args, arg)
425 }
426 }
427
428 if len(args) > 0 {
429 if c == p.Command {
430 fmt.Fprintf(wr, "\nArguments:\n")
431 } else {
432 fmt.Fprintf(wr, "\n[%s command arguments]\n", c.Name)
433 }
434
435 descStart := aligninfo.descriptionStart() + paddingBeforeOption
436
437 for _, arg := range args {
438 argPrefix := strings.Repeat(" ", paddingBeforeOption)
439 argPrefix += arg.Name
440
441 if len(arg.Description) > 0 {
442 argPrefix += ":"
443 wr.WriteString(argPrefix)
444
445 // Space between "arg:" and the description start
446 descPadding := strings.Repeat(" ", descStart-len(argPrefix))
447 // How much space the description gets before wrapping
448 descWidth := aligninfo.terminalColumns - 1 - descStart
449 // Whitespace to which we can indent new description lines
450 descPrefix := strings.Repeat(" ", descStart)
451
452 wr.WriteString(descPadding)
453 wr.WriteString(wrapText(arg.Description, descWidth, descPrefix))
454 } else {
455 wr.WriteString(argPrefix)
456 }
457
458 fmt.Fprintln(wr)
459 }
460 }
461
462 c = c.Active
463 }
464
465 scommands := cmd.sortedVisibleCommands()
466
467 if len(scommands) > 0 {
468 maxnamelen := maxCommandLength(scommands)
469
470 fmt.Fprintln(wr)
471 fmt.Fprintln(wr, "Available commands:")
472
473 for _, c := range scommands {
474 fmt.Fprintf(wr, " %s", c.Name)
475
476 if len(c.ShortDescription) > 0 {
477 pad := strings.Repeat(" ", maxnamelen-len(c.Name))
478 fmt.Fprintf(wr, "%s %s", pad, c.ShortDescription)
479
480 if len(c.Aliases) > 0 {
481 fmt.Fprintf(wr, " (aliases: %s)", strings.Join(c.Aliases, ", "))
482 }
483
484 }
485
486 fmt.Fprintln(wr)
487 }
488 }
489
490 wr.Flush()
491}