tractor.dev/toolkit-go@v0.0.0-20241010005851-214d91207d07/engine/cli/command.go (about)

     1  package cli
     2  
     3  import (
     4  	"bytes"
     5  	"flag"
     6  	"fmt"
     7  	"strings"
     8  )
     9  
    10  // Command is a command or subcommand that can be run with Execute.
    11  type Command struct {
    12  	// Use is the one-line usage message.
    13  	// Recommended syntax is as follow:
    14  	//   [ ] identifies an optional argument. Arguments that are not enclosed in brackets are required.
    15  	//   ... indicates that you can specify multiple values for the previous argument.
    16  	//   |   indicates mutually exclusive information. You can use the argument to the left of the separator or the
    17  	//       argument to the right of the separator. You cannot use both arguments in a single use of the command.
    18  	//   { } delimits a set of mutually exclusive arguments when one of the arguments is required. If the arguments are
    19  	//       optional, they are enclosed in brackets ([ ]).
    20  	// Example: add [-F file | -D dir]... [-f format] <profile>
    21  	Usage string
    22  
    23  	// Short is the short description shown in the 'help' output.
    24  	Short string
    25  
    26  	// Long is the long message shown in the 'help <this-command>' output.
    27  	Long string
    28  
    29  	// Hidden defines, if this command is hidden and should NOT show up in the list of available commands.
    30  	Hidden bool
    31  
    32  	// Aliases is an array of aliases that can be used instead of the first word in Use.
    33  	Aliases []string
    34  
    35  	// Example is examples of how to use the command.
    36  	Example string
    37  
    38  	// Annotations are key/value pairs that can be used by applications to identify or
    39  	// group commands.
    40  	Annotations map[string]interface{}
    41  
    42  	// Version defines the version for this command. If this value is non-empty and the command does not
    43  	// define a "version" flag, a "version" boolean flag will be added to the command and, if specified,
    44  	// will print content of the "Version" variable. A shorthand "v" flag will also be added if the
    45  	// command does not define one.
    46  	Version string
    47  
    48  	// Expected arguments
    49  	Args PositionalArgs
    50  
    51  	// Run is the function that performs the command
    52  	Run func(ctx *Context, args []string)
    53  
    54  	commands []*Command
    55  	parent   *Command
    56  	flags    *flag.FlagSet
    57  }
    58  
    59  // Flags returns the complete FlagSet that applies to this command.
    60  func (c *Command) Flags() *flag.FlagSet {
    61  	if c.flags == nil {
    62  		c.flags = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
    63  		var null bytes.Buffer
    64  		c.flags.SetOutput(&null)
    65  	}
    66  	return c.flags
    67  }
    68  
    69  // AddCommand adds one or more commands to this parent command.
    70  func (c *Command) AddCommand(sub *Command) {
    71  	if sub == c {
    72  		panic("command can't be a child of itself")
    73  	}
    74  	sub.parent = c
    75  	c.commands = append(c.commands, sub)
    76  }
    77  
    78  // CommandPath returns the full path to this command.
    79  func (c *Command) CommandPath() string {
    80  	if c.parent != nil {
    81  		return c.parent.CommandPath() + " " + c.Name()
    82  	}
    83  	return c.Name()
    84  }
    85  
    86  // UseLine puts out the full usage for a given command (including parents).
    87  func (c *Command) UseLine() string {
    88  	if c.parent != nil {
    89  		return c.parent.CommandPath() + " " + c.Usage
    90  	} else {
    91  		return c.Usage
    92  	}
    93  }
    94  
    95  // Name returns the command's name: the first word in the usage line.
    96  func (c *Command) Name() string {
    97  	name := c.Usage
    98  	i := strings.Index(name, " ")
    99  	if i >= 0 {
   100  		name = name[:i]
   101  	}
   102  	return name
   103  }
   104  
   105  // Find the target command given the args and command tree.
   106  // Meant to be run on the highest node. Only searches down.
   107  // Also returns the arguments consumed to reach the command.
   108  func (c *Command) Find(args []string) (cmd *Command, n int) {
   109  	cmd = c
   110  	if len(args) == 0 {
   111  		return
   112  	}
   113  	var arg string
   114  	for n, arg = range args {
   115  		if cc := cmd.findSub(arg); cc != nil {
   116  			cmd = cc
   117  		} else {
   118  			return
   119  		}
   120  	}
   121  	n += 1
   122  	return
   123  }
   124  
   125  func (c *Command) findSub(name string) *Command {
   126  	for _, cmd := range c.commands {
   127  		if cmd.Name() == name || hasAlias(cmd, name) {
   128  			return cmd
   129  		}
   130  	}
   131  	return nil
   132  }
   133  
   134  func hasAlias(cmd *Command, name string) bool {
   135  	for _, a := range cmd.Aliases {
   136  		if a == name {
   137  			return true
   138  		}
   139  	}
   140  	return false
   141  }
   142  
   143  // PositionalArgs is a function type used by the Command Args field
   144  // for detecting whether the arguments match a given expectation.
   145  type PositionalArgs func(cmd *Command, args []string) error
   146  
   147  // MinArgs returns an error if there is not at least N args.
   148  func MinArgs(n int) PositionalArgs {
   149  	return func(cmd *Command, args []string) error {
   150  		if len(args) < n {
   151  			return fmt.Errorf("requires at least %d arg(s), only received %d", n, len(args))
   152  		}
   153  		return nil
   154  	}
   155  }
   156  
   157  // MaxArgs returns an error if there are more than N args.
   158  func MaxArgs(n int) PositionalArgs {
   159  	return func(cmd *Command, args []string) error {
   160  		if len(args) > n {
   161  			return fmt.Errorf("accepts at most %d arg(s), received %d", n, len(args))
   162  		}
   163  		return nil
   164  	}
   165  }
   166  
   167  // ExactArgs returns an error if there are not exactly n args.
   168  func ExactArgs(n int) PositionalArgs {
   169  	return func(cmd *Command, args []string) error {
   170  		if len(args) != n {
   171  			return fmt.Errorf("accepts %d arg(s), received %d", n, len(args))
   172  		}
   173  		return nil
   174  	}
   175  }
   176  
   177  // RangeArgs returns an error if the number of args is not within the expected range.
   178  func RangeArgs(min int, max int) PositionalArgs {
   179  	return func(cmd *Command, args []string) error {
   180  		if len(args) < min || len(args) > max {
   181  			return fmt.Errorf("accepts between %d and %d arg(s), received %d", min, max, len(args))
   182  		}
   183  		return nil
   184  	}
   185  }