1// 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 && option.DefaultMask != "-" {
220			def = option.DefaultMask
221		} else {
222			def = option.defaultLiteral
223		}
224
225		var envDef string
226		if option.EnvDefaultKey != "" {
227			var envPrintable string
228			if runtime.GOOS == "windows" {
229				envPrintable = "%" + option.EnvDefaultKey + "%"
230			} else {
231				envPrintable = "$" + option.EnvDefaultKey
232			}
233			envDef = fmt.Sprintf(" [%s]", envPrintable)
234		}
235
236		var desc string
237
238		if def != "" {
239			desc = fmt.Sprintf("%s (default: %v)%s", option.Description, def, envDef)
240		} else {
241			desc = option.Description + envDef
242		}
243
244		writer.WriteString(wrapText(desc,
245			info.terminalColumns-descstart,
246			strings.Repeat(" ", descstart)))
247	}
248
249	writer.WriteString("\n")
250}
251
252func maxCommandLength(s []*Command) int {
253	if len(s) == 0 {
254		return 0
255	}
256
257	ret := len(s[0].Name)
258
259	for _, v := range s[1:] {
260		l := len(v.Name)
261
262		if l > ret {
263			ret = l
264		}
265	}
266
267	return ret
268}
269
270// WriteHelp writes a help message containing all the possible options and
271// their descriptions to the provided writer. Note that the HelpFlag parser
272// option provides a convenient way to add a -h/--help option group to the
273// command line parser which will automatically show the help messages using
274// this method.
275func (p *Parser) WriteHelp(writer io.Writer) {
276	if writer == nil {
277		return
278	}
279
280	wr := bufio.NewWriter(writer)
281	aligninfo := p.getAlignmentInfo()
282
283	cmd := p.Command
284
285	for cmd.Active != nil {
286		cmd = cmd.Active
287	}
288
289	if p.Name != "" {
290		wr.WriteString("Usage:\n")
291		wr.WriteString(" ")
292
293		allcmd := p.Command
294
295		for allcmd != nil {
296			var usage string
297
298			if allcmd == p.Command {
299				if len(p.Usage) != 0 {
300					usage = p.Usage
301				} else if p.Options&HelpFlag != 0 {
302					usage = "[OPTIONS]"
303				}
304			} else if us, ok := allcmd.data.(Usage); ok {
305				usage = us.Usage()
306			} else if allcmd.hasCliOptions() {
307				usage = fmt.Sprintf("[%s-OPTIONS]", allcmd.Name)
308			}
309
310			if len(usage) != 0 {
311				fmt.Fprintf(wr, " %s %s", allcmd.Name, usage)
312			} else {
313				fmt.Fprintf(wr, " %s", allcmd.Name)
314			}
315
316			if len(allcmd.args) > 0 {
317				fmt.Fprintf(wr, " ")
318			}
319
320			for i, arg := range allcmd.args {
321				if i != 0 {
322					fmt.Fprintf(wr, " ")
323				}
324
325				name := arg.Name
326
327				if arg.isRemaining() {
328					name = name + "..."
329				}
330
331				if !allcmd.ArgsRequired {
332					fmt.Fprintf(wr, "[%s]", name)
333				} else {
334					fmt.Fprintf(wr, "%s", name)
335				}
336			}
337
338			if allcmd.Active == nil && len(allcmd.commands) > 0 {
339				var co, cc string
340
341				if allcmd.SubcommandsOptional {
342					co, cc = "[", "]"
343				} else {
344					co, cc = "<", ">"
345				}
346
347				visibleCommands := allcmd.visibleCommands()
348
349				if len(visibleCommands) > 3 {
350					fmt.Fprintf(wr, " %scommand%s", co, cc)
351				} else {
352					subcommands := allcmd.sortedVisibleCommands()
353					names := make([]string, len(subcommands))
354
355					for i, subc := range subcommands {
356						names[i] = subc.Name
357					}
358
359					fmt.Fprintf(wr, " %s%s%s", co, strings.Join(names, " | "), cc)
360				}
361			}
362
363			allcmd = allcmd.Active
364		}
365
366		fmt.Fprintln(wr)
367
368		if len(cmd.LongDescription) != 0 {
369			fmt.Fprintln(wr)
370
371			t := wrapText(cmd.LongDescription,
372				aligninfo.terminalColumns,
373				"")
374
375			fmt.Fprintln(wr, t)
376		}
377	}
378
379	c := p.Command
380
381	for c != nil {
382		printcmd := c != p.Command
383
384		c.eachGroup(func(grp *Group) {
385			first := true
386
387			// Skip built-in help group for all commands except the top-level
388			// parser
389			if grp.Hidden || (grp.isBuiltinHelp && c != p.Command) {
390				return
391			}
392
393			for _, info := range grp.options {
394				if !info.canCli() || info.Hidden {
395					continue
396				}
397
398				if printcmd {
399					fmt.Fprintf(wr, "\n[%s command options]\n", c.Name)
400					aligninfo.indent = true
401					printcmd = false
402				}
403
404				if first && cmd.Group != grp {
405					fmt.Fprintln(wr)
406
407					if aligninfo.indent {
408						wr.WriteString("    ")
409					}
410
411					fmt.Fprintf(wr, "%s:\n", grp.ShortDescription)
412					first = false
413				}
414
415				p.writeHelpOption(wr, info, aligninfo)
416			}
417		})
418
419		var args []*Arg
420		for _, arg := range c.args {
421			if arg.Description != "" {
422				args = append(args, arg)
423			}
424		}
425
426		if len(args) > 0 {
427			if c == p.Command {
428				fmt.Fprintf(wr, "\nArguments:\n")
429			} else {
430				fmt.Fprintf(wr, "\n[%s command arguments]\n", c.Name)
431			}
432
433			descStart := aligninfo.descriptionStart() + paddingBeforeOption
434
435			for _, arg := range args {
436				argPrefix := strings.Repeat(" ", paddingBeforeOption)
437				argPrefix += arg.Name
438
439				if len(arg.Description) > 0 {
440					argPrefix += ":"
441					wr.WriteString(argPrefix)
442
443					// Space between "arg:" and the description start
444					descPadding := strings.Repeat(" ", descStart-len(argPrefix))
445					// How much space the description gets before wrapping
446					descWidth := aligninfo.terminalColumns - 1 - descStart
447					// Whitespace to which we can indent new description lines
448					descPrefix := strings.Repeat(" ", descStart)
449
450					wr.WriteString(descPadding)
451					wr.WriteString(wrapText(arg.Description, descWidth, descPrefix))
452				} else {
453					wr.WriteString(argPrefix)
454				}
455
456				fmt.Fprintln(wr)
457			}
458		}
459
460		c = c.Active
461	}
462
463	scommands := cmd.sortedVisibleCommands()
464
465	if len(scommands) > 0 {
466		maxnamelen := maxCommandLength(scommands)
467
468		fmt.Fprintln(wr)
469		fmt.Fprintln(wr, "Available commands:")
470
471		for _, c := range scommands {
472			fmt.Fprintf(wr, "  %s", c.Name)
473
474			if len(c.ShortDescription) > 0 {
475				pad := strings.Repeat(" ", maxnamelen-len(c.Name))
476				fmt.Fprintf(wr, "%s  %s", pad, c.ShortDescription)
477
478				if len(c.Aliases) > 0 {
479					fmt.Fprintf(wr, " (aliases: %s)", strings.Join(c.Aliases, ", "))
480				}
481
482			}
483
484			fmt.Fprintln(wr)
485		}
486	}
487
488	wr.Flush()
489}
490