package cli import ( "flag" "fmt" "sort" "strings" ) // Command is a subcommand for a cli.App. type Command struct { // The name of the command Name string // short name of the command. Typically one character (deprecated, use `Aliases`) ShortName string // A list of aliases for the command Aliases []string // A short description of the usage of this command Usage string // Custom text to show on USAGE section of help UsageText string // A longer explanation of how the command works Description string // A short description of the arguments of this command ArgsUsage string // The category the command is part of Category string // The function to call when checking for bash command completions BashComplete BashCompleteFunc // An action to execute before any sub-subcommands are run, but after the context is ready // If a non-nil error is returned, no sub-subcommands are run Before BeforeFunc // An action to execute after any subcommands are run, but after the subcommand has finished // It is run even if Action() panics After AfterFunc // The function to call when this command is invoked Action interface{} // TODO: replace `Action: interface{}` with `Action: ActionFunc` once some kind // of deprecation period has passed, maybe? // Execute this function if a usage error occurs. OnUsageError OnUsageErrorFunc // List of child commands Subcommands Commands // List of flags to parse Flags []Flag // Treat all flags as normal arguments if true SkipFlagParsing bool // Skip argument reordering which attempts to move flags before arguments, // but only works if all flags appear after all arguments. This behavior was // removed n version 2 since it only works under specific conditions so we // backport here by exposing it as an option for compatibility. SkipArgReorder bool // Boolean to hide built-in help command HideHelp bool // Boolean to hide this command from help or completion Hidden bool // Boolean to enable short-option handling so user can combine several // single-character bool arguments into one // i.e. foobar -o -v -> foobar -ov UseShortOptionHandling bool // Full name of command for help, defaults to full command name, including parent commands. HelpName string commandNamePath []string // CustomHelpTemplate the text template for the command help topic. // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. CustomHelpTemplate string } type CommandsByName []Command func (c CommandsByName) Len() int { return len(c) } func (c CommandsByName) Less(i, j int) bool { return lexicographicLess(c[i].Name, c[j].Name) } func (c CommandsByName) Swap(i, j int) { c[i], c[j] = c[j], c[i] } // FullName returns the full name of the command. // For subcommands this ensures that parent commands are part of the command path func (c Command) FullName() string { if c.commandNamePath == nil { return c.Name } return strings.Join(c.commandNamePath, " ") } // Commands is a slice of Command type Commands []Command // Run invokes the command given the context, parses ctx.Args() to generate command-specific flags func (c Command) Run(ctx *Context) (err error) { if len(c.Subcommands) > 0 { return c.startApp(ctx) } if !c.HideHelp && (HelpFlag != BoolFlag{}) { // append help to flags c.Flags = append( c.Flags, HelpFlag, ) } if ctx.App.UseShortOptionHandling { c.UseShortOptionHandling = true } set, err := c.parseFlags(ctx.Args().Tail(), ctx.shellComplete) context := NewContext(ctx.App, set, ctx) context.Command = c if checkCommandCompletions(context, c.Name) { return nil } if err != nil { if c.OnUsageError != nil { err := c.OnUsageError(context, err, false) context.App.handleExitCoder(context, err) return err } _, _ = fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error()) _, _ = fmt.Fprintln(context.App.Writer) _ = ShowCommandHelp(context, c.Name) return err } if checkCommandHelp(context, c.Name) { return nil } cerr := checkRequiredFlags(c.Flags, context) if cerr != nil { _ = ShowCommandHelp(context, c.Name) return cerr } if c.After != nil { defer func() { afterErr := c.After(context) if afterErr != nil { context.App.handleExitCoder(context, err) if err != nil { err = NewMultiError(err, afterErr) } else { err = afterErr } } }() } if c.Before != nil { err = c.Before(context) if err != nil { context.App.handleExitCoder(context, err) return err } } if c.Action == nil { c.Action = helpSubcommand.Action } err = HandleAction(c.Action, context) if err != nil { context.App.handleExitCoder(context, err) } return err } func (c *Command) parseFlags(args Args, shellComplete bool) (*flag.FlagSet, error) { if c.SkipFlagParsing { set, err := c.newFlagSet() if err != nil { return nil, err } return set, set.Parse(append([]string{"--"}, args...)) } if !c.SkipArgReorder { args = reorderArgs(c.Flags, args) } set, err := c.newFlagSet() if err != nil { return nil, err } err = parseIter(set, c, args, shellComplete) if err != nil { return nil, err } err = normalizeFlags(c.Flags, set) if err != nil { return nil, err } return set, nil } func (c *Command) newFlagSet() (*flag.FlagSet, error) { return flagSet(c.Name, c.Flags) } func (c *Command) useShortOptionHandling() bool { return c.UseShortOptionHandling } // reorderArgs moves all flags (via reorderedArgs) before the rest of // the arguments (remainingArgs) as this is what flag expects. func reorderArgs(commandFlags []Flag, args []string) []string { var remainingArgs, reorderedArgs []string nextIndexMayContainValue := false for i, arg := range args { // if we're expecting an option-value, check if this arg is a value, in // which case it should be re-ordered next to its associated flag if nextIndexMayContainValue && !argIsFlag(commandFlags, arg) { nextIndexMayContainValue = false reorderedArgs = append(reorderedArgs, arg) } else if arg == "--" { // don't reorder any args after the -- delimiter As described in the POSIX spec: // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02 // > Guideline 10: // > The first -- argument that is not an option-argument should be accepted // > as a delimiter indicating the end of options. Any following arguments // > should be treated as operands, even if they begin with the '-' character. // make sure the "--" delimiter itself is at the start remainingArgs = append([]string{"--"}, remainingArgs...) remainingArgs = append(remainingArgs, args[i+1:]...) break // checks if this is an arg that should be re-ordered } else if argIsFlag(commandFlags, arg) { // we have determined that this is a flag that we should re-order reorderedArgs = append(reorderedArgs, arg) // if this arg does not contain a "=", then the next index may contain the value for this flag nextIndexMayContainValue = !strings.Contains(arg, "=") // simply append any remaining args } else { remainingArgs = append(remainingArgs, arg) } } return append(reorderedArgs, remainingArgs...) } // argIsFlag checks if an arg is one of our command flags func argIsFlag(commandFlags []Flag, arg string) bool { if arg == "-" || arg == "--"{ // `-` is never a flag // `--` is an option-value when following a flag, and a delimiter indicating the end of options in other cases. return false } // flags always start with a - if !strings.HasPrefix(arg, "-") { return false } // this line turns `--flag` into `flag` if strings.HasPrefix(arg, "--") { arg = strings.Replace(arg, "-", "", 2) } // this line turns `-flag` into `flag` if strings.HasPrefix(arg, "-") { arg = strings.Replace(arg, "-", "", 1) } // this line turns `flag=value` into `flag` arg = strings.Split(arg, "=")[0] // look through all the flags, to see if the `arg` is one of our flags for _, flag := range commandFlags { for _, key := range strings.Split(flag.GetName(), ",") { key := strings.TrimSpace(key) if key == arg { return true } } } // return false if this arg was not one of our flags return false } // Names returns the names including short names and aliases. func (c Command) Names() []string { names := []string{c.Name} if c.ShortName != "" { names = append(names, c.ShortName) } return append(names, c.Aliases...) } // HasName returns true if Command.Name or Command.ShortName matches given name func (c Command) HasName(name string) bool { for _, n := range c.Names() { if n == name { return true } } return false } func (c Command) startApp(ctx *Context) error { app := NewApp() app.Metadata = ctx.App.Metadata app.ExitErrHandler = ctx.App.ExitErrHandler // set the name and usage app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name) if c.HelpName == "" { app.HelpName = c.HelpName } else { app.HelpName = app.Name } app.Usage = c.Usage app.Description = c.Description app.ArgsUsage = c.ArgsUsage // set CommandNotFound app.CommandNotFound = ctx.App.CommandNotFound app.CustomAppHelpTemplate = c.CustomHelpTemplate // set the flags and commands app.Commands = c.Subcommands app.Flags = c.Flags app.HideHelp = c.HideHelp app.Version = ctx.App.Version app.HideVersion = ctx.App.HideVersion app.Compiled = ctx.App.Compiled app.Author = ctx.App.Author app.Email = ctx.App.Email app.Writer = ctx.App.Writer app.ErrWriter = ctx.App.ErrWriter app.UseShortOptionHandling = ctx.App.UseShortOptionHandling app.categories = CommandCategories{} for _, command := range c.Subcommands { app.categories = app.categories.AddCommand(command.Category, command) } sort.Sort(app.categories) // bash completion app.EnableBashCompletion = ctx.App.EnableBashCompletion if c.BashComplete != nil { app.BashComplete = c.BashComplete } // set the actions app.Before = c.Before app.After = c.After if c.Action != nil { app.Action = c.Action } else { app.Action = helpSubcommand.Action } app.OnUsageError = c.OnUsageError for index, cc := range app.Commands { app.Commands[index].commandNamePath = []string{c.Name, cc.Name} } return app.RunAsSubcommand(ctx) } // VisibleFlags returns a slice of the Flags with Hidden=false func (c Command) VisibleFlags() []Flag { return visibleFlags(c.Flags) }