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