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 !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}
515