You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

command.go 9.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. package cli
  2. import (
  3. "flag"
  4. "fmt"
  5. "sort"
  6. "strings"
  7. )
  8. // Command is a subcommand for a cli.App.
  9. type Command struct {
  10. // The name of the command
  11. Name string
  12. // short name of the command. Typically one character (deprecated, use `Aliases`)
  13. ShortName string
  14. // A list of aliases for the command
  15. Aliases []string
  16. // A short description of the usage of this command
  17. Usage string
  18. // Custom text to show on USAGE section of help
  19. UsageText string
  20. // A longer explanation of how the command works
  21. Description string
  22. // A short description of the arguments of this command
  23. ArgsUsage string
  24. // The category the command is part of
  25. Category string
  26. // The function to call when checking for bash command completions
  27. BashComplete BashCompleteFunc
  28. // An action to execute before any sub-subcommands are run, but after the context is ready
  29. // If a non-nil error is returned, no sub-subcommands are run
  30. Before BeforeFunc
  31. // An action to execute after any subcommands are run, but after the subcommand has finished
  32. // It is run even if Action() panics
  33. After AfterFunc
  34. // The function to call when this command is invoked
  35. Action interface{}
  36. // TODO: replace `Action: interface{}` with `Action: ActionFunc` once some kind
  37. // of deprecation period has passed, maybe?
  38. // Execute this function if a usage error occurs.
  39. OnUsageError OnUsageErrorFunc
  40. // List of child commands
  41. Subcommands Commands
  42. // List of flags to parse
  43. Flags []Flag
  44. // Treat all flags as normal arguments if true
  45. SkipFlagParsing bool
  46. // Skip argument reordering which attempts to move flags before arguments,
  47. // but only works if all flags appear after all arguments. This behavior was
  48. // removed n version 2 since it only works under specific conditions so we
  49. // backport here by exposing it as an option for compatibility.
  50. SkipArgReorder bool
  51. // Boolean to hide built-in help command
  52. HideHelp bool
  53. // Boolean to hide this command from help or completion
  54. Hidden bool
  55. // Boolean to enable short-option handling so user can combine several
  56. // single-character bool arguments into one
  57. // i.e. foobar -o -v -> foobar -ov
  58. UseShortOptionHandling bool
  59. // Full name of command for help, defaults to full command name, including parent commands.
  60. HelpName string
  61. commandNamePath []string
  62. // CustomHelpTemplate the text template for the command help topic.
  63. // cli.go uses text/template to render templates. You can
  64. // render custom help text by setting this variable.
  65. CustomHelpTemplate string
  66. }
  67. type CommandsByName []Command
  68. func (c CommandsByName) Len() int {
  69. return len(c)
  70. }
  71. func (c CommandsByName) Less(i, j int) bool {
  72. return lexicographicLess(c[i].Name, c[j].Name)
  73. }
  74. func (c CommandsByName) Swap(i, j int) {
  75. c[i], c[j] = c[j], c[i]
  76. }
  77. // FullName returns the full name of the command.
  78. // For subcommands this ensures that parent commands are part of the command path
  79. func (c Command) FullName() string {
  80. if c.commandNamePath == nil {
  81. return c.Name
  82. }
  83. return strings.Join(c.commandNamePath, " ")
  84. }
  85. // Commands is a slice of Command
  86. type Commands []Command
  87. // Run invokes the command given the context, parses ctx.Args() to generate command-specific flags
  88. func (c Command) Run(ctx *Context) (err error) {
  89. if len(c.Subcommands) > 0 {
  90. return c.startApp(ctx)
  91. }
  92. if !c.HideHelp && (HelpFlag != BoolFlag{}) {
  93. // append help to flags
  94. c.Flags = append(
  95. c.Flags,
  96. HelpFlag,
  97. )
  98. }
  99. if ctx.App.UseShortOptionHandling {
  100. c.UseShortOptionHandling = true
  101. }
  102. set, err := c.parseFlags(ctx.Args().Tail(), ctx.shellComplete)
  103. context := NewContext(ctx.App, set, ctx)
  104. context.Command = c
  105. if checkCommandCompletions(context, c.Name) {
  106. return nil
  107. }
  108. if err != nil {
  109. if c.OnUsageError != nil {
  110. err := c.OnUsageError(context, err, false)
  111. context.App.handleExitCoder(context, err)
  112. return err
  113. }
  114. _, _ = fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error())
  115. _, _ = fmt.Fprintln(context.App.Writer)
  116. _ = ShowCommandHelp(context, c.Name)
  117. return err
  118. }
  119. if checkCommandHelp(context, c.Name) {
  120. return nil
  121. }
  122. cerr := checkRequiredFlags(c.Flags, context)
  123. if cerr != nil {
  124. _ = ShowCommandHelp(context, c.Name)
  125. return cerr
  126. }
  127. if c.After != nil {
  128. defer func() {
  129. afterErr := c.After(context)
  130. if afterErr != nil {
  131. context.App.handleExitCoder(context, err)
  132. if err != nil {
  133. err = NewMultiError(err, afterErr)
  134. } else {
  135. err = afterErr
  136. }
  137. }
  138. }()
  139. }
  140. if c.Before != nil {
  141. err = c.Before(context)
  142. if err != nil {
  143. context.App.handleExitCoder(context, err)
  144. return err
  145. }
  146. }
  147. if c.Action == nil {
  148. c.Action = helpSubcommand.Action
  149. }
  150. err = HandleAction(c.Action, context)
  151. if err != nil {
  152. context.App.handleExitCoder(context, err)
  153. }
  154. return err
  155. }
  156. func (c *Command) parseFlags(args Args, shellComplete bool) (*flag.FlagSet, error) {
  157. if c.SkipFlagParsing {
  158. set, err := c.newFlagSet()
  159. if err != nil {
  160. return nil, err
  161. }
  162. return set, set.Parse(append([]string{"--"}, args...))
  163. }
  164. if !c.SkipArgReorder {
  165. args = reorderArgs(c.Flags, args)
  166. }
  167. set, err := c.newFlagSet()
  168. if err != nil {
  169. return nil, err
  170. }
  171. err = parseIter(set, c, args, shellComplete)
  172. if err != nil {
  173. return nil, err
  174. }
  175. err = normalizeFlags(c.Flags, set)
  176. if err != nil {
  177. return nil, err
  178. }
  179. return set, nil
  180. }
  181. func (c *Command) newFlagSet() (*flag.FlagSet, error) {
  182. return flagSet(c.Name, c.Flags)
  183. }
  184. func (c *Command) useShortOptionHandling() bool {
  185. return c.UseShortOptionHandling
  186. }
  187. // reorderArgs moves all flags (via reorderedArgs) before the rest of
  188. // the arguments (remainingArgs) as this is what flag expects.
  189. func reorderArgs(commandFlags []Flag, args []string) []string {
  190. var remainingArgs, reorderedArgs []string
  191. nextIndexMayContainValue := false
  192. for i, arg := range args {
  193. // dont reorder any args after a --
  194. // read about -- here:
  195. // https://unix.stackexchange.com/questions/11376/what-does-double-dash-mean-also-known-as-bare-double-dash
  196. if arg == "--" {
  197. remainingArgs = append(remainingArgs, args[i:]...)
  198. break
  199. // checks if this arg is a value that should be re-ordered next to its associated flag
  200. } else if nextIndexMayContainValue && !strings.HasPrefix(arg, "-") {
  201. nextIndexMayContainValue = false
  202. reorderedArgs = append(reorderedArgs, arg)
  203. // checks if this is an arg that should be re-ordered
  204. } else if argIsFlag(commandFlags, arg) {
  205. // we have determined that this is a flag that we should re-order
  206. reorderedArgs = append(reorderedArgs, arg)
  207. // if this arg does not contain a "=", then the next index may contain the value for this flag
  208. nextIndexMayContainValue = !strings.Contains(arg, "=")
  209. // simply append any remaining args
  210. } else {
  211. remainingArgs = append(remainingArgs, arg)
  212. }
  213. }
  214. return append(reorderedArgs, remainingArgs...)
  215. }
  216. // argIsFlag checks if an arg is one of our command flags
  217. func argIsFlag(commandFlags []Flag, arg string) bool {
  218. // checks if this is just a `-`, and so definitely not a flag
  219. if arg == "-" {
  220. return false
  221. }
  222. // flags always start with a -
  223. if !strings.HasPrefix(arg, "-") {
  224. return false
  225. }
  226. // this line turns `--flag` into `flag`
  227. if strings.HasPrefix(arg, "--") {
  228. arg = strings.Replace(arg, "-", "", 2)
  229. }
  230. // this line turns `-flag` into `flag`
  231. if strings.HasPrefix(arg, "-") {
  232. arg = strings.Replace(arg, "-", "", 1)
  233. }
  234. // this line turns `flag=value` into `flag`
  235. arg = strings.Split(arg, "=")[0]
  236. // look through all the flags, to see if the `arg` is one of our flags
  237. for _, flag := range commandFlags {
  238. for _, key := range strings.Split(flag.GetName(), ",") {
  239. key := strings.TrimSpace(key)
  240. if key == arg {
  241. return true
  242. }
  243. }
  244. }
  245. // return false if this arg was not one of our flags
  246. return false
  247. }
  248. // Names returns the names including short names and aliases.
  249. func (c Command) Names() []string {
  250. names := []string{c.Name}
  251. if c.ShortName != "" {
  252. names = append(names, c.ShortName)
  253. }
  254. return append(names, c.Aliases...)
  255. }
  256. // HasName returns true if Command.Name or Command.ShortName matches given name
  257. func (c Command) HasName(name string) bool {
  258. for _, n := range c.Names() {
  259. if n == name {
  260. return true
  261. }
  262. }
  263. return false
  264. }
  265. func (c Command) startApp(ctx *Context) error {
  266. app := NewApp()
  267. app.Metadata = ctx.App.Metadata
  268. app.ExitErrHandler = ctx.App.ExitErrHandler
  269. // set the name and usage
  270. app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name)
  271. if c.HelpName == "" {
  272. app.HelpName = c.HelpName
  273. } else {
  274. app.HelpName = app.Name
  275. }
  276. app.Usage = c.Usage
  277. app.Description = c.Description
  278. app.ArgsUsage = c.ArgsUsage
  279. // set CommandNotFound
  280. app.CommandNotFound = ctx.App.CommandNotFound
  281. app.CustomAppHelpTemplate = c.CustomHelpTemplate
  282. // set the flags and commands
  283. app.Commands = c.Subcommands
  284. app.Flags = c.Flags
  285. app.HideHelp = c.HideHelp
  286. app.Version = ctx.App.Version
  287. app.HideVersion = ctx.App.HideVersion
  288. app.Compiled = ctx.App.Compiled
  289. app.Author = ctx.App.Author
  290. app.Email = ctx.App.Email
  291. app.Writer = ctx.App.Writer
  292. app.ErrWriter = ctx.App.ErrWriter
  293. app.UseShortOptionHandling = ctx.App.UseShortOptionHandling
  294. app.categories = CommandCategories{}
  295. for _, command := range c.Subcommands {
  296. app.categories = app.categories.AddCommand(command.Category, command)
  297. }
  298. sort.Sort(app.categories)
  299. // bash completion
  300. app.EnableBashCompletion = ctx.App.EnableBashCompletion
  301. if c.BashComplete != nil {
  302. app.BashComplete = c.BashComplete
  303. }
  304. // set the actions
  305. app.Before = c.Before
  306. app.After = c.After
  307. if c.Action != nil {
  308. app.Action = c.Action
  309. } else {
  310. app.Action = helpSubcommand.Action
  311. }
  312. app.OnUsageError = c.OnUsageError
  313. for index, cc := range app.Commands {
  314. app.Commands[index].commandNamePath = []string{c.Name, cc.Name}
  315. }
  316. return app.RunAsSubcommand(ctx)
  317. }
  318. // VisibleFlags returns a slice of the Flags with Hidden=false
  319. func (c Command) VisibleFlags() []Flag {
  320. return visibleFlags(c.Flags)
  321. }