package runner import ( "context" "errors" "flag" "fmt" "log/slog" "os" "os/signal" "syscall" "go.sudomsg.com/kit/logging" ) const ( ExitSuccess = 0 ExitFailure = 1 ExitPanic = 2 ExitUsage = 3 ) type UsageError struct { Err error } func (e UsageError) Error() string { return e.Err.Error() } func (e UsageError) Unwrap() error { return e.Err } func RunWithArgs(args []string, cmd Command) { ctx := context.Background() ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) defer stop() defer func() { if err := recover(); err != nil { logging.RecoverAndLog(ctx, "Panicked", err) os.Exit(ExitPanic) } }() if err := runCmd(ctx, cmd, args); err != nil { var ue UsageError if errors.As(err, &ue) { os.Exit(ExitUsage) } slog.Log(ctx, slog.LevelError, "Program Terminated", "error", err) os.Exit(ExitFailure) } } func Run(cmd Command) { RunWithArgs(os.Args, cmd) } type Command struct { Name string Description string Run func(ctx context.Context, cs *CommandSet, args []string) error } func runCmd(ctx context.Context, cmd Command, args []string) error { name := "command" if len(args) > 0 && args[0] != "" { name = args[0] args = args[1:] } if cmd.Name != "" { name = cmd.Name } fs := flag.NewFlagSet(name, flag.ContinueOnError) c := &CommandSet{ FlagSet: fs, description: cmd.Description, } c.Usage = func() { fmt.Fprintf(fs.Output(), "Usage of %s:\n", fs.Name()) c.PrintDefaults() } c.AddSubcommand("help", "Print out the help", func(ctx context.Context, cs *CommandSet, args []string) error { c.Usage() return nil }) return cmd.Run(ctx, c, args) } type CommandSet struct { *flag.FlagSet cmds []Command description string } var ErrInvalidSubcommand = errors.New("invalid subcommand") func (c *CommandSet) Run(ctx context.Context, args []string) error { if err := c.Parse(args); err != nil { return err } args = c.Args() if len(args) == 0 { fmt.Fprintf(c.Output(), "Missing Subcommand") c.Usage() return UsageError{Err: ErrInvalidSubcommand} } for _, sc := range c.cmds { if sc.Name == args[0] { return runCmd(ctx, sc, args) } } fmt.Fprintf(c.Output(), "Invalid Subcommand: %s", args[0]) c.Usage() return UsageError{Err: fmt.Errorf("invalid subcommand: %s", args[0])} } func (c *CommandSet) AddSubcommand(name string, usage string, run func(ctx context.Context, cs *CommandSet, args []string) error) { c.cmds = append(c.cmds, Command{Name: name, Description: usage, Run: run}) } func (c *CommandSet) PrintDefaults() { c.FlagSet.PrintDefaults() if c.description != "" { fmt.Fprintf(c.Output(), "\n%s\n", c.description) } if len(c.cmds) > 0 { fmt.Fprintf(c.Output(), "\nSubcommands:\n") for _, sc := range c.cmds { fmt.Fprintf(c.Output(), " %s\t%s\n", sc.Name, sc.Description) } } } func (c *CommandSet) Parse(args []string) error { if err := c.FlagSet.Parse(args); err != nil { return UsageError{Err: err} } return nil }