package bonfire import ( "context" "errors" "io" "os" "slices" "strings" "charm.land/lipgloss/v2" "github.com/spf13/cobra" ) // Exit codes returned by Execute and Fail. const ( // ExitOK means the command succeeded. ExitOK = 0 // ExitFailure means the command failed. ExitFailure = 1 // ExitUsage means the command was called wrongly: an unknown command or // flag, or a wrong number of arguments. ExitUsage = 2 ) // Execute runs root with args and returns the process exit code: ExitOK, // ExitFailure, ExitUsage for usage errors, or the code an error carries // through WithExitCode. With no command word in args (only global options, // -h or --help) it prints the command list. Errors are rendered on the // root's error stream by RenderError. Execute leaves the root's help // function alone, and on Windows enables ANSI processing on the output and // error streams when they are files (a no-op elsewhere). func Execute(ctx context.Context, root *cobra.Command, args []string) int { if ctx == nil { ctx = context.Background() } if args == nil { // cobra reads os.Args when the args are nil. args = []string{} } g := PeekGlobals(args) if !hasCommandWord(args) { args = slices.DeleteFunc(slices.Clone(args), func(a string) bool { return a == "-h" || a == "--help" }) } resetFlags(root) root.SetArgs(args) // The settings fang applied; NewRootIO sets them already, a root built // elsewhere gets them here. root.SilenceUsage = true root.SilenceErrors = true root.CompletionOptions.DisableDefaultCmd = true for _, w := range []io.Writer{root.OutOrStdout(), root.ErrOrStderr()} { if f, ok := w.(*os.File); ok { lipgloss.EnableLegacyWindowsANSI(f) } } err := root.ExecuteContext(ctx) if err == nil { return ExitOK } err = unknownCommand(root, args, err) name := root.Name() RenderError(root.ErrOrStderr(), name, wrongDirectory(name, err, args, ""), g) return exitCode(err) } // Fail renders err like Execute does and returns ExitFailure (ExitOK for a // nil error). The generated main uses it for errors that happen before the // command root exists, such as a configuration that does not load. Like // Execute, it adds the wrong-directory hint for an application binary run // outside its application root. func Fail(w io.Writer, name string, err error, g Globals) int { if err == nil { return ExitOK } RenderError(w, name, wrongDirectory(name, err, os.Args[1:], ""), g) return ExitFailure } func exitCode(err error) int { var carrier *exitCodeError switch { case err == nil: return ExitOK case errors.As(err, &carrier): if carrier.code < 1 || carrier.code > 255 { return ExitFailure } return carrier.code case isUsage(err): return ExitUsage default: return ExitFailure } } // hasCommandWord reports whether args, before "--", hold a token that is // neither a flag nor the value of --env. func hasCommandWord(args []string) bool { for i := 0; i < len(args); i++ { a := args[i] switch { case a == "--": return false case a == "--"+flagEnv: i++ case strings.HasPrefix(a, "-"): default: return true } } return false }