blob: 8fd324443a0d9630444b8d11a9715aa60aceb962 [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) {
Naveen Sampath04696f72022-06-13 15:19:14 +053075 if c != prevcmd {
76 for _, arg := range c.args {
77 ret.updateLen(arg.Name, c != p.Command)
78 }
Abhay Kumarfe505f22025-11-10 14:16:31 +000079 prevcmd = c
Naveen Sampath04696f72022-06-13 15:19:14 +053080 }
Abhay Kumarfe505f22025-11-10 14:16:31 +000081 if !grp.showInHelp() {
82 return
83 }
Naveen Sampath04696f72022-06-13 15:19:14 +053084 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 {
Abhay Kumarfe505f22025-11-10 14:16:31 +0000337 if arg.Required > 0 {
338 fmt.Fprintf(wr, "%s", name)
339 } else {
340 fmt.Fprintf(wr, "[%s]", name)
341 }
Naveen Sampath04696f72022-06-13 15:19:14 +0530342 } else {
343 fmt.Fprintf(wr, "%s", name)
344 }
345 }
346
347 if allcmd.Active == nil && len(allcmd.commands) > 0 {
348 var co, cc string
349
350 if allcmd.SubcommandsOptional {
351 co, cc = "[", "]"
352 } else {
353 co, cc = "<", ">"
354 }
355
356 visibleCommands := allcmd.visibleCommands()
357
358 if len(visibleCommands) > 3 {
359 fmt.Fprintf(wr, " %scommand%s", co, cc)
360 } else {
361 subcommands := allcmd.sortedVisibleCommands()
362 names := make([]string, len(subcommands))
363
364 for i, subc := range subcommands {
365 names[i] = subc.Name
366 }
367
368 fmt.Fprintf(wr, " %s%s%s", co, strings.Join(names, " | "), cc)
369 }
370 }
371
372 allcmd = allcmd.Active
373 }
374
375 fmt.Fprintln(wr)
376
377 if len(cmd.LongDescription) != 0 {
378 fmt.Fprintln(wr)
379
380 t := wrapText(cmd.LongDescription,
381 aligninfo.terminalColumns,
382 "")
383
384 fmt.Fprintln(wr, t)
385 }
386 }
387
388 c := p.Command
389
390 for c != nil {
391 printcmd := c != p.Command
392
393 c.eachGroup(func(grp *Group) {
394 first := true
395
396 // Skip built-in help group for all commands except the top-level
397 // parser
398 if grp.Hidden || (grp.isBuiltinHelp && c != p.Command) {
399 return
400 }
401
402 for _, info := range grp.options {
403 if !info.showInHelp() {
404 continue
405 }
406
407 if printcmd {
408 fmt.Fprintf(wr, "\n[%s command options]\n", c.Name)
409 aligninfo.indent = true
410 printcmd = false
411 }
412
413 if first && cmd.Group != grp {
414 fmt.Fprintln(wr)
415
416 if aligninfo.indent {
417 wr.WriteString(" ")
418 }
419
420 fmt.Fprintf(wr, "%s:\n", grp.ShortDescription)
421 first = false
422 }
423
424 p.writeHelpOption(wr, info, aligninfo)
425 }
426 })
427
428 var args []*Arg
429 for _, arg := range c.args {
430 if arg.Description != "" {
431 args = append(args, arg)
432 }
433 }
434
435 if len(args) > 0 {
436 if c == p.Command {
437 fmt.Fprintf(wr, "\nArguments:\n")
438 } else {
439 fmt.Fprintf(wr, "\n[%s command arguments]\n", c.Name)
440 }
441
442 descStart := aligninfo.descriptionStart() + paddingBeforeOption
443
444 for _, arg := range args {
445 argPrefix := strings.Repeat(" ", paddingBeforeOption)
446 argPrefix += arg.Name
447
448 if len(arg.Description) > 0 {
449 argPrefix += ":"
450 wr.WriteString(argPrefix)
451
452 // Space between "arg:" and the description start
453 descPadding := strings.Repeat(" ", descStart-len(argPrefix))
454 // How much space the description gets before wrapping
455 descWidth := aligninfo.terminalColumns - 1 - descStart
456 // Whitespace to which we can indent new description lines
457 descPrefix := strings.Repeat(" ", descStart)
458
459 wr.WriteString(descPadding)
460 wr.WriteString(wrapText(arg.Description, descWidth, descPrefix))
461 } else {
462 wr.WriteString(argPrefix)
463 }
464
465 fmt.Fprintln(wr)
466 }
467 }
468
469 c = c.Active
470 }
471
472 scommands := cmd.sortedVisibleCommands()
473
474 if len(scommands) > 0 {
475 maxnamelen := maxCommandLength(scommands)
476
477 fmt.Fprintln(wr)
478 fmt.Fprintln(wr, "Available commands:")
479
480 for _, c := range scommands {
481 fmt.Fprintf(wr, " %s", c.Name)
482
483 if len(c.ShortDescription) > 0 {
484 pad := strings.Repeat(" ", maxnamelen-len(c.Name))
485 fmt.Fprintf(wr, "%s %s", pad, c.ShortDescription)
486
487 if len(c.Aliases) > 0 {
488 fmt.Fprintf(wr, " (aliases: %s)", strings.Join(c.Aliases, ", "))
489 }
490
491 }
492
493 fmt.Fprintln(wr)
494 }
495 }
496
497 wr.Flush()
498}
499
500// WroteHelp is a helper to test the error from ParseArgs() to
501// determine if the help message was written. It is safe to
502// call without first checking that error is nil.
503func WroteHelp(err error) bool {
504 if err == nil { // No error
505 return false
506 }
507
508 flagError, ok := err.(*Error)
509 if !ok { // Not a go-flag error
510 return false
511 }
512
513 if flagError.Type != ErrHelp { // Did not print the help message
514 return false
515 }
516
517 return true
518}