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 }