//nolint:forbidigo // This is part of the CLI and the errors will be written as fmt.Pxxx package cmd import ( "context" "errors" "flag" "fmt" "os" "os/signal" "syscall" devcmd "git.maronato.dev/maronato/goshort/cmd/dev" healthcheckcmd "git.maronato.dev/maronato/goshort/cmd/healthcheck" servecmd "git.maronato.dev/maronato/goshort/cmd/serve" "git.maronato.dev/maronato/goshort/cmd/shared" "git.maronato.dev/maronato/goshort/internal/config" "git.maronato.dev/maronato/goshort/internal/util/logging" "git.maronato.dev/maronato/goshort/internal/util/tracing" "github.com/peterbourgon/ff/v3/ffcli" ) var ( // ErrUnknownCommand is returned when the user tries to run an unknown command. ErrUnknownCommand = errors.New("unknown command") // ErrUnknownHelpTopic is returned when the user tries to run an unknown help topic. ErrUnknownHelpTopic = errors.New("unknown help topic") ) func NewUnknownCommand(cmd string) error { return fmt.Errorf("%q: %w\nRun 'goshort help' for usage", cmd, ErrUnknownCommand) } func NewUnknownHelpTopic(topic string) error { return fmt.Errorf("%q: %w. Run 'goshort help'", topic, ErrUnknownHelpTopic) } func Run(version string) error { // Create the application-wide context, and // implement graceful shutdown. ctx, cancel := context.WithCancel(context.Background()) defer cancel() trapSignalsCrossPlatform(cancel) // Create the root command and register subcommands. rootCmd, cfg := newRootCmd(version) rootCmd.Subcommands = append( rootCmd.Subcommands, servecmd.New(cfg), healthcheckcmd.New(cfg), ) // Look for the env ENV_DOCKER=true to disable the dev command // since the docker image won't have node installed. if os.Getenv("ENV_DOCKER") != "true" { rootCmd.Subcommands = append(rootCmd.Subcommands, devcmd.New(cfg)) } // Parse the command-line arguments. if err := rootCmd.Parse(os.Args[1:]); err != nil { if errors.Is(err, flag.ErrHelp) { return nil } return fmt.Errorf("%w", err) } // Validate config if err := config.Validate(cfg); err != nil { if errors.Is(err, flag.ErrHelp) { return nil } return fmt.Errorf("%w", err) } // Create system logger l := logging.NewLogger(cfg) ctx = logging.WithLogger(ctx, l) // Initialize tracing ctx, stopTracing, err := tracing.InitTracer(ctx, cfg, version) if err != nil { return fmt.Errorf("%w", err) } // Close the tracer when the application exits defer stopTracing() // Run the command. if err := rootCmd.Run(ctx); err != nil { if errors.Is(err, flag.ErrHelp) { return nil } return fmt.Errorf("%w", err) } return nil } // newRootCmd constructs a root command from the provided options. func newRootCmd(version string) (*ffcli.Command, *config.Config) { var cfg config.Config fs := flag.NewFlagSet("goshort", flag.ContinueOnError) cmd := &ffcli.Command{ Name: "goshort", ShortUsage: "goshort [flags]", ShortHelp: "goshort is a tiny URL shortener", FlagSet: fs, Exec: func(_ context.Context, args []string) error { // If the user didn't provide any subcommand, then // we'll just show the help message. if len(args) == 0 { return flag.ErrHelp } return NewUnknownCommand(args[0]) }, } cmd.Subcommands = []*ffcli.Command{ // Register help command { Name: "help", ShortUsage: "help [command]", ShortHelp: "Show help for a command", Exec: func(ctx context.Context, args []string) error { if len(args) == 0 || args[0] == "help" { cmd.FlagSet.Usage() fmt.Printf("\nUse 'goshort help ' for more information about that command.\n\n") return nil } for _, c := range cmd.Subcommands { if c.Name == args[0] { return c.ParseAndRun(ctx, []string{"-h"}) //nolint:wrapcheck // We don't want to wrap the error here } } return NewUnknownHelpTopic(args[0]) }, }, // Register version command { Name: "version", ShortUsage: "version", ShortHelp: "Show version information", Exec: func(_ context.Context, _ []string) error { fmt.Printf("goshort version %s\n", version) return nil }, }, // Register config command NewConfigCmd(&cfg), } return cmd, &cfg } // https://github.com/caddyserver/caddy/blob/fbb0ecfa322aa7710a3448453fd3ae40f037b8d1/sigtrap.go#L37 // trapSignalsCrossPlatform captures SIGINT or interrupt (depending // on the OS), which initiates a graceful shutdown. A second SIGINT // or interrupt will forcefully exit the process immediately. func trapSignalsCrossPlatform(cancel context.CancelFunc) { go func() { shutdown := make(chan os.Signal, 1) signal.Notify(shutdown, os.Interrupt, syscall.SIGINT) for i := 0; true; i++ { <-shutdown if i > 0 { fmt.Printf("\nForce quit\n") os.Exit(1) } fmt.Printf("\nGracefully shutting down. Press Ctrl+C again to force quit\n") cancel() } }() } func NewConfigCmd(cfg *config.Config) *ffcli.Command { // Create the flagset and register the flags. fs := flag.NewFlagSet("goshort serve", flag.ContinueOnError) shared.RegisterBaseFlags(fs, cfg) shared.RegisterServerFlags(fs, cfg) fs.BoolVar(&cfg.Prod, "prod", config.DefaultProd, "run in production mode") // Return the ffcli command. return &ffcli.Command{ Name: "config", ShortUsage: "goshort config [flags]", ShortHelp: "Prints the current config", FlagSet: fs, Exec: func(_ context.Context, _ []string) error { cfg.PrettyPrint() return nil }, Options: shared.NewSharedCmdOptions(), } }