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	"reflect"
13	"runtime"
14	"strings"
15	"unicode/utf8"
16)
17
18type alignmentInfo struct {
19	maxLongLen      int
20	hasShort        bool
21	hasValueName    bool
22	terminalColumns int
23	indent          bool
24}
25
26const (
27	paddingBeforeOption                 = 2
28	distanceBetweenOptionAndDescription = 2
29)
30
31func (a *alignmentInfo) descriptionStart() int {
32	ret := a.maxLongLen + distanceBetweenOptionAndDescription
33
34	if a.hasShort {
35		ret += 2
36	}
37
38	if a.maxLongLen > 0 {
39		ret += 4
40	}
41
42	if a.hasValueName {
43		ret += 3
44	}
45
46	return ret
47}
48
49func (a *alignmentInfo) updateLen(name string, indent bool) {
50	l := utf8.RuneCountInString(name)
51
52	if indent {
53		l = l + 4
54	}
55
56	if l > a.maxLongLen {
57		a.maxLongLen = l
58	}
59}
60
61func (p *Parser) getAlignmentInfo() alignmentInfo {
62	ret := alignmentInfo{
63		maxLongLen:      0,
64		hasShort:        false,
65		hasValueName:    false,
66		terminalColumns: getTerminalColumns(),
67	}
68
69	if ret.terminalColumns <= 0 {
70		ret.terminalColumns = 80
71	}
72
73	var prevcmd *Command
74
75	p.eachActiveGroup(func(c *Command, grp *Group) {
76		if c != prevcmd {
77			for _, arg := range c.args {
78				ret.updateLen(arg.Name, c != p.Command)
79			}
80		}
81
82		for _, info := range grp.options {
83			if !info.canCli() {
84				continue
85			}
86
87			if info.ShortName != 0 {
88				ret.hasShort = true
89			}
90
91			if len(info.ValueName) > 0 {
92				ret.hasValueName = true
93			}
94
95			ret.updateLen(info.LongNameWithNamespace()+info.ValueName, c != p.Command)
96		}
97	})
98
99	return ret
100}
101
102func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alignmentInfo) {
103	line := &bytes.Buffer{}
104
105	prefix := paddingBeforeOption
106
107	if info.indent {
108		prefix += 4
109	}
110
111	line.WriteString(strings.Repeat(" ", prefix))
112
113	if option.ShortName != 0 {
114		line.WriteRune(defaultShortOptDelimiter)
115		line.WriteRune(option.ShortName)
116	} else if info.hasShort {
117		line.WriteString("  ")
118	}
119
120	descstart := info.descriptionStart() + paddingBeforeOption
121
122	if len(option.LongName) > 0 {
123		if option.ShortName != 0 {
124			line.WriteString(", ")
125		} else if info.hasShort {
126			line.WriteString("  ")
127		}
128
129		line.WriteString(defaultLongOptDelimiter)
130		line.WriteString(option.LongNameWithNamespace())
131	}
132
133	if option.canArgument() {
134		line.WriteRune(defaultNameArgDelimiter)
135
136		if len(option.ValueName) > 0 {
137			line.WriteString(option.ValueName)
138		}
139	}
140
141	written := line.Len()
142	line.WriteTo(writer)
143
144	if option.Description != "" {
145		dw := descstart - written
146		writer.WriteString(strings.Repeat(" ", dw))
147
148		def := ""
149		defs := option.Default
150
151		if len(option.DefaultMask) != 0 {
152			if option.DefaultMask != "-" {
153				def = option.DefaultMask
154			}
155		} else if len(defs) == 0 && option.canArgument() {
156			var showdef bool
157
158			switch option.field.Type.Kind() {
159			case reflect.Func, reflect.Ptr:
160				showdef = !option.value.IsNil()
161			case reflect.Slice, reflect.String, reflect.Array:
162				showdef = option.value.Len() > 0
163			case reflect.Map:
164				showdef = !option.value.IsNil() && option.value.Len() > 0
165			default:
166				zeroval := reflect.Zero(option.field.Type)
167				showdef = !reflect.DeepEqual(zeroval.Interface(), option.value.Interface())
168			}
169
170			if showdef {
171				def, _ = convertToString(option.value, option.tag)
172			}
173		} else if len(defs) != 0 {
174			l := len(defs) - 1
175
176			for i := 0; i < l; i++ {
177				def += quoteIfNeeded(defs[i]) + ", "
178			}
179
180			def += quoteIfNeeded(defs[l])
181		}
182
183		var envDef string
184		if option.EnvDefaultKey != "" {
185			var envPrintable string
186			if runtime.GOOS == "windows" {
187				envPrintable = "%" + option.EnvDefaultKey + "%"
188			} else {
189				envPrintable = "$" + option.EnvDefaultKey
190			}
191			envDef = fmt.Sprintf(" [%s]", envPrintable)
192		}
193
194		var desc string
195
196		if def != "" {
197			desc = fmt.Sprintf("%s (%v)%s", option.Description, def, envDef)
198		} else {
199			desc = option.Description + envDef
200		}
201
202		writer.WriteString(wrapText(desc,
203			info.terminalColumns-descstart,
204			strings.Repeat(" ", descstart)))
205	}
206
207	writer.WriteString("\n")
208}
209
210func maxCommandLength(s []*Command) int {
211	if len(s) == 0 {
212		return 0
213	}
214
215	ret := len(s[0].Name)
216
217	for _, v := range s[1:] {
218		l := len(v.Name)
219
220		if l > ret {
221			ret = l
222		}
223	}
224
225	return ret
226}
227
228// WriteHelp writes a help message containing all the possible options and
229// their descriptions to the provided writer. Note that the HelpFlag parser
230// option provides a convenient way to add a -h/--help option group to the
231// command line parser which will automatically show the help messages using
232// this method.
233func (p *Parser) WriteHelp(writer io.Writer) {
234	if writer == nil {
235		return
236	}
237
238	wr := bufio.NewWriter(writer)
239	aligninfo := p.getAlignmentInfo()
240
241	cmd := p.Command
242
243	for cmd.Active != nil {
244		cmd = cmd.Active
245	}
246
247	if p.Name != "" {
248		wr.WriteString("Usage:\n")
249		wr.WriteString(" ")
250
251		allcmd := p.Command
252
253		for allcmd != nil {
254			var usage string
255
256			if allcmd == p.Command {
257				if len(p.Usage) != 0 {
258					usage = p.Usage
259				} else if p.Options&HelpFlag != 0 {
260					usage = "[OPTIONS]"
261				}
262			} else if us, ok := allcmd.data.(Usage); ok {
263				usage = us.Usage()
264			} else if allcmd.hasCliOptions() {
265				usage = fmt.Sprintf("[%s-OPTIONS]", allcmd.Name)
266			}
267
268			if len(usage) != 0 {
269				fmt.Fprintf(wr, " %s %s", allcmd.Name, usage)
270			} else {
271				fmt.Fprintf(wr, " %s", allcmd.Name)
272			}
273
274			if len(allcmd.args) > 0 {
275				fmt.Fprintf(wr, " ")
276			}
277
278			for i, arg := range allcmd.args {
279				if i != 0 {
280					fmt.Fprintf(wr, " ")
281				}
282
283				name := arg.Name
284
285				if arg.isRemaining() {
286					name = name + "..."
287				}
288
289				if !allcmd.ArgsRequired {
290					fmt.Fprintf(wr, "[%s]", name)
291				} else {
292					fmt.Fprintf(wr, "%s", name)
293				}
294			}
295
296			if allcmd.Active == nil && len(allcmd.commands) > 0 {
297				var co, cc string
298
299				if allcmd.SubcommandsOptional {
300					co, cc = "[", "]"
301				} else {
302					co, cc = "<", ">"
303				}
304
305				if len(allcmd.commands) > 3 {
306					fmt.Fprintf(wr, " %scommand%s", co, cc)
307				} else {
308					subcommands := allcmd.sortedCommands()
309					names := make([]string, len(subcommands))
310
311					for i, subc := range subcommands {
312						names[i] = subc.Name
313					}
314
315					fmt.Fprintf(wr, " %s%s%s", co, strings.Join(names, " | "), cc)
316				}
317			}
318
319			allcmd = allcmd.Active
320		}
321
322		fmt.Fprintln(wr)
323
324		if len(cmd.LongDescription) != 0 {
325			fmt.Fprintln(wr)
326
327			t := wrapText(cmd.LongDescription,
328				aligninfo.terminalColumns,
329				"")
330
331			fmt.Fprintln(wr, t)
332		}
333	}
334
335	c := p.Command
336
337	for c != nil {
338		printcmd := c != p.Command
339
340		c.eachGroup(func(grp *Group) {
341			first := true
342
343			// Skip built-in help group for all commands except the top-level
344			// parser
345			if grp.isBuiltinHelp && c != p.Command {
346				return
347			}
348
349			for _, info := range grp.options {
350				if !info.canCli() {
351					continue
352				}
353
354				if printcmd {
355					fmt.Fprintf(wr, "\n[%s command options]\n", c.Name)
356					aligninfo.indent = true
357					printcmd = false
358				}
359
360				if first && cmd.Group != grp {
361					fmt.Fprintln(wr)
362
363					if aligninfo.indent {
364						wr.WriteString("    ")
365					}
366
367					fmt.Fprintf(wr, "%s:\n", grp.ShortDescription)
368					first = false
369				}
370
371				p.writeHelpOption(wr, info, aligninfo)
372			}
373		})
374
375		if len(c.args) > 0 {
376			if c == p.Command {
377				fmt.Fprintf(wr, "\nArguments:\n")
378			} else {
379				fmt.Fprintf(wr, "\n[%s command arguments]\n", c.Name)
380			}
381
382			maxlen := aligninfo.descriptionStart()
383
384			for _, arg := range c.args {
385				prefix := strings.Repeat(" ", paddingBeforeOption)
386				fmt.Fprintf(wr, "%s%s", prefix, arg.Name)
387
388				if len(arg.Description) > 0 {
389					align := strings.Repeat(" ", maxlen-len(arg.Name)-1)
390					fmt.Fprintf(wr, ":%s%s", align, arg.Description)
391				}
392
393				fmt.Fprintln(wr)
394			}
395		}
396
397		c = c.Active
398	}
399
400	scommands := cmd.sortedCommands()
401
402	if len(scommands) > 0 {
403		maxnamelen := maxCommandLength(scommands)
404
405		fmt.Fprintln(wr)
406		fmt.Fprintln(wr, "Available commands:")
407
408		for _, c := range scommands {
409			fmt.Fprintf(wr, "  %s", c.Name)
410
411			if len(c.ShortDescription) > 0 {
412				pad := strings.Repeat(" ", maxnamelen-len(c.Name))
413				fmt.Fprintf(wr, "%s  %s", pad, c.ShortDescription)
414
415				if len(c.Aliases) > 0 {
416					fmt.Fprintf(wr, " (aliases: %s)", strings.Join(c.Aliases, ", "))
417				}
418
419			}
420
421			fmt.Fprintln(wr)
422		}
423	}
424
425	wr.Flush()
426}
427