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(c *cobra.Command, err error) error { return usageFor(c, root, err) }) st := &rootState{ root: root, name: name, commands: commands, version: cfg.version, out: out, console: output, flags: flags, } 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 := st.wrap(command, true) if err != nil { return nil, err } root.AddCommand(cmd) } listCmd, err := st.wrap(st.listCommand(), false) if err != nil { return nil, err } root.AddCommand(listCmd) helpCmd, err := st.wrap(st.helpCommand(), false) if err != nil { return nil, err } root.SetHelpCommand(helpCmd) root.SetHelpFunc(func(*cobra.Command, []string) { _ = st.renderList(st.globals(), false) }) root.RunE = func(cmd *cobra.Command, _ []string) error { return st.renderList(st.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 } // rootState is what the list, help and error rendering of one root share. type rootState struct { root *cobra.Command name string commands []Command version Version out io.Writer console *console flags *globalFlags } // globals returns the parsed global options of the current run. func (st *rootState) globals() Globals { return st.flags.globals() } // wrap turns a command value into a cobra command. validate checks the // name form (the builtins list and help skip it). func (st *rootState) wrap(c Command, validate bool) (*cobra.Command, error) { cmd, err := wrapCommand(c, st.console, validate) if err != nil { return nil, err } cmd.Hidden = c.Hidden cmd.SetHelpFunc(func(*cobra.Command, []string) { _ = st.renderHelp(c, st.globals()) }) return cmd, 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 usageFor(cmd, cmd.Root(), fn(cmd, args)) } } // usageFor wraps err as a UsageError that remembers the command, so the // error block can suggest its help. func usageFor(cmd, root *cobra.Command, err error) error { if err == nil { return nil } var ue *UsageError if errors.As(err, &ue) { return err } ue = &UsageError{Err: err} if cmd != nil && cmd != root { ue.command = cmd.Name() } return ue }