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