1package cli
2
3import (
4	"flag"
5	"fmt"
6	"io/ioutil"
7	"reflect"
8	"regexp"
9	"runtime"
10	"strconv"
11	"strings"
12	"syscall"
13	"time"
14)
15
16const defaultPlaceholder = "value"
17
18var (
19	slPfx = fmt.Sprintf("sl:::%d:::", time.Now().UTC().UnixNano())
20
21	commaWhitespace = regexp.MustCompile("[, ]+.*")
22)
23
24// BashCompletionFlag enables bash-completion for all commands and subcommands
25var BashCompletionFlag Flag = &BoolFlag{
26	Name:   "generate-bash-completion",
27	Hidden: true,
28}
29
30// VersionFlag prints the version for the application
31var VersionFlag Flag = &BoolFlag{
32	Name:    "version",
33	Aliases: []string{"v"},
34	Usage:   "print the version",
35}
36
37// HelpFlag prints the help for all commands and subcommands.
38// Set to nil to disable the flag.  The subcommand
39// will still be added unless HideHelp or HideHelpCommand is set to true.
40var HelpFlag Flag = &BoolFlag{
41	Name:    "help",
42	Aliases: []string{"h"},
43	Usage:   "show help",
44}
45
46// FlagStringer converts a flag definition to a string. This is used by help
47// to display a flag.
48var FlagStringer FlagStringFunc = stringifyFlag
49
50// Serializer is used to circumvent the limitations of flag.FlagSet.Set
51type Serializer interface {
52	Serialize() string
53}
54
55// FlagNamePrefixer converts a full flag name and its placeholder into the help
56// message flag prefix. This is used by the default FlagStringer.
57var FlagNamePrefixer FlagNamePrefixFunc = prefixedNames
58
59// FlagEnvHinter annotates flag help message with the environment variable
60// details. This is used by the default FlagStringer.
61var FlagEnvHinter FlagEnvHintFunc = withEnvHint
62
63// FlagFileHinter annotates flag help message with the environment variable
64// details. This is used by the default FlagStringer.
65var FlagFileHinter FlagFileHintFunc = withFileHint
66
67// FlagsByName is a slice of Flag.
68type FlagsByName []Flag
69
70func (f FlagsByName) Len() int {
71	return len(f)
72}
73
74func (f FlagsByName) Less(i, j int) bool {
75	if len(f[j].Names()) == 0 {
76		return false
77	} else if len(f[i].Names()) == 0 {
78		return true
79	}
80	return lexicographicLess(f[i].Names()[0], f[j].Names()[0])
81}
82
83func (f FlagsByName) Swap(i, j int) {
84	f[i], f[j] = f[j], f[i]
85}
86
87// Flag is a common interface related to parsing flags in cli.
88// For more advanced flag parsing techniques, it is recommended that
89// this interface be implemented.
90type Flag interface {
91	fmt.Stringer
92	// Apply Flag settings to the given flag set
93	Apply(*flag.FlagSet) error
94	Names() []string
95	IsSet() bool
96}
97
98// RequiredFlag is an interface that allows us to mark flags as required
99// it allows flags required flags to be backwards compatible with the Flag interface
100type RequiredFlag interface {
101	Flag
102
103	IsRequired() bool
104}
105
106// DocGenerationFlag is an interface that allows documentation generation for the flag
107type DocGenerationFlag interface {
108	Flag
109
110	// TakesValue returns true if the flag takes a value, otherwise false
111	TakesValue() bool
112
113	// GetUsage returns the usage string for the flag
114	GetUsage() string
115
116	// GetValue returns the flags value as string representation and an empty
117	// string if the flag takes no value at all.
118	GetValue() string
119}
120
121func flagSet(name string, flags []Flag) (*flag.FlagSet, error) {
122	set := flag.NewFlagSet(name, flag.ContinueOnError)
123
124	for _, f := range flags {
125		if err := f.Apply(set); err != nil {
126			return nil, err
127		}
128	}
129	set.SetOutput(ioutil.Discard)
130	return set, nil
131}
132
133func visibleFlags(fl []Flag) []Flag {
134	var visible []Flag
135	for _, f := range fl {
136		field := flagValue(f).FieldByName("Hidden")
137		if !field.IsValid() || !field.Bool() {
138			visible = append(visible, f)
139		}
140	}
141	return visible
142}
143
144func prefixFor(name string) (prefix string) {
145	if len(name) == 1 {
146		prefix = "-"
147	} else {
148		prefix = "--"
149	}
150
151	return
152}
153
154// Returns the placeholder, if any, and the unquoted usage string.
155func unquoteUsage(usage string) (string, string) {
156	for i := 0; i < len(usage); i++ {
157		if usage[i] == '`' {
158			for j := i + 1; j < len(usage); j++ {
159				if usage[j] == '`' {
160					name := usage[i+1 : j]
161					usage = usage[:i] + name + usage[j+1:]
162					return name, usage
163				}
164			}
165			break
166		}
167	}
168	return "", usage
169}
170
171func prefixedNames(names []string, placeholder string) string {
172	var prefixed string
173	for i, name := range names {
174		if name == "" {
175			continue
176		}
177
178		prefixed += prefixFor(name) + name
179		if placeholder != "" {
180			prefixed += " " + placeholder
181		}
182		if i < len(names)-1 {
183			prefixed += ", "
184		}
185	}
186	return prefixed
187}
188
189func withEnvHint(envVars []string, str string) string {
190	envText := ""
191	if envVars != nil && len(envVars) > 0 {
192		prefix := "$"
193		suffix := ""
194		sep := ", $"
195		if runtime.GOOS == "windows" {
196			prefix = "%"
197			suffix = "%"
198			sep = "%, %"
199		}
200
201		envText = fmt.Sprintf(" [%s%s%s]", prefix, strings.Join(envVars, sep), suffix)
202	}
203	return str + envText
204}
205
206func flagNames(name string, aliases []string) []string {
207	var ret []string
208
209	for _, part := range append([]string{name}, aliases...) {
210		// v1 -> v2 migration warning zone:
211		// Strip off anything after the first found comma or space, which
212		// *hopefully* makes it a tiny bit more obvious that unexpected behavior is
213		// caused by using the v1 form of stringly typed "Name".
214		ret = append(ret, commaWhitespace.ReplaceAllString(part, ""))
215	}
216
217	return ret
218}
219
220func flagStringSliceField(f Flag, name string) []string {
221	fv := flagValue(f)
222	field := fv.FieldByName(name)
223
224	if field.IsValid() {
225		return field.Interface().([]string)
226	}
227
228	return []string{}
229}
230
231func withFileHint(filePath, str string) string {
232	fileText := ""
233	if filePath != "" {
234		fileText = fmt.Sprintf(" [%s]", filePath)
235	}
236	return str + fileText
237}
238
239func flagValue(f Flag) reflect.Value {
240	fv := reflect.ValueOf(f)
241	for fv.Kind() == reflect.Ptr {
242		fv = reflect.Indirect(fv)
243	}
244	return fv
245}
246
247func formatDefault(format string) string {
248	return " (default: " + format + ")"
249}
250
251func stringifyFlag(f Flag) string {
252	fv := flagValue(f)
253
254	switch f := f.(type) {
255	case *IntSliceFlag:
256		return withEnvHint(flagStringSliceField(f, "EnvVars"),
257			stringifyIntSliceFlag(f))
258	case *Int64SliceFlag:
259		return withEnvHint(flagStringSliceField(f, "EnvVars"),
260			stringifyInt64SliceFlag(f))
261	case *Float64SliceFlag:
262		return withEnvHint(flagStringSliceField(f, "EnvVars"),
263			stringifyFloat64SliceFlag(f))
264	case *StringSliceFlag:
265		return withEnvHint(flagStringSliceField(f, "EnvVars"),
266			stringifyStringSliceFlag(f))
267	}
268
269	placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String())
270
271	needsPlaceholder := false
272	defaultValueString := ""
273	val := fv.FieldByName("Value")
274	if val.IsValid() {
275		needsPlaceholder = val.Kind() != reflect.Bool
276		defaultValueString = fmt.Sprintf(formatDefault("%v"), val.Interface())
277
278		if val.Kind() == reflect.String && val.String() != "" {
279			defaultValueString = fmt.Sprintf(formatDefault("%q"), val.String())
280		}
281	}
282
283	helpText := fv.FieldByName("DefaultText")
284	if helpText.IsValid() && helpText.String() != "" {
285		needsPlaceholder = val.Kind() != reflect.Bool
286		defaultValueString = fmt.Sprintf(formatDefault("%s"), helpText.String())
287	}
288
289	if defaultValueString == formatDefault("") {
290		defaultValueString = ""
291	}
292
293	if needsPlaceholder && placeholder == "" {
294		placeholder = defaultPlaceholder
295	}
296
297	usageWithDefault := strings.TrimSpace(usage + defaultValueString)
298
299	return withEnvHint(flagStringSliceField(f, "EnvVars"),
300		fmt.Sprintf("%s\t%s", prefixedNames(f.Names(), placeholder), usageWithDefault))
301}
302
303func stringifyIntSliceFlag(f *IntSliceFlag) string {
304	var defaultVals []string
305	if f.Value != nil && len(f.Value.Value()) > 0 {
306		for _, i := range f.Value.Value() {
307			defaultVals = append(defaultVals, strconv.Itoa(i))
308		}
309	}
310
311	return stringifySliceFlag(f.Usage, f.Names(), defaultVals)
312}
313
314func stringifyInt64SliceFlag(f *Int64SliceFlag) string {
315	var defaultVals []string
316	if f.Value != nil && len(f.Value.Value()) > 0 {
317		for _, i := range f.Value.Value() {
318			defaultVals = append(defaultVals, strconv.FormatInt(i, 10))
319		}
320	}
321
322	return stringifySliceFlag(f.Usage, f.Names(), defaultVals)
323}
324
325func stringifyFloat64SliceFlag(f *Float64SliceFlag) string {
326	var defaultVals []string
327
328	if f.Value != nil && len(f.Value.Value()) > 0 {
329		for _, i := range f.Value.Value() {
330			defaultVals = append(defaultVals, strings.TrimRight(strings.TrimRight(fmt.Sprintf("%f", i), "0"), "."))
331		}
332	}
333
334	return stringifySliceFlag(f.Usage, f.Names(), defaultVals)
335}
336
337func stringifyStringSliceFlag(f *StringSliceFlag) string {
338	var defaultVals []string
339	if f.Value != nil && len(f.Value.Value()) > 0 {
340		for _, s := range f.Value.Value() {
341			if len(s) > 0 {
342				defaultVals = append(defaultVals, strconv.Quote(s))
343			}
344		}
345	}
346
347	return stringifySliceFlag(f.Usage, f.Names(), defaultVals)
348}
349
350func stringifySliceFlag(usage string, names, defaultVals []string) string {
351	placeholder, usage := unquoteUsage(usage)
352	if placeholder == "" {
353		placeholder = defaultPlaceholder
354	}
355
356	defaultVal := ""
357	if len(defaultVals) > 0 {
358		defaultVal = fmt.Sprintf(formatDefault("%s"), strings.Join(defaultVals, ", "))
359	}
360
361	usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal))
362	return fmt.Sprintf("%s\t%s", prefixedNames(names, placeholder), usageWithDefault)
363}
364
365func hasFlag(flags []Flag, fl Flag) bool {
366	for _, existing := range flags {
367		if fl == existing {
368			return true
369		}
370	}
371
372	return false
373}
374
375func flagFromEnvOrFile(envVars []string, filePath string) (val string, ok bool) {
376	for _, envVar := range envVars {
377		envVar = strings.TrimSpace(envVar)
378		if val, ok := syscall.Getenv(envVar); ok {
379			return val, true
380		}
381	}
382	for _, fileVar := range strings.Split(filePath, ",") {
383		if data, err := ioutil.ReadFile(fileVar); err == nil {
384			return string(data), true
385		}
386	}
387	return "", false
388}
389