diff options
Diffstat (limited to 'runner/run.go')
| -rw-r--r-- | runner/run.go | 145 |
1 files changed, 119 insertions, 26 deletions
diff --git a/runner/run.go b/runner/run.go index 04f1907..893ee7d 100644 --- a/runner/run.go +++ b/runner/run.go @@ -2,7 +2,9 @@ package runner import ( "context" + "errors" "flag" + "fmt" "log/slog" "os" "os/signal" @@ -11,49 +13,140 @@ import ( "go.sudomsg.com/kit/logging" ) -// LoadWithArgs initializes context, signal handling, flag parsing, and runs the given application callback. -// -// - args: Command-line arguments (excluding/exact os.Args). -// - run: Callback function (your main app logic). Receives a context (with signal cancellation), -// a FlagSet (ready for use but not yet parsed), and trailing arguments. -// -// Handles panics and logs via logging.RecoverAndLog. -// Logs and exits on non-nil errors from the run callback. -func LoadWithArgs(args []string, run func(ctx context.Context, fs *flag.FlagSet, args []string) error) { +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) + 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(1) + os.Exit(ExitPanic) } }() - name := "app" + 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 +} - fs := flag.NewFlagSet(name, flag.ExitOnError) +var ErrInvalidSubcommand = errors.New("invalid subcommand") - if err := run(ctx, fs, args); err != nil { - slog.ErrorContext(ctx, "Program Terminated", "error", err) - os.Exit(1) +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) + } } } -// LoadWithArgs initializes context, signal handling, flag parsing, and runs the given application callback. -// -// - args: Command-line arguments (excluding/exact os.Args). -// - run: Callback function (your main app logic). Receives a context (with signal cancellation), -// a FlagSet (ready for use but not yet parsed), and trailing arguments. -// -// Handles panics and logs via logging.RecoverAndLog. -// Logs and exits on non-nil errors from the run callback. -func Load(run func(ctx context.Context, fs *flag.FlagSet, args []string) error) { - LoadWithArgs(os.Args, run) +func (c *CommandSet) Parse(args []string) error { + if err := c.FlagSet.Parse(args); err != nil { + return UsageError{Err: err} + } + return nil } |
