Skip to content

Commit

Permalink
Runner exit (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
cristaloleg authored Jan 22, 2022
1 parent 7bc2c58 commit c041fe4
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 12 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ r := acmd.RunnerOf(cmds, acmd.Config{
})

if err := r.Run(); err != nil {
panic(err)
r.Exit(err)
}
```

Expand Down
24 changes: 21 additions & 3 deletions acmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
}
Expand Down
60 changes: 52 additions & 8 deletions acmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -233,30 +234,30 @@ 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",
},
}

for _, tc := range testCases {
buf := &bytes.Buffer{}
r := RunnerOf(tc.cmds, Config{
Args: tc.args,
AppName: "ci",
AppName: "myapp",
Output: buf,
Usage: nopUsage,
})
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
}
10 changes: 10 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -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))
}

0 comments on commit c041fe4

Please sign in to comment.