...

Source file src/github.com/urfave/cli/v2/app.go

Documentation: github.com/urfave/cli/v2

     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." // this is to ignore test flags when adding flags from other packages
    25  
    26  	SuggestFlag               SuggestFlagFunc    = nil // initialized in suggestions.go unless built with urfave_cli_no_suggest
    27  	SuggestCommand            SuggestCommandFunc = nil // initialized in suggestions.go unless built with urfave_cli_no_suggest
    28  	SuggestDidYouMeanTemplate string             = suggestDidYouMeanTemplate
    29  )
    30  
    31  // App is the main structure of a cli application. It is recommended that
    32  // an app be created with the cli.NewApp() function
    33  type App struct {
    34  	// The name of the program. Defaults to path.Base(os.Args[0])
    35  	Name string
    36  	// Full name of command for help, defaults to Name
    37  	HelpName string
    38  	// Description of the program.
    39  	Usage string
    40  	// Text to override the USAGE section of help
    41  	UsageText string
    42  	// Whether this command supports arguments
    43  	Args bool
    44  	// Description of the program argument format.
    45  	ArgsUsage string
    46  	// Version of the program
    47  	Version string
    48  	// Description of the program
    49  	Description string
    50  	// DefaultCommand is the (optional) name of a command
    51  	// to run if no command names are passed as CLI arguments.
    52  	DefaultCommand string
    53  	// List of commands to execute
    54  	Commands []*Command
    55  	// List of flags to parse
    56  	Flags []Flag
    57  	// Boolean to enable bash completion commands
    58  	EnableBashCompletion bool
    59  	// Boolean to hide built-in help command and help flag
    60  	HideHelp bool
    61  	// Boolean to hide built-in help command but keep help flag.
    62  	// Ignored if HideHelp is true.
    63  	HideHelpCommand bool
    64  	// Boolean to hide built-in version flag and the VERSION section of help
    65  	HideVersion bool
    66  	// categories contains the categorized commands and is populated on app startup
    67  	categories CommandCategories
    68  	// flagCategories contains the categorized flags and is populated on app startup
    69  	flagCategories FlagCategories
    70  	// An action to execute when the shell completion flag is set
    71  	BashComplete BashCompleteFunc
    72  	// An action to execute before any subcommands are run, but after the context is ready
    73  	// If a non-nil error is returned, no subcommands are run
    74  	Before BeforeFunc
    75  	// An action to execute after any subcommands are run, but after the subcommand has finished
    76  	// It is run even if Action() panics
    77  	After AfterFunc
    78  	// The action to execute when no subcommands are specified
    79  	Action ActionFunc
    80  	// Execute this function if the proper command cannot be found
    81  	CommandNotFound CommandNotFoundFunc
    82  	// Execute this function if a usage error occurs
    83  	OnUsageError OnUsageErrorFunc
    84  	// Execute this function when an invalid flag is accessed from the context
    85  	InvalidFlagAccessHandler InvalidFlagAccessFunc
    86  	// Compilation date
    87  	Compiled time.Time
    88  	// List of all authors who contributed
    89  	Authors []*Author
    90  	// Copyright of the binary if any
    91  	Copyright string
    92  	// Reader reader to write input to (useful for tests)
    93  	Reader io.Reader
    94  	// Writer writer to write output to
    95  	Writer io.Writer
    96  	// ErrWriter writes error output
    97  	ErrWriter io.Writer
    98  	// ExitErrHandler processes any error encountered while running an App before
    99  	// it is returned to the caller. If no function is provided, HandleExitCoder
   100  	// is used as the default behavior.
   101  	ExitErrHandler ExitErrHandlerFunc
   102  	// Other custom info
   103  	Metadata map[string]interface{}
   104  	// Carries a function which returns app specific info.
   105  	ExtraInfo func() map[string]string
   106  	// CustomAppHelpTemplate the text template for app help topic.
   107  	// cli.go uses text/template to render templates. You can
   108  	// render custom help text by setting this variable.
   109  	CustomAppHelpTemplate string
   110  	// SliceFlagSeparator is used to customize the separator for SliceFlag, the default is ","
   111  	SliceFlagSeparator string
   112  	// DisableSliceFlagSeparator is used to disable SliceFlagSeparator, the default is false
   113  	DisableSliceFlagSeparator bool
   114  	// Boolean to enable short-option handling so user can combine several
   115  	// single-character bool arguments into one
   116  	// i.e. foobar -o -v -> foobar -ov
   117  	UseShortOptionHandling bool
   118  	// Enable suggestions for commands and flags
   119  	Suggest bool
   120  	// Allows global flags set by libraries which use flag.XXXVar(...) directly
   121  	// to be parsed through this library
   122  	AllowExtFlags bool
   123  	// Treat all flags as normal arguments if true
   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  // Tries to find out when this binary was compiled.
   137  // Returns the current time if it fails to find it.
   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  // NewApp creates a new cli Application with some reasonable defaults for Name,
   147  // Usage, Version and Action.
   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  // Setup runs initialization code to ensure all data structures are ready for
   163  // `Run` or inspection prior to `Run`.  It is internally called by `Run`, but
   164  // will return early if setup has already happened.
   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  		// add global flags added by other packages
   214  		flag.VisitAll(func(f *flag.Flag) {
   215  			// skip test flags
   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  // Run is the entry point to the cli app. Parses the arguments slice and routes
   309  // to the proper flag/args combination
   310  func (a *App) Run(arguments []string) (err error) {
   311  	return a.RunContext(context.Background(), arguments)
   312  }
   313  
   314  // RunContext is like Run except it takes a Context that will be
   315  // passed to its commands and sub-commands. Through this, you can
   316  // propagate timeouts and cancellation requests
   317  func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
   318  	a.Setup()
   319  
   320  	// handle the completion flag separately from the flagset since
   321  	// completion could be attempted after a flag, but before its value was put
   322  	// on the command line. this causes the flagset to interpret the completion
   323  	// flag name as the value of the flag before it which is undesirable
   324  	// note that we can only do this because the shell autocomplete function
   325  	// always appends the completion flag at the end of the command
   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  // RunAsSubcommand is for legacy/compatibility purposes only. New code should only
   341  // use App.RunContext. This function is slated to be removed in v3.
   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  // RunAndExitOnError calls .Run() and exits non-zero if an error was returned
   383  //
   384  // Deprecated: instead you should return an error that fulfills cli.ExitCoder
   385  // to cli.App.Run. This will cause the application to exit with the given error
   386  // code in the cli.ExitCoder
   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  // Command returns the named command on App. Returns nil if the command does not exist
   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  // VisibleCategories returns a slice of categories and commands that are
   406  // Hidden=false
   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  // VisibleCommands returns a slice of the Commands with Hidden=false
   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  // VisibleFlagCategories returns a slice containing all the categories with the flags they contain
   434  func (a *App) VisibleFlagCategories() []VisibleFlagCategory {
   435  	if a.flagCategories == nil {
   436  		return []VisibleFlagCategory{}
   437  	}
   438  	return a.flagCategories.VisibleCategories()
   439  }
   440  
   441  // VisibleFlags returns a slice of the Flags with Hidden=false
   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  // Author represents someone who has contributed to a cli project.
   498  type Author struct {
   499  	Name  string // The Authors name
   500  	Email string // The Authors email
   501  }
   502  
   503  // String makes Author comply to the Stringer interface, to allow an easy print in the templating process
   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  // HandleAction attempts to figure out which Action signature was used.  If
   514  // it's an ActionFunc or a func with the legacy signature for Action, the func
   515  // is run!
   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): // deprecated function signature
   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