From c041fe49274ada573fe49bf6bff7cd54b88e90d0 Mon Sep 17 00:00:00 2001 From: Oleg Kovalov Date: Sat, 22 Jan 2022 17:10:48 +0100 Subject: [PATCH] Runner exit (#10) --- README.md | 2 +- acmd.go | 24 ++++++++++++++++++--- acmd_test.go | 60 +++++++++++++++++++++++++++++++++++++++++++++------- errors.go | 10 +++++++++ 4 files changed, 84 insertions(+), 12 deletions(-) create mode 100644 errors.go diff --git a/README.md b/README.md index 9208c3b..0000dee 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ r := acmd.RunnerOf(cmds, acmd.Config{ }) if err := r.Run(); err != nil { - panic(err) + r.Exit(err) } ``` diff --git a/acmd.go b/acmd.go index 29599a7..280b55d 100644 --- a/acmd.go +++ b/acmd.go @@ -12,6 +12,9 @@ import ( "text/tabwriter" ) +// changed only in tests. +var doExit = os.Exit + // Runner of the sub-commands. type Runner struct { cfg Config @@ -67,7 +70,7 @@ type Config struct { // Args passed to the executable, if nil os.Args[1:] will be used. Args []string - // Usage of the application, if nil default will be used, + // Usage of the application, if nil default will be used. Usage func(cfg Config, cmds []Command) } @@ -96,6 +99,21 @@ func RunnerOf(cmds []Command, cfg Config) *Runner { return r } +// Exit the application depending on the error. +// If err is nil, so successful/no error exit is done: os.Exit(0) +// If err is of type ErrCode: code from the error is returned: os.Exit(code) +// Otherwise: os.Exit(1). +func (r *Runner) Exit(err error) { + if err == nil { + os.Exit(0) + } + errCode := ErrCode(1) + errors.As(err, &errCode) + + fmt.Fprintf(r.cfg.Output, "%s: %s\n", r.cfg.AppName, err.Error()) + doExit(int(errCode)) +} + func (r *Runner) init() error { if r.cfg.AppName == "" { r.cfg.AppName = os.Args[0] @@ -228,14 +246,14 @@ func isStringValid(s string) bool { // Run commands. func (r *Runner) Run() error { if r.errInit != nil { - return fmt.Errorf("cannot init runner: %w", r.errInit) + return fmt.Errorf("init error: %w", r.errInit) } cmd, params, err := findCmd(r.cfg, r.cmds, r.args) if err != nil { return err } if err := cmd(r.ctx, params); err != nil { - return fmt.Errorf("cannot run command: %w", err) + return fmt.Errorf("got error: %w", err) } return nil } diff --git a/acmd_test.go b/acmd_test.go index 8d5779b..12b410e 100644 --- a/acmd_test.go +++ b/acmd_test.go @@ -55,8 +55,8 @@ func TestRunner(t *testing.T) { } r := RunnerOf(cmds, Config{ Args: []string{"test", "foo", "for"}, - AppName: "acmd_test_app", - AppDescription: "acmd_test_app is a test application.", + AppName: "myapp", + AppDescription: "myapp is a test application.", Version: time.Now().String(), Output: buf, }) @@ -127,6 +127,7 @@ func TestRunnerMustSortCommands(t *testing.T) { return r.cmds[i].Name < r.cmds[j].Name }) } + func TestRunnerPanicWithoutCommands(t *testing.T) { defer func() { if r := recover(); r == nil { @@ -233,22 +234,22 @@ func TestRunner_suggestCommand(t *testing.T) { {Name: "bar", Do: nopFunc}, }, args: []string{"fooo"}, - want: `"fooo" unknown command, did you mean "foo"?` + "\n" + `Run "ci help" for usage.` + "\n\n", + want: `"fooo" unknown command, did you mean "foo"?` + "\n" + `Run "myapp help" for usage.` + "\n\n", }, { cmds: []Command{{Name: "for", Do: nopFunc}}, args: []string{"hell"}, - want: `"hell" unknown command, did you mean "help"?` + "\n" + `Run "ci help" for usage.` + "\n\n", + want: `"hell" unknown command, did you mean "help"?` + "\n" + `Run "myapp help" for usage.` + "\n\n", }, { cmds: []Command{{Name: "for", Do: nopFunc}}, args: []string{"verZION"}, - want: `"verZION" unknown command` + "\n" + `Run "ci help" for usage.` + "\n\n", + want: `"verZION" unknown command` + "\n" + `Run "myapp help" for usage.` + "\n\n", }, { cmds: []Command{{Name: "for", Do: nopFunc}}, args: []string{"verZion"}, - want: `"verZion" unknown command, did you mean "version"?` + "\n" + `Run "ci help" for usage.` + "\n\n", + want: `"verZion" unknown command, did you mean "version"?` + "\n" + `Run "myapp help" for usage.` + "\n\n", }, } @@ -256,7 +257,7 @@ func TestRunner_suggestCommand(t *testing.T) { buf := &bytes.Buffer{} r := RunnerOf(tc.cmds, Config{ Args: tc.args, - AppName: "ci", + AppName: "myapp", Output: buf, Usage: nopUsage, }) @@ -296,7 +297,7 @@ func TestCommand_IsHidden(t *testing.T) { } r := RunnerOf(cmds, Config{ Args: []string{"help"}, - AppName: "ci", + AppName: "myapp", Output: buf, }) if err := r.Run(); err != nil { @@ -307,3 +308,46 @@ func TestCommand_IsHidden(t *testing.T) { t.Fatal("should not show foo") } } + +func TestExit(t *testing.T) { + wantStatus := 42 + wantOutput := "myapp: got error: code 42\n" + + cmds := []Command{ + { + Name: "for", + Do: func(ctx context.Context, args []string) error { + return ErrCode(wantStatus) + }, + }, + } + + buf := &bytes.Buffer{} + r := RunnerOf(cmds, Config{ + AppName: "myapp", + Args: []string{"for"}, + Output: buf, + }) + + err := r.Run() + if err == nil { + t.Fatal("must not be nil") + } + + var gotStatus int + doExitOld := func(code int) { + gotStatus = code + } + defer func() { doExit = doExitOld }() + + doExitOld, doExit = doExit, doExitOld + + r.Exit(err) + + if gotStatus != wantStatus { + t.Fatalf("got %d want %d", gotStatus, wantStatus) + } + if got := buf.String(); got != wantOutput { + t.Fatalf("got %q want %q", got, wantOutput) + } +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..ec59976 --- /dev/null +++ b/errors.go @@ -0,0 +1,10 @@ +package acmd + +import "fmt" + +// ErrCode contains an int to be returned as an exit code. +type ErrCode int + +func (e ErrCode) Error() string { + return fmt.Sprintf("code %d", int(e)) +}