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 {
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}
492