github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/commands/command.go (about) 1 package commands 2 3 import ( 4 "context" 5 "errors" 6 "flag" 7 "fmt" 8 "os" 9 "strings" 10 "text/tabwriter" 11 12 "github.com/peterbourgon/ff/v3" 13 ) 14 15 // Config defines the command config interface 16 // that holds flag values and execution logic 17 type Config interface { 18 // RegisterFlags registers the specific flags to the flagset 19 RegisterFlags(*flag.FlagSet) 20 } 21 22 // ExecMethod executes the command using the specified config 23 type ExecMethod func(ctx context.Context, args []string) error 24 25 // HelpExec is a standard exec method for displaying 26 // help information about a command 27 func HelpExec(_ context.Context, _ []string) error { 28 return flag.ErrHelp 29 } 30 31 // Metadata contains basic help 32 // information about a command 33 type Metadata struct { 34 Name string 35 ShortUsage string 36 ShortHelp string 37 LongHelp string 38 Options []ff.Option 39 } 40 41 // Command is a simple wrapper for gnoland commands. 42 type Command struct { 43 name string 44 shortUsage string 45 shortHelp string 46 longHelp string 47 options []ff.Option 48 cfg Config 49 flagSet *flag.FlagSet 50 subcommands []*Command 51 exec ExecMethod 52 selected *Command 53 args []string 54 } 55 56 func NewCommand( 57 meta Metadata, 58 config Config, 59 exec ExecMethod, 60 ) *Command { 61 command := &Command{ 62 name: meta.Name, 63 shortUsage: meta.ShortUsage, 64 shortHelp: meta.ShortHelp, 65 longHelp: meta.LongHelp, 66 options: meta.Options, 67 flagSet: flag.NewFlagSet(meta.Name, flag.ContinueOnError), 68 exec: exec, 69 cfg: config, 70 } 71 72 if config != nil { 73 // Register the base command flags 74 config.RegisterFlags(command.flagSet) 75 } 76 77 return command 78 } 79 80 // AddSubCommands adds a variable number of subcommands 81 // and registers common flags using the flagset 82 func (c *Command) AddSubCommands(cmds ...*Command) { 83 for _, cmd := range cmds { 84 if c.cfg != nil { 85 // Register the parent flagset with the child. 86 // The syntax is not intuitive, but the flagset being 87 // modified is the subcommand's, using the flags defined 88 // in the parent command 89 c.cfg.RegisterFlags(cmd.flagSet) 90 91 // Register the parent flagset with all the 92 // subcommands of the child as well 93 // (ex. grandparent flags are available in child commands) 94 registerFlagsWithSubcommands(c.cfg, cmd) 95 96 // Register the parent options with the child. 97 cmd.options = append(cmd.options, c.options...) 98 99 // Register the parent options with all the 100 // subcommands of the child as well 101 registerOptionsWithSubcommands(cmd) 102 } 103 104 // Append the subcommand to the parent 105 c.subcommands = append(c.subcommands, cmd) 106 } 107 } 108 109 // Execute is a helper function for command entry. It wraps ParseAndRun and 110 // handles the flag.ErrHelp error, ensuring that every command with -h or 111 // --help won't show an error message: 112 // 'error parsing commandline arguments: flag: help requested' 113 func (c *Command) Execute(ctx context.Context, args []string) { 114 if err := c.ParseAndRun(ctx, args); err != nil { 115 if !errors.Is(err, flag.ErrHelp) { 116 _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) 117 } 118 os.Exit(1) 119 } 120 } 121 122 // ParseAndRun is a helper function that calls Parse and then Run in a single 123 // invocation. It's useful for simple command trees that don't need two-phase 124 // setup. 125 // 126 // Forked from peterbourgon/ff/ffcli 127 func (c *Command) ParseAndRun(ctx context.Context, args []string) error { 128 if err := c.Parse(args); err != nil { 129 return err 130 } 131 132 if err := c.Run(ctx); err != nil { 133 return err 134 } 135 136 return nil 137 } 138 139 // Parse the commandline arguments for this command and all sub-commands 140 // recursively, defining flags along the way. If Parse returns without an error, 141 // the terminal command has been successfully identified, and may be invoked by 142 // calling Run. 143 // 144 // If the terminal command identified by Parse doesn't define an Exec function, 145 // then Parse will return NoExecError. 146 // 147 // Forked from peterbourgon/ff/ffcli 148 func (c *Command) Parse(args []string) error { 149 if c.selected != nil { 150 return nil 151 } 152 153 if c.flagSet == nil { 154 c.flagSet = flag.NewFlagSet(c.name, flag.ExitOnError) 155 } 156 157 c.flagSet.Usage = func() { 158 fmt.Fprintln(c.flagSet.Output(), usage(c)) 159 } 160 161 c.args = []string{} 162 // Use a loop to support flag declaration after arguments and subcommands. 163 // At the end of each iteration: 164 // - c.args receives the first argument found 165 // - args is truncated by anything that has been parsed 166 // The loop ends whether if: 167 // - no more arguments to parse 168 // - a double delimiter "--" is met 169 for { 170 // ff.Parse iterates over args, feeding FlagSet with the flags encountered. 171 // It stops when: 172 // 1) there's nothing more to parse. In that case, FlagSet.Args() is empty. 173 // 2) it encounters a double delimiter "--". In that case FlagSet.Args() 174 // contains everything that follows the double delimiter. 175 // 3) it encounters an item that is not a flag. In that case FlagSet.Args() 176 // contains that last item and everything that follows it. The item can be 177 // an argument or a subcommand. 178 if err := ff.Parse(c.flagSet, args, c.options...); err != nil { 179 return err 180 } 181 if c.flagSet.NArg() == 0 { 182 // 1) Nothing more to parse 183 break 184 } 185 // Determine if ff.Parse() has been interrupted by a double delimiter. 186 // This is case if the last parsed arg is a "--" 187 parsedArgs := args[:len(args)-c.flagSet.NArg()] 188 if n := len(parsedArgs); n > 0 && parsedArgs[n-1] == "--" { 189 // 2) Double delimiter has been met, everything that follow it can be 190 // considered as arguments. 191 c.args = append(c.args, c.flagSet.Args()...) 192 break 193 } 194 // 3) c.FlagSet.Arg(0) is not a flag, determine if it's an argument or a 195 // subcommand. 196 // NOTE: it can be a subcommand if and only if the argument list is empty. 197 // In other words, a command can't have both arguments and subcommands. 198 if len(c.args) == 0 { 199 for _, subcommand := range c.subcommands { 200 if strings.EqualFold(c.flagSet.Arg(0), subcommand.name) { 201 // c.FlagSet.Arg(0) is a subcommand 202 c.selected = subcommand 203 return subcommand.Parse(c.flagSet.Args()[1:]) 204 } 205 } 206 } 207 // c.FlagSet.Arg(0) is an argument, append it to the argument list 208 c.args = append(c.args, c.flagSet.Arg(0)) 209 // Truncate args and continue 210 args = c.flagSet.Args()[1:] 211 } 212 213 c.selected = c 214 215 if c.exec == nil { 216 return fmt.Errorf("command %s not executable", c.name) 217 } 218 219 return nil 220 } 221 222 // Run selects the terminal command in a command tree previously identified by a 223 // successful call to Parse, and calls that command's Exec function with the 224 // appropriate subset of commandline args. 225 // 226 // If the terminal command previously identified by Parse doesn't define an Exec 227 // function, then Run will return an error. 228 // 229 // Forked from peterbourgon/ff/ffcli 230 func (c *Command) Run(ctx context.Context) (err error) { 231 var ( 232 unparsed = c.selected == nil 233 terminal = c.selected == c && c.exec != nil 234 noop = c.selected == c && c.exec == nil 235 ) 236 237 defer func() { 238 if terminal && errors.Is(err, flag.ErrHelp) { 239 c.flagSet.Usage() 240 } 241 }() 242 243 switch { 244 case unparsed: 245 return fmt.Errorf("command %s not parsed", c.name) 246 case terminal: 247 return c.exec(ctx, c.args) 248 case noop: 249 return fmt.Errorf("command %s not executable", c.name) 250 default: 251 return c.selected.Run(ctx) 252 } 253 } 254 255 // Forked from peterbourgon/ff/ffcli 256 func usage(c *Command) string { 257 var b strings.Builder 258 259 fmt.Fprintf(&b, "USAGE\n") 260 if c.shortUsage != "" { 261 fmt.Fprintf(&b, " %s\n", c.shortUsage) 262 } else { 263 fmt.Fprintf(&b, " %s\n", c.name) 264 } 265 fmt.Fprintf(&b, "\n") 266 267 if c.longHelp != "" { 268 fmt.Fprintf(&b, "%s\n\n", c.longHelp) 269 } else if c.shortHelp != "" { 270 fmt.Fprintf(&b, "%s.\n\n", c.shortHelp) 271 } 272 273 if len(c.subcommands) > 0 { 274 fmt.Fprintf(&b, "SUBCOMMANDS\n") 275 tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) 276 for _, subcommand := range c.subcommands { 277 fmt.Fprintf(tw, " %s\t%s\n", subcommand.name, subcommand.shortHelp) 278 } 279 tw.Flush() 280 fmt.Fprintf(&b, "\n") 281 } 282 283 if countFlags(c.flagSet) > 0 { 284 fmt.Fprintf(&b, "FLAGS\n") 285 tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) 286 c.flagSet.VisitAll(func(f *flag.Flag) { 287 space := " " 288 if isBoolFlag(f) { 289 space = "=" 290 } 291 292 def := f.DefValue 293 if def == "" { 294 def = "..." 295 } 296 297 fmt.Fprintf(tw, " -%s%s%s\t%s\n", f.Name, space, def, f.Usage) 298 }) 299 tw.Flush() 300 fmt.Fprintf(&b, "\n") 301 } 302 303 return strings.TrimSpace(b.String()) + "\n" 304 } 305 306 // Forked from peterbourgon/ff/ffcli 307 func countFlags(fs *flag.FlagSet) (n int) { 308 fs.VisitAll(func(*flag.Flag) { n++ }) 309 return n 310 } 311 312 // Forked from peterbourgon/ff/ffcli 313 func isBoolFlag(f *flag.Flag) bool { 314 b, ok := f.Value.(interface { 315 IsBoolFlag() bool 316 }) 317 return ok && b.IsBoolFlag() 318 } 319 320 // registerFlagsWithSubcommands recursively registers the passed in 321 // configuration's flagset with the subcommand tree. At the point of calling 322 // this method, the child subcommand tree should already be present, due to the 323 // way subcommands are built (LIFO) 324 func registerFlagsWithSubcommands(cfg Config, root *Command) { 325 subcommands := []*Command{root} 326 327 // Traverse the direct subcommand tree, 328 // and register the top-level flagset with each 329 // direct line subcommand 330 for len(subcommands) > 0 { 331 current := subcommands[0] 332 subcommands = subcommands[1:] 333 334 for _, subcommand := range current.subcommands { 335 cfg.RegisterFlags(subcommand.flagSet) 336 subcommands = append(subcommands, subcommand) 337 } 338 } 339 } 340 341 // registerOptionsWithSubcommands recursively registers the passed in 342 // options with the subcommand tree. At the point of calling 343 func registerOptionsWithSubcommands(root *Command) { 344 subcommands := []*Command{root} 345 346 // Traverse the direct subcommand tree, 347 // and register the top-level flagset with each 348 // direct line subcommand 349 for len(subcommands) > 0 { 350 current := subcommands[0] 351 subcommands = subcommands[1:] 352 353 for _, subcommand := range current.subcommands { 354 subcommand.options = append(subcommand.options, root.options...) 355 subcommands = append(subcommands, subcommand) 356 } 357 } 358 }