package bonfire import ( "errors" "fmt" "io" "os" "slices" "github.com/spf13/cobra" ) // ErrDuplicateCommand is returned by NewRoot when two commands share a name, // or a command is named list or help (the root's own commands). var ErrDuplicateCommand = errors.New("bonfire: duplicate command name") // ErrReservedFlag is returned by NewRoot when a command flag reuses the name // or shorthand of a global option (verbose, quiet, ansi, no-ansi, // no-interaction, env, help; -v, -q, -n, -h). var ErrReservedFlag = errors.New("bonfire: flag name is reserved for a global option") // RootOption configures NewRoot and NewRootIO. type RootOption func(*rootConfig) type rootConfig struct { version Version hasVersion bool hooks []func(Globals) } // WithVersion sets the version shown in the header of the command list. // Without it the root reads BuildVersion. func WithVersion(v Version) RootOption { return func(c *rootConfig) { c.version = v c.hasVersion = true } } // OnGlobals registers fn to run with the parsed global options before every // command of the root runs. The generated main uses it to re-apply the // options to diary once cobra has parsed them; roots built by Call never run // a hook, so a scheduled command cannot change process-wide settings. func OnGlobals(fn func(Globals)) RootOption { return func(c *rootConfig) { if fn != nil { c.hooks = append(c.hooks, fn) } } } // NewRoot builds the shared cobra root used by the summer tool and app binaries. // It accepts command values rather than a party registry to avoid an import cycle. // When out is the process stdout, the error block of Execute goes to stderr. func NewRoot(name string, commands []Command, out io.Writer, opts ...RootOption) (*cobra.Command, error) { root, err := NewRootIO(name, commands, os.Stdin, out, out, opts...) if err != nil { return nil, err } if out == nil || out == os.Stdout { root.SetErr(os.Stderr) } return root, nil } // NewRootIO is NewRoot with injected stdin/stdout/stderr for tests. func NewRootIO(name string, commands []Command, in io.Reader, out, errW io.Writer, opts ...RootOption) (*cobra.Command, error) { if out == nil { out = os.Stdout } if errW == nil { errW = out } if in == nil { in = os.Stdin } cfg := &rootConfig{} for _, opt := range opts { if opt != nil { opt(cfg) } } if !cfg.hasVersion { cfg.version = BuildVersion() } output := newConsole(in, out, errW, os.Getenv, nil, nil) root := &cobra.Command{ Use: name, SilenceUsage: true, SilenceErrors: true, CompletionOptions: cobra.CompletionOptions{ DisableDefaultCmd: true, }, } root.SetIn(in) root.SetOut(out) root.SetErr(errW) flags := &globalFlags{} flags.register(root.PersistentFlags()) root.SetFlagErrorFunc(func(_ *cobra.Command, err error) error { return Usage(err) }) seen := map[string]bool{} for _, b := range builtinCommands() { seen[b.Name] = true } for _, command := range commands { if seen[command.Name] { return nil, fmt.Errorf("%w: %q", ErrDuplicateCommand, command.Name) } seen[command.Name] = true cmd, err := wrap(command, output) if err != nil { return nil, err } root.AddCommand(cmd) } lister := &lister{name: name, commands: commands, version: cfg.version, out: out, console: output} listCmd, err := wrapCommand(lister.command(), output, false) if err != nil { return nil, err } root.AddCommand(listCmd) root.RunE = func(cmd *cobra.Command, _ []string) error { return lister.render(output.globals, false) } root.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { var g Globals if cmd.DisableFlagParsing { // Passthrough commands parse no flags; read the globals the // way the generated main did before boot. g = PeekGlobals(args) } else { g = flags.globals() } output.setGlobals(g) for _, hook := range cfg.hooks { hook(g) } return nil } return root, nil } func wrap(c Command, out Output) (*cobra.Command, error) { return wrapCommand(c, out, true) } func wrapCommand(c Command, out Output, validate bool) (*cobra.Command, error) { if validate && !validCommandName(c.Name) { return nil, fmt.Errorf("%w: %q", ErrCommandName, c.Name) } for _, flag := range c.Flags { if slices.Contains(reservedFlagNames, flag.Name) { return nil, fmt.Errorf("%w: command %q declares --%s", ErrReservedFlag, c.Name, flag.Name) } if flag.Shorthand != "" && slices.Contains(reservedFlagShorthands, flag.Shorthand) { return nil, fmt.Errorf("%w: command %q declares -%s for --%s", ErrReservedFlag, c.Name, flag.Shorthand, flag.Name) } } cmd := &cobra.Command{ Use: commandUsage(c), Short: c.Description, Long: c.Long, Args: usageArgs(positionalArgs(c.Args)), RunE: func(cmd *cobra.Command, args []string) error { if c.Run == nil { return nil } return c.Run(cmd.Context(), cobraInput{cmd: cmd, spec: c, args: args}, out) }, } if c.Passthrough { // Every argument, flags included, reaches Run as given. cmd.DisableFlagParsing = true cmd.Args = cobra.ArbitraryArgs } for _, flag := range c.Flags { if flag.Repeatable { // D-19: an ordered, multi-occurrence string flag (Cobra // StringSlice), read back through Input.Flags. Default is a // single-element slice only when non-empty, matching the // scalar path's zero-value convention. var def []string if flag.Default != "" { def = []string{flag.Default} } if flag.Shorthand != "" { cmd.Flags().StringSliceP(flag.Name, flag.Shorthand, def, flag.Description) } else { cmd.Flags().StringSlice(flag.Name, def, flag.Description) } continue } if flag.Shorthand != "" { cmd.Flags().StringP(flag.Name, flag.Shorthand, flag.Default, flag.Description) } else { cmd.Flags().String(flag.Name, flag.Default, flag.Description) } if flag.Bare { if f := cmd.Flags().Lookup(flag.Name); f != nil { f.NoOptDefVal = "true" } } } return cmd, nil } // usageArgs marks argument-count failures as usage errors (exit code 2). func usageArgs(fn cobra.PositionalArgs) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { return Usage(fn(cmd, args)) } }