1 package cli
2
3 import (
4 "errors"
5 "flag"
6 "fmt"
7 "io"
8 "os"
9 "regexp"
10 "runtime"
11 "strings"
12 "syscall"
13 "time"
14 )
15
16 const defaultPlaceholder = "value"
17
18 const (
19 defaultSliceFlagSeparator = ","
20 disableSliceFlagSeparator = false
21 )
22
23 var (
24 slPfx = fmt.Sprintf("sl:::%d:::", time.Now().UTC().UnixNano())
25
26 commaWhitespace = regexp.MustCompile("[, ]+.*")
27 )
28
29
30 var BashCompletionFlag Flag = &BoolFlag{
31 Name: "generate-bash-completion",
32 Hidden: true,
33 }
34
35
36 var VersionFlag Flag = &BoolFlag{
37 Name: "version",
38 Aliases: []string{"v"},
39 Usage: "print the version",
40 DisableDefaultText: true,
41 }
42
43
44
45
46 var HelpFlag Flag = &BoolFlag{
47 Name: "help",
48 Aliases: []string{"h"},
49 Usage: "show help",
50 DisableDefaultText: true,
51 }
52
53
54
55 var FlagStringer FlagStringFunc = stringifyFlag
56
57
58 type Serializer interface {
59 Serialize() string
60 }
61
62
63
64 var FlagNamePrefixer FlagNamePrefixFunc = prefixedNames
65
66
67
68 var FlagEnvHinter FlagEnvHintFunc = withEnvHint
69
70
71
72 var FlagFileHinter FlagFileHintFunc = withFileHint
73
74
75 type FlagsByName []Flag
76
77 func (f FlagsByName) Len() int {
78 return len(f)
79 }
80
81 func (f FlagsByName) Less(i, j int) bool {
82 if len(f[j].Names()) == 0 {
83 return false
84 } else if len(f[i].Names()) == 0 {
85 return true
86 }
87 return lexicographicLess(f[i].Names()[0], f[j].Names()[0])
88 }
89
90 func (f FlagsByName) Swap(i, j int) {
91 f[i], f[j] = f[j], f[i]
92 }
93
94
95 type ActionableFlag interface {
96 Flag
97 RunAction(*Context) error
98 }
99
100
101
102
103 type Flag interface {
104 fmt.Stringer
105
106 Apply(*flag.FlagSet) error
107 Names() []string
108 IsSet() bool
109 }
110
111
112
113 type RequiredFlag interface {
114 Flag
115
116 IsRequired() bool
117 }
118
119
120 type DocGenerationFlag interface {
121 Flag
122
123
124 TakesValue() bool
125
126
127 GetUsage() string
128
129
130
131 GetValue() string
132
133
134 GetDefaultText() string
135
136
137 GetEnvVars() []string
138 }
139
140
141 type DocGenerationSliceFlag interface {
142 DocGenerationFlag
143
144
145 IsSliceFlag() bool
146 }
147
148
149 type VisibleFlag interface {
150 Flag
151
152
153 IsVisible() bool
154 }
155
156
157
158 type CategorizableFlag interface {
159 VisibleFlag
160
161 GetCategory() string
162 }
163
164
165
166 type Countable interface {
167 Count() int
168 }
169
170 func flagSet(name string, flags []Flag, spec separatorSpec) (*flag.FlagSet, error) {
171 set := flag.NewFlagSet(name, flag.ContinueOnError)
172
173 for _, f := range flags {
174 if c, ok := f.(customizedSeparator); ok {
175 c.WithSeparatorSpec(spec)
176 }
177 if err := f.Apply(set); err != nil {
178 return nil, err
179 }
180 }
181 set.SetOutput(io.Discard)
182 return set, nil
183 }
184
185 func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) {
186 switch ff.Value.(type) {
187 case Serializer:
188 _ = set.Set(name, ff.Value.(Serializer).Serialize())
189 default:
190 _ = set.Set(name, ff.Value.String())
191 }
192 }
193
194 func normalizeFlags(flags []Flag, set *flag.FlagSet) error {
195 visited := make(map[string]bool)
196 set.Visit(func(f *flag.Flag) {
197 visited[f.Name] = true
198 })
199 for _, f := range flags {
200 parts := f.Names()
201 if len(parts) == 1 {
202 continue
203 }
204 var ff *flag.Flag
205 for _, name := range parts {
206 name = strings.Trim(name, " ")
207 if visited[name] {
208 if ff != nil {
209 return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name)
210 }
211 ff = set.Lookup(name)
212 }
213 }
214 if ff == nil {
215 continue
216 }
217 for _, name := range parts {
218 name = strings.Trim(name, " ")
219 if !visited[name] {
220 copyFlag(name, ff, set)
221 }
222 }
223 }
224 return nil
225 }
226
227 func visibleFlags(fl []Flag) []Flag {
228 var visible []Flag
229 for _, f := range fl {
230 if vf, ok := f.(VisibleFlag); ok && vf.IsVisible() {
231 visible = append(visible, f)
232 }
233 }
234 return visible
235 }
236
237 func prefixFor(name string) (prefix string) {
238 if len(name) == 1 {
239 prefix = "-"
240 } else {
241 prefix = "--"
242 }
243
244 return
245 }
246
247
248 func unquoteUsage(usage string) (string, string) {
249 for i := 0; i < len(usage); i++ {
250 if usage[i] == '`' {
251 for j := i + 1; j < len(usage); j++ {
252 if usage[j] == '`' {
253 name := usage[i+1 : j]
254 usage = usage[:i] + name + usage[j+1:]
255 return name, usage
256 }
257 }
258 break
259 }
260 }
261 return "", usage
262 }
263
264 func prefixedNames(names []string, placeholder string) string {
265 var prefixed string
266 for i, name := range names {
267 if name == "" {
268 continue
269 }
270
271 prefixed += prefixFor(name) + name
272 if placeholder != "" {
273 prefixed += " " + placeholder
274 }
275 if i < len(names)-1 {
276 prefixed += ", "
277 }
278 }
279 return prefixed
280 }
281
282 func envFormat(envVars []string, prefix, sep, suffix string) string {
283 if len(envVars) > 0 {
284 return fmt.Sprintf(" [%s%s%s]", prefix, strings.Join(envVars, sep), suffix)
285 }
286 return ""
287 }
288
289 func defaultEnvFormat(envVars []string) string {
290 return envFormat(envVars, "$", ", $", "")
291 }
292
293 func withEnvHint(envVars []string, str string) string {
294 envText := ""
295 if runtime.GOOS != "windows" || os.Getenv("PSHOME") != "" {
296 envText = defaultEnvFormat(envVars)
297 } else {
298 envText = envFormat(envVars, "%", "%, %", "%")
299 }
300 return str + envText
301 }
302
303 func FlagNames(name string, aliases []string) []string {
304 var ret []string
305
306 for _, part := range append([]string{name}, aliases...) {
307
308
309
310
311 ret = append(ret, commaWhitespace.ReplaceAllString(part, ""))
312 }
313
314 return ret
315 }
316
317 func withFileHint(filePath, str string) string {
318 fileText := ""
319 if filePath != "" {
320 fileText = fmt.Sprintf(" [%s]", filePath)
321 }
322 return str + fileText
323 }
324
325 func formatDefault(format string) string {
326 return " (default: " + format + ")"
327 }
328
329 func stringifyFlag(f Flag) string {
330
331 df, ok := f.(DocGenerationFlag)
332 if !ok {
333 return ""
334 }
335
336 placeholder, usage := unquoteUsage(df.GetUsage())
337 needsPlaceholder := df.TakesValue()
338
339 if needsPlaceholder && placeholder == "" {
340 placeholder = defaultPlaceholder
341 }
342
343 defaultValueString := ""
344
345
346
347
348 if bf, ok := f.(*BoolFlag); !ok || !bf.DisableDefaultText {
349 if s := df.GetDefaultText(); s != "" {
350 defaultValueString = fmt.Sprintf(formatDefault("%s"), s)
351 }
352 }
353
354 usageWithDefault := strings.TrimSpace(usage + defaultValueString)
355
356 pn := prefixedNames(df.Names(), placeholder)
357 sliceFlag, ok := f.(DocGenerationSliceFlag)
358 if ok && sliceFlag.IsSliceFlag() {
359 pn = pn + " [ " + pn + " ]"
360 }
361
362 return withEnvHint(df.GetEnvVars(), fmt.Sprintf("%s\t%s", pn, usageWithDefault))
363 }
364
365 func 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
375
376
377
378 func flagFromEnvOrFile(envVars []string, filePath string) (value string, fromWhere string, found bool) {
379 for _, envVar := range envVars {
380 envVar = strings.TrimSpace(envVar)
381 if value, found := syscall.Getenv(envVar); found {
382 return value, fmt.Sprintf("environment variable %q", envVar), true
383 }
384 }
385 for _, fileVar := range strings.Split(filePath, ",") {
386 if fileVar != "" {
387 if data, err := os.ReadFile(fileVar); err == nil {
388 return string(data), fmt.Sprintf("file %q", filePath), true
389 }
390 }
391 }
392 return "", "", false
393 }
394
395 type customizedSeparator interface {
396 WithSeparatorSpec(separatorSpec)
397 }
398
399 type separatorSpec struct {
400 sep string
401 disabled bool
402 customized bool
403 }
404
405 func (s separatorSpec) flagSplitMultiValues(val string) []string {
406 var (
407 disabled bool = s.disabled
408 sep string = s.sep
409 )
410 if !s.customized {
411 disabled = disableSliceFlagSeparator
412 sep = defaultSliceFlagSeparator
413 }
414 if disabled {
415 return []string{val}
416 }
417
418 return strings.Split(val, sep)
419 }
420
View as plain text