summaryrefslogtreecommitdiffstats
path: root/runner/run.go
diff options
context:
space:
mode:
Diffstat (limited to 'runner/run.go')
-rw-r--r--runner/run.go145
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
}