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  }