...

Source file src/github.com/peterbourgon/ff/v3/ffcli/command.go

Documentation: github.com/peterbourgon/ff/v3/ffcli

     1  package ffcli
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"flag"
     7  	"fmt"
     8  	"strings"
     9  	"text/tabwriter"
    10  
    11  	"github.com/peterbourgon/ff/v3"
    12  )
    13  
    14  // Command combines a main function with a flag.FlagSet, and zero or more
    15  // sub-commands. A commandline program can be represented as a declarative tree
    16  // of commands.
    17  type Command struct {
    18  	// Name of the command. Used for sub-command matching, and as a replacement
    19  	// for Usage, if no Usage string is provided. Required for sub-commands,
    20  	// optional for the root command.
    21  	Name string
    22  
    23  	// ShortUsage string for this command. Consumed by the DefaultUsageFunc and
    24  	// printed at the top of the help output. Recommended but not required.
    25  	// Should be one line of the form
    26  	//
    27  	//     cmd [flags] subcmd [flags] <required> [<optional> ...]
    28  	//
    29  	// If it's not provided, the DefaultUsageFunc will use Name instead.
    30  	// Optional, but recommended.
    31  	ShortUsage string
    32  
    33  	// ShortHelp is printed next to the command name when it appears as a
    34  	// sub-command, in the help output of its parent command. Optional, but
    35  	// recommended.
    36  	ShortHelp string
    37  
    38  	// LongHelp is consumed by the DefaultUsageFunc and printed in the help
    39  	// output, after ShortUsage and before flags. Typically a paragraph or more
    40  	// of prose-like text, providing more explicit context and guidance than
    41  	// what is implied by flags and arguments. Optional.
    42  	LongHelp string
    43  
    44  	// UsageFunc generates a complete usage output, written to the io.Writer
    45  	// returned by FlagSet.Output() when the -h flag is passed. The function is
    46  	// invoked with its corresponding command, and its output should reflect the
    47  	// command's short usage, short help, and long help strings, subcommands,
    48  	// and available flags. Optional; if not provided, a suitable, compact
    49  	// default is used.
    50  	UsageFunc func(c *Command) string
    51  
    52  	// FlagSet associated with this command. Optional, but if none is provided,
    53  	// an empty FlagSet will be defined and attached during the parse phase, so
    54  	// that the -h flag works as expected.
    55  	FlagSet *flag.FlagSet
    56  
    57  	// Options provided to ff.Parse when parsing arguments for this command.
    58  	// Optional.
    59  	Options []ff.Option
    60  
    61  	// Subcommands accessible underneath (i.e. after) this command. Optional.
    62  	Subcommands []*Command
    63  
    64  	// A successful Parse populates these unexported fields.
    65  	selected *Command // the command itself (if terminal) or a subcommand
    66  	args     []string // args that should be passed to Run, if any
    67  
    68  	// Exec is invoked if this command has been determined to be the terminal
    69  	// command selected by the arguments provided to Parse or ParseAndRun. The
    70  	// args passed to Exec are the args left over after flags parsing. Optional.
    71  	//
    72  	// If Exec returns flag.ErrHelp, then Run (or ParseAndRun) will behave as if
    73  	// -h were passed and emit the complete usage output.
    74  	//
    75  	// If Exec is nil, and this command is identified as the terminal command,
    76  	// then Parse, Run, and ParseAndRun will all return NoExecError. Callers may
    77  	// check for this error and print e.g. help or usage text to the user, in
    78  	// effect treating some commands as just collections of subcommands, rather
    79  	// than being invocable themselves.
    80  	Exec func(ctx context.Context, args []string) error
    81  }
    82  
    83  // Parse the commandline arguments for this command and all sub-commands
    84  // recursively, defining flags along the way. If Parse returns without an error,
    85  // the terminal command has been successfully identified, and may be invoked by
    86  // calling Run.
    87  //
    88  // If the terminal command identified by Parse doesn't define an Exec function,
    89  // then Parse will return NoExecError.
    90  func (c *Command) Parse(args []string) error {
    91  	if c.selected != nil {
    92  		return nil
    93  	}
    94  
    95  	if c.FlagSet == nil {
    96  		c.FlagSet = flag.NewFlagSet(c.Name, flag.ExitOnError)
    97  	}
    98  
    99  	if c.UsageFunc == nil {
   100  		c.UsageFunc = DefaultUsageFunc
   101  	}
   102  
   103  	c.FlagSet.Usage = func() {
   104  		fmt.Fprintln(c.FlagSet.Output(), c.UsageFunc(c))
   105  	}
   106  
   107  	if err := ff.Parse(c.FlagSet, args, c.Options...); err != nil {
   108  		return err
   109  	}
   110  
   111  	c.args = c.FlagSet.Args()
   112  	if len(c.args) > 0 {
   113  		for _, subcommand := range c.Subcommands {
   114  			if strings.EqualFold(c.args[0], subcommand.Name) {
   115  				c.selected = subcommand
   116  				return subcommand.Parse(c.args[1:])
   117  			}
   118  		}
   119  	}
   120  
   121  	c.selected = c
   122  
   123  	if c.Exec == nil {
   124  		return NoExecError{Command: c}
   125  	}
   126  
   127  	return nil
   128  }
   129  
   130  // Run selects the terminal command in a command tree previously identified by a
   131  // successful call to Parse, and calls that command's Exec function with the
   132  // appropriate subset of commandline args.
   133  //
   134  // If the terminal command previously identified by Parse doesn't define an Exec
   135  // function, then Run will return NoExecError.
   136  func (c *Command) Run(ctx context.Context) (err error) {
   137  	var (
   138  		unparsed = c.selected == nil
   139  		terminal = c.selected == c && c.Exec != nil
   140  		noop     = c.selected == c && c.Exec == nil
   141  	)
   142  
   143  	defer func() {
   144  		if terminal && errors.Is(err, flag.ErrHelp) {
   145  			c.FlagSet.Usage()
   146  		}
   147  	}()
   148  
   149  	switch {
   150  	case unparsed:
   151  		return ErrUnparsed
   152  	case terminal:
   153  		return c.Exec(ctx, c.args)
   154  	case noop:
   155  		return NoExecError{Command: c}
   156  	default:
   157  		return c.selected.Run(ctx)
   158  	}
   159  }
   160  
   161  // ParseAndRun is a helper function that calls Parse and then Run in a single
   162  // invocation. It's useful for simple command trees that don't need two-phase
   163  // setup.
   164  func (c *Command) ParseAndRun(ctx context.Context, args []string) error {
   165  	if err := c.Parse(args); err != nil {
   166  		return err
   167  	}
   168  
   169  	if err := c.Run(ctx); err != nil {
   170  		return err
   171  	}
   172  
   173  	return nil
   174  }
   175  
   176  //
   177  //
   178  //
   179  
   180  // ErrUnparsed is returned by Run if Parse hasn't been called first.
   181  var ErrUnparsed = errors.New("command tree is unparsed, can't run")
   182  
   183  // NoExecError is returned if the terminal command selected during the parse
   184  // phase doesn't define an Exec function.
   185  type NoExecError struct {
   186  	Command *Command
   187  }
   188  
   189  // Error implements the error interface.
   190  func (e NoExecError) Error() string {
   191  	return fmt.Sprintf("terminal command (%s) doesn't define an Exec function", e.Command.Name)
   192  }
   193  
   194  //
   195  //
   196  //
   197  
   198  // DefaultUsageFunc is the default UsageFunc used for all commands
   199  // if no custom UsageFunc is provided.
   200  func DefaultUsageFunc(c *Command) string {
   201  	var b strings.Builder
   202  
   203  	fmt.Fprintf(&b, "USAGE\n")
   204  	if c.ShortUsage != "" {
   205  		fmt.Fprintf(&b, "  %s\n", c.ShortUsage)
   206  	} else {
   207  		fmt.Fprintf(&b, "  %s\n", c.Name)
   208  	}
   209  	fmt.Fprintf(&b, "\n")
   210  
   211  	if c.LongHelp != "" {
   212  		fmt.Fprintf(&b, "%s\n\n", c.LongHelp)
   213  	}
   214  
   215  	if len(c.Subcommands) > 0 {
   216  		fmt.Fprintf(&b, "SUBCOMMANDS\n")
   217  		tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
   218  		for _, subcommand := range c.Subcommands {
   219  			fmt.Fprintf(tw, "  %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
   220  		}
   221  		tw.Flush()
   222  		fmt.Fprintf(&b, "\n")
   223  	}
   224  
   225  	if countFlags(c.FlagSet) > 0 {
   226  		fmt.Fprintf(&b, "FLAGS\n")
   227  		tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
   228  		c.FlagSet.VisitAll(func(f *flag.Flag) {
   229  			space := " "
   230  			if isBoolFlag(f) {
   231  				space = "="
   232  			}
   233  
   234  			def := f.DefValue
   235  			if def == "" {
   236  				def = "..."
   237  			}
   238  
   239  			fmt.Fprintf(tw, "  -%s%s%s\t%s\n", f.Name, space, def, f.Usage)
   240  		})
   241  		tw.Flush()
   242  		fmt.Fprintf(&b, "\n")
   243  	}
   244  
   245  	return strings.TrimSpace(b.String()) + "\n"
   246  }
   247  
   248  func countFlags(fs *flag.FlagSet) (n int) {
   249  	fs.VisitAll(func(*flag.Flag) { n++ })
   250  	return n
   251  }
   252  
   253  func isBoolFlag(f *flag.Flag) bool {
   254  	b, ok := f.Value.(interface {
   255  		IsBoolFlag() bool
   256  	})
   257  	return ok && b.IsBoolFlag()
   258  }
   259  

View as plain text