422 lines
9.8 KiB
Go
422 lines
9.8 KiB
Go
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
|
|
}
|