| package cli |
| |
| import ( |
| "flag" |
| "fmt" |
| "reflect" |
| "sort" |
| "strings" |
| ) |
| |
| // Command is a subcommand for a cli.App. |
| type Command struct { |
| // The name of the command |
| Name 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 |
| // Whether this command supports arguments |
| Args bool |
| // 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 ActionFunc |
| // Execute this function if a usage error occurs. |
| OnUsageError OnUsageErrorFunc |
| // List of child commands |
| Subcommands []*Command |
| // List of flags to parse |
| Flags []Flag |
| flagCategories FlagCategories |
| // Treat all flags as normal arguments if true |
| SkipFlagParsing bool |
| // Boolean to hide built-in help command and help flag |
| HideHelp bool |
| // Boolean to hide built-in help command but keep help flag |
| // Ignored if HideHelp is true. |
| HideHelpCommand 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 |
| |
| // categories contains the categorized commands and is populated on app startup |
| categories CommandCategories |
| |
| // if this is a root "special" command |
| isRoot bool |
| |
| separator separatorSpec |
| } |
| |
| type Commands []*Command |
| |
| 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, " ") |
| } |
| |
| func (cmd *Command) Command(name string) *Command { |
| for _, c := range cmd.Subcommands { |
| if c.HasName(name) { |
| return c |
| } |
| } |
| |
| return nil |
| } |
| |
| func (c *Command) setup(ctx *Context) { |
| if c.Command(helpCommand.Name) == nil && !c.HideHelp { |
| if !c.HideHelpCommand { |
| c.Subcommands = append(c.Subcommands, helpCommand) |
| } |
| } |
| |
| if !c.HideHelp && HelpFlag != nil { |
| // append help to flags |
| c.appendFlag(HelpFlag) |
| } |
| |
| if ctx.App.UseShortOptionHandling { |
| c.UseShortOptionHandling = true |
| } |
| |
| c.categories = newCommandCategories() |
| for _, command := range c.Subcommands { |
| c.categories.AddCommand(command.Category, command) |
| } |
| sort.Sort(c.categories.(*commandCategories)) |
| |
| for _, scmd := range c.Subcommands { |
| if scmd.HelpName == "" { |
| scmd.HelpName = fmt.Sprintf("%s %s", c.HelpName, scmd.Name) |
| } |
| scmd.separator = c.separator |
| } |
| |
| if c.BashComplete == nil { |
| c.BashComplete = DefaultCompleteWithFlags(c) |
| } |
| } |
| |
| func (c *Command) Run(cCtx *Context, arguments ...string) (err error) { |
| |
| if !c.isRoot { |
| c.setup(cCtx) |
| if err := checkDuplicatedCmds(c); err != nil { |
| return err |
| } |
| } |
| |
| a := args(arguments) |
| set, err := c.parseFlags(&a, cCtx.shellComplete) |
| cCtx.flagSet = set |
| |
| if checkCompletions(cCtx) { |
| return nil |
| } |
| |
| if err != nil { |
| if c.OnUsageError != nil { |
| err = c.OnUsageError(cCtx, err, !c.isRoot) |
| cCtx.App.handleExitCoder(cCtx, err) |
| return err |
| } |
| _, _ = fmt.Fprintf(cCtx.App.Writer, "%s %s\n\n", "Incorrect Usage:", err.Error()) |
| if cCtx.App.Suggest { |
| if suggestion, err := c.suggestFlagFromError(err, ""); err == nil { |
| fmt.Fprintf(cCtx.App.Writer, "%s", suggestion) |
| } |
| } |
| if !c.HideHelp { |
| if c.isRoot { |
| _ = ShowAppHelp(cCtx) |
| } else { |
| _ = ShowCommandHelp(cCtx.parentContext, c.Name) |
| } |
| } |
| return err |
| } |
| |
| if checkHelp(cCtx) { |
| return helpCommand.Action(cCtx) |
| } |
| |
| if c.isRoot && !cCtx.App.HideVersion && checkVersion(cCtx) { |
| ShowVersion(cCtx) |
| return nil |
| } |
| |
| if c.After != nil && !cCtx.shellComplete { |
| defer func() { |
| afterErr := c.After(cCtx) |
| if afterErr != nil { |
| cCtx.App.handleExitCoder(cCtx, err) |
| if err != nil { |
| err = newMultiError(err, afterErr) |
| } else { |
| err = afterErr |
| } |
| } |
| }() |
| } |
| |
| cerr := cCtx.checkRequiredFlags(c.Flags) |
| if cerr != nil { |
| _ = helpCommand.Action(cCtx) |
| return cerr |
| } |
| |
| if c.Before != nil && !cCtx.shellComplete { |
| beforeErr := c.Before(cCtx) |
| if beforeErr != nil { |
| cCtx.App.handleExitCoder(cCtx, beforeErr) |
| err = beforeErr |
| return err |
| } |
| } |
| |
| if err = runFlagActions(cCtx, c.Flags); err != nil { |
| return err |
| } |
| |
| var cmd *Command |
| args := cCtx.Args() |
| if args.Present() { |
| name := args.First() |
| cmd = c.Command(name) |
| if cmd == nil { |
| hasDefault := cCtx.App.DefaultCommand != "" |
| isFlagName := checkStringSliceIncludes(name, cCtx.FlagNames()) |
| |
| var ( |
| isDefaultSubcommand = false |
| defaultHasSubcommands = false |
| ) |
| |
| if hasDefault { |
| dc := cCtx.App.Command(cCtx.App.DefaultCommand) |
| defaultHasSubcommands = len(dc.Subcommands) > 0 |
| for _, dcSub := range dc.Subcommands { |
| if checkStringSliceIncludes(name, dcSub.Names()) { |
| isDefaultSubcommand = true |
| break |
| } |
| } |
| } |
| |
| if isFlagName || (hasDefault && (defaultHasSubcommands && isDefaultSubcommand)) { |
| argsWithDefault := cCtx.App.argsWithDefaultCommand(args) |
| if !reflect.DeepEqual(args, argsWithDefault) { |
| cmd = cCtx.App.rootCommand.Command(argsWithDefault.First()) |
| } |
| } |
| } |
| } else if c.isRoot && cCtx.App.DefaultCommand != "" { |
| if dc := cCtx.App.Command(cCtx.App.DefaultCommand); dc != c { |
| cmd = dc |
| } |
| } |
| |
| if cmd != nil { |
| newcCtx := NewContext(cCtx.App, nil, cCtx) |
| newcCtx.Command = cmd |
| return cmd.Run(newcCtx, cCtx.Args().Slice()...) |
| } |
| |
| if c.Action == nil { |
| c.Action = helpCommand.Action |
| } |
| |
| err = c.Action(cCtx) |
| |
| cCtx.App.handleExitCoder(cCtx, err) |
| return err |
| } |
| |
| func (c *Command) newFlagSet() (*flag.FlagSet, error) { |
| return flagSet(c.Name, c.Flags, c.separator) |
| } |
| |
| func (c *Command) useShortOptionHandling() bool { |
| return c.UseShortOptionHandling |
| } |
| |
| func (c *Command) suggestFlagFromError(err error, command string) (string, error) { |
| flag, parseErr := flagFromError(err) |
| if parseErr != nil { |
| return "", err |
| } |
| |
| flags := c.Flags |
| hideHelp := c.HideHelp |
| if command != "" { |
| cmd := c.Command(command) |
| if cmd == nil { |
| return "", err |
| } |
| flags = cmd.Flags |
| hideHelp = hideHelp || cmd.HideHelp |
| } |
| |
| suggestion := SuggestFlag(flags, flag, hideHelp) |
| if len(suggestion) == 0 { |
| return "", err |
| } |
| |
| return fmt.Sprintf(SuggestDidYouMeanTemplate, suggestion) + "\n\n", nil |
| } |
| |
| func (c *Command) parseFlags(args Args, shellComplete bool) (*flag.FlagSet, error) { |
| set, err := c.newFlagSet() |
| if err != nil { |
| return nil, err |
| } |
| |
| if c.SkipFlagParsing { |
| return set, set.Parse(append([]string{"--"}, args.Tail()...)) |
| } |
| |
| err = parseIter(set, c, args.Tail(), shellComplete) |
| if err != nil { |
| return nil, err |
| } |
| |
| err = normalizeFlags(c.Flags, set) |
| if err != nil { |
| return nil, err |
| } |
| |
| return set, nil |
| } |
| |
| // Names returns the names including short names and aliases. |
| func (c *Command) Names() []string { |
| return append([]string{c.Name}, c.Aliases...) |
| } |
| |
| // HasName returns true if Command.Name matches given name |
| func (c *Command) HasName(name string) bool { |
| for _, n := range c.Names() { |
| if n == name { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // VisibleCategories returns a slice of categories and commands that are |
| // Hidden=false |
| func (c *Command) VisibleCategories() []CommandCategory { |
| ret := []CommandCategory{} |
| for _, category := range c.categories.Categories() { |
| if visible := func() CommandCategory { |
| if len(category.VisibleCommands()) > 0 { |
| return category |
| } |
| return nil |
| }(); visible != nil { |
| ret = append(ret, visible) |
| } |
| } |
| return ret |
| } |
| |
| // VisibleCommands returns a slice of the Commands with Hidden=false |
| func (c *Command) VisibleCommands() []*Command { |
| var ret []*Command |
| for _, command := range c.Subcommands { |
| if !command.Hidden { |
| ret = append(ret, command) |
| } |
| } |
| return ret |
| } |
| |
| // VisibleFlagCategories returns a slice containing all the visible flag categories with the flags they contain |
| func (c *Command) VisibleFlagCategories() []VisibleFlagCategory { |
| if c.flagCategories == nil { |
| c.flagCategories = newFlagCategoriesFromFlags(c.Flags) |
| } |
| return c.flagCategories.VisibleCategories() |
| } |
| |
| // VisibleFlags returns a slice of the Flags with Hidden=false |
| func (c *Command) VisibleFlags() []Flag { |
| return visibleFlags(c.Flags) |
| } |
| |
| func (c *Command) appendFlag(fl Flag) { |
| if !hasFlag(c.Flags, fl) { |
| c.Flags = append(c.Flags, fl) |
| } |
| } |
| |
| func hasCommand(commands []*Command, command *Command) bool { |
| for _, existing := range commands { |
| if command == existing { |
| return true |
| } |
| } |
| |
| return false |
| } |
| |
| func checkDuplicatedCmds(parent *Command) error { |
| seen := make(map[string]struct{}) |
| for _, c := range parent.Subcommands { |
| for _, name := range c.Names() { |
| if _, exists := seen[name]; exists { |
| return fmt.Errorf("parent command [%s] has duplicated subcommand name or alias: %s", parent.Name, name) |
| } |
| seen[name] = struct{}{} |
| } |
| } |
| return nil |
| } |