blob: 068fce15255eefb80f1a00158dc62b4760fac93d [file] [log] [blame]
Naveen Sampath04696f72022-06-13 15:19:14 +05301// 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 !grp.showInHelp() {
76 return
77 }
78 if c != prevcmd {
79 for _, arg := range c.args {
80 ret.updateLen(arg.Name, c != p.Command)
81 }
82 }
83
84 for _, info := range grp.options {
85 if !info.showInHelp() {
86 continue
87 }
88
89 if info.ShortName != 0 {
90 ret.hasShort = true
91 }
92
93 if len(info.ValueName) > 0 {
94 ret.hasValueName = true
95 }
96
97 l := info.LongNameWithNamespace() + info.ValueName
98
99 if len(info.Choices) != 0 {
100 l += "[" + strings.Join(info.Choices, "|") + "]"
101 }
102
103 ret.updateLen(l, c != p.Command)
104 }
105 })
106
107 return ret
108}
109
110func wrapText(s string, l int, prefix string) string {
111 var ret string
112
113 if l < 10 {
114 l = 10
115 }
116
117 // Basic text wrapping of s at spaces to fit in l
118 lines := strings.Split(s, "\n")
119
120 for _, line := range lines {
121 var retline string
122
123 line = strings.TrimSpace(line)
124
125 for len(line) > l {
126 // Try to split on space
127 suffix := ""
128
129 pos := strings.LastIndex(line[:l], " ")
130
131 if pos < 0 {
132 pos = l - 1
133 suffix = "-\n"
134 }
135
136 if len(retline) != 0 {
137 retline += "\n" + prefix
138 }
139
140 retline += strings.TrimSpace(line[:pos]) + suffix
141 line = strings.TrimSpace(line[pos:])
142 }
143
144 if len(line) > 0 {
145 if len(retline) != 0 {
146 retline += "\n" + prefix
147 }
148
149 retline += line
150 }
151
152 if len(ret) > 0 {
153 ret += "\n"
154
155 if len(retline) > 0 {
156 ret += prefix
157 }
158 }
159
160 ret += retline
161 }
162
163 return ret
164}
165
166func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alignmentInfo) {
167 line := &bytes.Buffer{}
168
169 prefix := paddingBeforeOption
170
171 if info.indent {
172 prefix += 4
173 }
174
175 if option.Hidden {
176 return
177 }
178
179 line.WriteString(strings.Repeat(" ", prefix))
180
181 if option.ShortName != 0 {
182 line.WriteRune(defaultShortOptDelimiter)
183 line.WriteRune(option.ShortName)
184 } else if info.hasShort {
185 line.WriteString(" ")
186 }
187
188 descstart := info.descriptionStart() + paddingBeforeOption
189
190 if len(option.LongName) > 0 {
191 if option.ShortName != 0 {
192 line.WriteString(", ")
193 } else if info.hasShort {
194 line.WriteString(" ")
195 }
196
197 line.WriteString(defaultLongOptDelimiter)
198 line.WriteString(option.LongNameWithNamespace())
199 }
200
201 if option.canArgument() {
202 line.WriteRune(defaultNameArgDelimiter)
203
204 if len(option.ValueName) > 0 {
205 line.WriteString(option.ValueName)
206 }
207
208 if len(option.Choices) > 0 {
209 line.WriteString("[" + strings.Join(option.Choices, "|") + "]")
210 }
211 }
212
213 written := line.Len()
214 line.WriteTo(writer)
215
216 if option.Description != "" {
217 dw := descstart - written
218 writer.WriteString(strings.Repeat(" ", dw))
219
220 var def string
221
222 if len(option.DefaultMask) != 0 {
223 if option.DefaultMask != "-" {
224 def = option.DefaultMask
225 }
226 } else {
227 def = option.defaultLiteral
228 }
229
230 var envDef string
231 if option.EnvKeyWithNamespace() != "" {
232 var envPrintable string
233 if runtime.GOOS == "windows" {
234 envPrintable = "%" + option.EnvKeyWithNamespace() + "%"
235 } else {
236 envPrintable = "$" + option.EnvKeyWithNamespace()
237 }
238 envDef = fmt.Sprintf(" [%s]", envPrintable)
239 }
240
241 var desc string
242
243 if def != "" {
244 desc = fmt.Sprintf("%s (default: %v)%s", option.Description, def, envDef)
245 } else {
246 desc = option.Description + envDef
247 }
248
249 writer.WriteString(wrapText(desc,
250 info.terminalColumns-descstart,
251 strings.Repeat(" ", descstart)))
252 }
253
254 writer.WriteString("\n")
255}
256
257func maxCommandLength(s []*Command) int {
258 if len(s) == 0 {
259 return 0
260 }
261
262 ret := len(s[0].Name)
263
264 for _, v := range s[1:] {
265 l := len(v.Name)
266
267 if l > ret {
268 ret = l
269 }
270 }
271
272 return ret
273}
274
275// WriteHelp writes a help message containing all the possible options and
276// their descriptions to the provided writer. Note that the HelpFlag parser
277// option provides a convenient way to add a -h/--help option group to the
278// command line parser which will automatically show the help messages using
279// this method.
280func (p *Parser) WriteHelp(writer io.Writer) {
281 if writer == nil {
282 return
283 }
284
285 wr := bufio.NewWriter(writer)
286 aligninfo := p.getAlignmentInfo()
287
288 cmd := p.Command
289
290 for cmd.Active != nil {
291 cmd = cmd.Active
292 }
293
294 if p.Name != "" {
295 wr.WriteString("Usage:\n")
296 wr.WriteString(" ")
297
298 allcmd := p.Command
299
300 for allcmd != nil {
301 var usage string
302
303 if allcmd == p.Command {
304 if len(p.Usage) != 0 {
305 usage = p.Usage
306 } else if p.Options&HelpFlag != 0 {
307 usage = "[OPTIONS]"
308 }
309 } else if us, ok := allcmd.data.(Usage); ok {
310 usage = us.Usage()
311 } else if allcmd.hasHelpOptions() {
312 usage = fmt.Sprintf("[%s-OPTIONS]", allcmd.Name)
313 }
314
315 if len(usage) != 0 {
316 fmt.Fprintf(wr, " %s %s", allcmd.Name, usage)
317 } else {
318 fmt.Fprintf(wr, " %s", allcmd.Name)
319 }
320
321 if len(allcmd.args) > 0 {
322 fmt.Fprintf(wr, " ")
323 }
324
325 for i, arg := range allcmd.args {
326 if i != 0 {
327 fmt.Fprintf(wr, " ")
328 }
329
330 name := arg.Name
331
332 if arg.isRemaining() {
333 name = name + "..."
334 }
335
336 if !allcmd.ArgsRequired {
337 fmt.Fprintf(wr, "[%s]", name)
338 } else {
339 fmt.Fprintf(wr, "%s", name)
340 }
341 }
342
343 if allcmd.Active == nil && len(allcmd.commands) > 0 {
344 var co, cc string
345
346 if allcmd.SubcommandsOptional {
347 co, cc = "[", "]"
348 } else {
349 co, cc = "<", ">"
350 }
351
352 visibleCommands := allcmd.visibleCommands()
353
354 if len(visibleCommands) > 3 {
355 fmt.Fprintf(wr, " %scommand%s", co, cc)
356 } else {
357 subcommands := allcmd.sortedVisibleCommands()
358 names := make([]string, len(subcommands))
359
360 for i, subc := range subcommands {
361 names[i] = subc.Name
362 }
363
364 fmt.Fprintf(wr, " %s%s%s", co, strings.Join(names, " | "), cc)
365 }
366 }
367
368 allcmd = allcmd.Active
369 }
370
371 fmt.Fprintln(wr)
372
373 if len(cmd.LongDescription) != 0 {
374 fmt.Fprintln(wr)
375
376 t := wrapText(cmd.LongDescription,
377 aligninfo.terminalColumns,
378 "")
379
380 fmt.Fprintln(wr, t)
381 }
382 }
383
384 c := p.Command
385
386 for c != nil {
387 printcmd := c != p.Command
388
389 c.eachGroup(func(grp *Group) {
390 first := true
391
392 // Skip built-in help group for all commands except the top-level
393 // parser
394 if grp.Hidden || (grp.isBuiltinHelp && c != p.Command) {
395 return
396 }
397
398 for _, info := range grp.options {
399 if !info.showInHelp() {
400 continue
401 }
402
403 if printcmd {
404 fmt.Fprintf(wr, "\n[%s command options]\n", c.Name)
405 aligninfo.indent = true
406 printcmd = false
407 }
408
409 if first && cmd.Group != grp {
410 fmt.Fprintln(wr)
411
412 if aligninfo.indent {
413 wr.WriteString(" ")
414 }
415
416 fmt.Fprintf(wr, "%s:\n", grp.ShortDescription)
417 first = false
418 }
419
420 p.writeHelpOption(wr, info, aligninfo)
421 }
422 })
423
424 var args []*Arg
425 for _, arg := range c.args {
426 if arg.Description != "" {
427 args = append(args, arg)
428 }
429 }
430
431 if len(args) > 0 {
432 if c == p.Command {
433 fmt.Fprintf(wr, "\nArguments:\n")
434 } else {
435 fmt.Fprintf(wr, "\n[%s command arguments]\n", c.Name)
436 }
437
438 descStart := aligninfo.descriptionStart() + paddingBeforeOption
439
440 for _, arg := range args {
441 argPrefix := strings.Repeat(" ", paddingBeforeOption)
442 argPrefix += arg.Name
443
444 if len(arg.Description) > 0 {
445 argPrefix += ":"
446 wr.WriteString(argPrefix)
447
448 // Space between "arg:" and the description start
449 descPadding := strings.Repeat(" ", descStart-len(argPrefix))
450 // How much space the description gets before wrapping
451 descWidth := aligninfo.terminalColumns - 1 - descStart
452 // Whitespace to which we can indent new description lines
453 descPrefix := strings.Repeat(" ", descStart)
454
455 wr.WriteString(descPadding)
456 wr.WriteString(wrapText(arg.Description, descWidth, descPrefix))
457 } else {
458 wr.WriteString(argPrefix)
459 }
460
461 fmt.Fprintln(wr)
462 }
463 }
464
465 c = c.Active
466 }
467
468 scommands := cmd.sortedVisibleCommands()
469
470 if len(scommands) > 0 {
471 maxnamelen := maxCommandLength(scommands)
472
473 fmt.Fprintln(wr)
474 fmt.Fprintln(wr, "Available commands:")
475
476 for _, c := range scommands {
477 fmt.Fprintf(wr, " %s", c.Name)
478
479 if len(c.ShortDescription) > 0 {
480 pad := strings.Repeat(" ", maxnamelen-len(c.Name))
481 fmt.Fprintf(wr, "%s %s", pad, c.ShortDescription)
482
483 if len(c.Aliases) > 0 {
484 fmt.Fprintf(wr, " (aliases: %s)", strings.Join(c.Aliases, ", "))
485 }
486
487 }
488
489 fmt.Fprintln(wr)
490 }
491 }
492
493 wr.Flush()
494}
495
496// WroteHelp is a helper to test the error from ParseArgs() to
497// determine if the help message was written. It is safe to
498// call without first checking that error is nil.
499func WroteHelp(err error) bool {
500 if err == nil { // No error
501 return false
502 }
503
504 flagError, ok := err.(*Error)
505 if !ok { // Not a go-flag error
506 return false
507 }
508
509 if flagError.Type != ErrHelp { // Did not print the help message
510 return false
511 }
512
513 return true
514}