package command import ( "context" "flag" "fmt" "strings" "text/tabwriter" "github.com/peterbourgon/ff/v3" "github.com/peterbourgon/ff/v3/ffcli" "edge-infra.dev/pkg/lib/cli/rags" ) var ( options = []ff.Option{ff.WithEnvVarNoPrefix()} ) // Command is a simple ffcli.Command wrapper used to make consistent ffcli Commands // with helpers. type Command struct { // Exec is the function executed when the command is called. It is wrapped // in Command.WrappedExec so that the command's context can be decorated with // CLI state based on the command being executed. Exec func(ctx context.Context, args []string) error // CLI machinery configuration. ShortUsage string // One-line usage message. Command name is parsed from this ShortHelp string LongHelp string Rags *rags.RagSet Flags []*rags.Rag // Flags specific to this command. Options []ff.Option // Options specific to this command Commands []*Command // Subcommands Usage func(*ffcli.Command) string // Usage dictates how the cli is displayed Extensions []Extension help bool // --help } // TODO: provide ability to store extension in context similar to test/f2/fctx? type Extension interface { RegisterFlags(rs *rags.RagSet) } type AfterParser interface { AfterParse() error } func (c *Command) LongName() string { n := c.ShortUsage if i := strings.Index(n, " ["); i >= 0 { n = n[:i] } if i := strings.Index(n, " "); i >= 0 { return n[i+1:] } return "" } func (c *Command) Name() string { n := c.LongName() if i := strings.LastIndex(n, " "); i >= 0 { n = n[i+1:] } return n } func (c *Command) Command() *ffcli.Command { c.Rags = rags.New(c.ShortUsage, flag.ContinueOnError) c.registerFlags() if c.Exec == nil { c.Exec = func(context.Context, []string) error { return nil } // root Exec will print the usage info same as --help would c.help = true } cmd := &ffcli.Command{ Name: c.Name(), ShortUsage: c.ShortUsage, ShortHelp: c.ShortHelp, LongHelp: c.LongHelp, FlagSet: c.Rags.FlagSet(), Options: append(options, c.Options...), Exec: c.wExec(), UsageFunc: c.Usage, } for _, s := range c.Commands { cmd.Subcommands = append(cmd.Subcommands, s.Command()) } cmd.UsageFunc = func(_ *ffcli.Command) string { return c.DefaultUsage() } return cmd } func (c *Command) afterParse(ctx context.Context) (context.Context, error) { for _, e := range c.Extensions { if ap, ok := e.(AfterParser); ok { if err := ap.AfterParse(); err != nil { return ctx, err } } } return ctx, nil } // wExec wraps the configured Exec function to implement common command logic // and capture command state. func (c *Command) wExec() func(context.Context, []string) error { return (func() func(context.Context, []string) error { cmd := c return func(ctx context.Context, args []string) error { if cmd.help { fmt.Println(c.DefaultUsage()) return nil } ctx, err := cmd.afterParse(ctx) if err != nil { return err } return cmd.Exec(ctx, args) } })() } func (c *Command) registerFlags() { // TODO: use declarative rags flag for this c.Rags.BoolVar(&c.help, "help", false, "display help information") for _, e := range c.Extensions { e.RegisterFlags(c.Rags) } c.Rags.Add(c.Flags...) } func (c *Command) DefaultUsage() string { var b strings.Builder fmt.Fprintf(&b, "Usage:\n") if c.ShortUsage != "" { fmt.Fprintf(&b, " %s\n", c.ShortUsage) } else { fmt.Fprintf(&b, " %s\n", c.Name()) } fmt.Fprintf(&b, "\n") if c.LongHelp != "" { fmt.Fprintf(&b, "%s\n\n", c.LongHelp) } if len(c.Commands) > 0 { fmt.Fprintf(&b, "Subcommands:\n") tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) for _, subcommand := range c.Commands { fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name(), subcommand.ShortHelp) } tw.Flush() fmt.Fprintf(&b, "\n") } c.Rags.SetOutput(&b) c.Rags.Usage() return (strings.TrimSpace(b.String()) + "\n") }