1 package cli
2
3 import (
4 "context"
5 "flag"
6 "fmt"
7 "io"
8 "os"
9 "path/filepath"
10 "sort"
11 "strings"
12 "time"
13 )
14
15 const suggestDidYouMeanTemplate = "Did you mean %q?"
16
17 var (
18 changeLogURL = "https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md"
19 appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL)
20 contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you."
21 errInvalidActionType = NewExitError("ERROR invalid Action type. "+
22 fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error). %s", contactSysadmin)+
23 fmt.Sprintf("See %s", appActionDeprecationURL), 2)
24 ignoreFlagPrefix = "test."
25
26 SuggestFlag SuggestFlagFunc = nil
27 SuggestCommand SuggestCommandFunc = nil
28 SuggestDidYouMeanTemplate string = suggestDidYouMeanTemplate
29 )
30
31
32
33 type App struct {
34
35 Name string
36
37 HelpName string
38
39 Usage string
40
41 UsageText string
42
43 Args bool
44
45 ArgsUsage string
46
47 Version string
48
49 Description string
50
51
52 DefaultCommand string
53
54 Commands []*Command
55
56 Flags []Flag
57
58 EnableBashCompletion bool
59
60 HideHelp bool
61
62
63 HideHelpCommand bool
64
65 HideVersion bool
66
67 categories CommandCategories
68
69 flagCategories FlagCategories
70
71 BashComplete BashCompleteFunc
72
73
74 Before BeforeFunc
75
76
77 After AfterFunc
78
79 Action ActionFunc
80
81 CommandNotFound CommandNotFoundFunc
82
83 OnUsageError OnUsageErrorFunc
84
85 InvalidFlagAccessHandler InvalidFlagAccessFunc
86
87 Compiled time.Time
88
89 Authors []*Author
90
91 Copyright string
92
93 Reader io.Reader
94
95 Writer io.Writer
96
97 ErrWriter io.Writer
98
99
100
101 ExitErrHandler ExitErrHandlerFunc
102
103 Metadata map[string]interface{}
104
105 ExtraInfo func() map[string]string
106
107
108
109 CustomAppHelpTemplate string
110
111 SliceFlagSeparator string
112
113 DisableSliceFlagSeparator bool
114
115
116
117 UseShortOptionHandling bool
118
119 Suggest bool
120
121
122 AllowExtFlags bool
123
124 SkipFlagParsing bool
125
126 didSetup bool
127 separator separatorSpec
128
129 rootCommand *Command
130 }
131
132 type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string
133
134 type SuggestCommandFunc func(commands []*Command, provided string) string
135
136
137
138 func compileTime() time.Time {
139 info, err := os.Stat(os.Args[0])
140 if err != nil {
141 return time.Now()
142 }
143 return info.ModTime()
144 }
145
146
147
148 func NewApp() *App {
149 return &App{
150 Name: filepath.Base(os.Args[0]),
151 Usage: "A new cli application",
152 UsageText: "",
153 BashComplete: DefaultAppComplete,
154 Action: helpCommand.Action,
155 Compiled: compileTime(),
156 Reader: os.Stdin,
157 Writer: os.Stdout,
158 ErrWriter: os.Stderr,
159 }
160 }
161
162
163
164
165 func (a *App) Setup() {
166 if a.didSetup {
167 return
168 }
169
170 a.didSetup = true
171
172 if a.Name == "" {
173 a.Name = filepath.Base(os.Args[0])
174 }
175
176 if a.HelpName == "" {
177 a.HelpName = a.Name
178 }
179
180 if a.Usage == "" {
181 a.Usage = "A new cli application"
182 }
183
184 if a.Version == "" {
185 a.HideVersion = true
186 }
187
188 if a.BashComplete == nil {
189 a.BashComplete = DefaultAppComplete
190 }
191
192 if a.Action == nil {
193 a.Action = helpCommand.Action
194 }
195
196 if a.Compiled == (time.Time{}) {
197 a.Compiled = compileTime()
198 }
199
200 if a.Reader == nil {
201 a.Reader = os.Stdin
202 }
203
204 if a.Writer == nil {
205 a.Writer = os.Stdout
206 }
207
208 if a.ErrWriter == nil {
209 a.ErrWriter = os.Stderr
210 }
211
212 if a.AllowExtFlags {
213
214 flag.VisitAll(func(f *flag.Flag) {
215
216 if !strings.HasPrefix(f.Name, ignoreFlagPrefix) {
217 a.Flags = append(a.Flags, &extFlag{f})
218 }
219 })
220 }
221
222 if len(a.SliceFlagSeparator) != 0 {
223 a.separator.customized = true
224 a.separator.sep = a.SliceFlagSeparator
225 }
226
227 if a.DisableSliceFlagSeparator {
228 a.separator.customized = true
229 a.separator.disabled = true
230 }
231
232 var newCommands []*Command
233
234 for _, c := range a.Commands {
235 cname := c.Name
236 if c.HelpName != "" {
237 cname = c.HelpName
238 }
239 c.separator = a.separator
240 c.HelpName = fmt.Sprintf("%s %s", a.HelpName, cname)
241 c.flagCategories = newFlagCategoriesFromFlags(c.Flags)
242 newCommands = append(newCommands, c)
243 }
244 a.Commands = newCommands
245
246 if a.Command(helpCommand.Name) == nil && !a.HideHelp {
247 if !a.HideHelpCommand {
248 a.appendCommand(helpCommand)
249 }
250
251 if HelpFlag != nil {
252 a.appendFlag(HelpFlag)
253 }
254 }
255
256 if !a.HideVersion {
257 a.appendFlag(VersionFlag)
258 }
259
260 a.categories = newCommandCategories()
261 for _, command := range a.Commands {
262 a.categories.AddCommand(command.Category, command)
263 }
264 sort.Sort(a.categories.(*commandCategories))
265
266 a.flagCategories = newFlagCategoriesFromFlags(a.Flags)
267
268 if a.Metadata == nil {
269 a.Metadata = make(map[string]interface{})
270 }
271 }
272
273 func (a *App) newRootCommand() *Command {
274 return &Command{
275 Name: a.Name,
276 Usage: a.Usage,
277 UsageText: a.UsageText,
278 Description: a.Description,
279 ArgsUsage: a.ArgsUsage,
280 BashComplete: a.BashComplete,
281 Before: a.Before,
282 After: a.After,
283 Action: a.Action,
284 OnUsageError: a.OnUsageError,
285 Subcommands: a.Commands,
286 Flags: a.Flags,
287 flagCategories: a.flagCategories,
288 HideHelp: a.HideHelp,
289 HideHelpCommand: a.HideHelpCommand,
290 UseShortOptionHandling: a.UseShortOptionHandling,
291 HelpName: a.HelpName,
292 CustomHelpTemplate: a.CustomAppHelpTemplate,
293 categories: a.categories,
294 SkipFlagParsing: a.SkipFlagParsing,
295 isRoot: true,
296 separator: a.separator,
297 }
298 }
299
300 func (a *App) newFlagSet() (*flag.FlagSet, error) {
301 return flagSet(a.Name, a.Flags, a.separator)
302 }
303
304 func (a *App) useShortOptionHandling() bool {
305 return a.UseShortOptionHandling
306 }
307
308
309
310 func (a *App) Run(arguments []string) (err error) {
311 return a.RunContext(context.Background(), arguments)
312 }
313
314
315
316
317 func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
318 a.Setup()
319
320
321
322
323
324
325
326 shellComplete, arguments := checkShellCompleteFlag(a, arguments)
327
328 cCtx := NewContext(a, nil, &Context{Context: ctx})
329 cCtx.shellComplete = shellComplete
330
331 a.rootCommand = a.newRootCommand()
332 cCtx.Command = a.rootCommand
333
334 if err := checkDuplicatedCmds(a.rootCommand); err != nil {
335 return err
336 }
337 return a.rootCommand.Run(cCtx, arguments...)
338 }
339
340
341
342 func (a *App) RunAsSubcommand(ctx *Context) (err error) {
343 a.Setup()
344
345 cCtx := NewContext(a, nil, ctx)
346 cCtx.shellComplete = ctx.shellComplete
347
348 a.rootCommand = a.newRootCommand()
349 cCtx.Command = a.rootCommand
350
351 return a.rootCommand.Run(cCtx, ctx.Args().Slice()...)
352 }
353
354 func (a *App) suggestFlagFromError(err error, command string) (string, error) {
355 flag, parseErr := flagFromError(err)
356 if parseErr != nil {
357 return "", err
358 }
359
360 flags := a.Flags
361 hideHelp := a.HideHelp
362 if command != "" {
363 cmd := a.Command(command)
364 if cmd == nil {
365 return "", err
366 }
367 flags = cmd.Flags
368 hideHelp = hideHelp || cmd.HideHelp
369 }
370
371 if SuggestFlag == nil {
372 return "", err
373 }
374 suggestion := SuggestFlag(flags, flag, hideHelp)
375 if len(suggestion) == 0 {
376 return "", err
377 }
378
379 return fmt.Sprintf(SuggestDidYouMeanTemplate+"\n\n", suggestion), nil
380 }
381
382
383
384
385
386
387 func (a *App) RunAndExitOnError() {
388 if err := a.Run(os.Args); err != nil {
389 _, _ = fmt.Fprintln(a.ErrWriter, err)
390 OsExiter(1)
391 }
392 }
393
394
395 func (a *App) Command(name string) *Command {
396 for _, c := range a.Commands {
397 if c.HasName(name) {
398 return c
399 }
400 }
401
402 return nil
403 }
404
405
406
407 func (a *App) VisibleCategories() []CommandCategory {
408 ret := []CommandCategory{}
409 for _, category := range a.categories.Categories() {
410 if visible := func() CommandCategory {
411 if len(category.VisibleCommands()) > 0 {
412 return category
413 }
414 return nil
415 }(); visible != nil {
416 ret = append(ret, visible)
417 }
418 }
419 return ret
420 }
421
422
423 func (a *App) VisibleCommands() []*Command {
424 var ret []*Command
425 for _, command := range a.Commands {
426 if !command.Hidden {
427 ret = append(ret, command)
428 }
429 }
430 return ret
431 }
432
433
434 func (a *App) VisibleFlagCategories() []VisibleFlagCategory {
435 if a.flagCategories == nil {
436 return []VisibleFlagCategory{}
437 }
438 return a.flagCategories.VisibleCategories()
439 }
440
441
442 func (a *App) VisibleFlags() []Flag {
443 return visibleFlags(a.Flags)
444 }
445
446 func (a *App) appendFlag(fl Flag) {
447 if !hasFlag(a.Flags, fl) {
448 a.Flags = append(a.Flags, fl)
449 }
450 }
451
452 func (a *App) appendCommand(c *Command) {
453 if !hasCommand(a.Commands, c) {
454 a.Commands = append(a.Commands, c)
455 }
456 }
457
458 func (a *App) handleExitCoder(cCtx *Context, err error) {
459 if a.ExitErrHandler != nil {
460 a.ExitErrHandler(cCtx, err)
461 } else {
462 HandleExitCoder(err)
463 }
464 }
465
466 func (a *App) argsWithDefaultCommand(oldArgs Args) Args {
467 if a.DefaultCommand != "" {
468 rawArgs := append([]string{a.DefaultCommand}, oldArgs.Slice()...)
469 newArgs := args(rawArgs)
470
471 return &newArgs
472 }
473
474 return oldArgs
475 }
476
477 func runFlagActions(c *Context, fs []Flag) error {
478 for _, f := range fs {
479 isSet := false
480 for _, name := range f.Names() {
481 if c.IsSet(name) {
482 isSet = true
483 break
484 }
485 }
486 if isSet {
487 if af, ok := f.(ActionableFlag); ok {
488 if err := af.RunAction(c); err != nil {
489 return err
490 }
491 }
492 }
493 }
494 return nil
495 }
496
497
498 type Author struct {
499 Name string
500 Email string
501 }
502
503
504 func (a *Author) String() string {
505 e := ""
506 if a.Email != "" {
507 e = " <" + a.Email + ">"
508 }
509
510 return fmt.Sprintf("%v%v", a.Name, e)
511 }
512
513
514
515
516 func HandleAction(action interface{}, cCtx *Context) (err error) {
517 switch a := action.(type) {
518 case ActionFunc:
519 return a(cCtx)
520 case func(*Context) error:
521 return a(cCtx)
522 case func(*Context):
523 a(cCtx)
524 return nil
525 }
526
527 return errInvalidActionType
528 }
529
530 func checkStringSliceIncludes(want string, sSlice []string) bool {
531 found := false
532 for _, s := range sSlice {
533 if want == s {
534 found = true
535 break
536 }
537 }
538
539 return found
540 }
541
View as plain text