Skip to content

Commit

Permalink
Print help when cmd not found
Browse files Browse the repository at this point in the history
  • Loading branch information
cristaloleg committed Nov 17, 2021
1 parent 67d03e7 commit a000865
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 21 deletions.
18 changes: 11 additions & 7 deletions acmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,10 @@ func validateSubcommands(cmds []Command) error {
// Run commands.
func (r *Runner) Run() error {
if r.errInit != nil {
return fmt.Errorf("acmd: cannot init runner: %w", r.errInit)
return fmt.Errorf("cannot init runner: %w", r.errInit)
}
if err := r.rootCmd.Do(r.ctx, r.args); err != nil {
return fmt.Errorf("acmd: cannot run command: %w", err)
return fmt.Errorf("cannot run command: %w", err)
}
return nil
}
Expand All @@ -233,7 +233,7 @@ func rootDo(cfg Config, cmds []Command) func(ctx context.Context, args []string)
// go deeper into subcommands
if c.Do == nil {
if len(params) == 0 {
return errors.New("no args for subcmd provided")
return errors.New("no args for command provided")
}
cmds, args = c.Subcommands, params
found = true
Expand All @@ -243,16 +243,20 @@ func rootDo(cfg Config, cmds []Command) func(ctx context.Context, args []string)
}

if !found {
return errNotFoundAndSuggest(cfg.Output, selected, cmds)
return errNotFoundAndSuggest(cfg.Output, cfg.AppName, selected, cmds)
}
}
}
}

func errNotFoundAndSuggest(w io.Writer, selected string, cmds []Command) error {
if suggestion := suggestCommand(selected, cmds); suggestion != "" {
fmt.Fprintf(w, "%q is not a subcommand, did you mean %q?\n", selected, suggestion)
func errNotFoundAndSuggest(w io.Writer, appName, selected string, cmds []Command) error {
suggestion := suggestCommand(selected, cmds)
if suggestion != "" {
fmt.Fprintf(w, "%q unknown command, did you mean %q?\n", selected, suggestion)
} else {
fmt.Fprintf(w, "%q unknown command\n", selected)
}
fmt.Fprintf(w, "Run %q for usage.\n\n", appName+" help")
return fmt.Errorf("no such command %q", selected)
}

Expand Down
27 changes: 16 additions & 11 deletions acmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import (
"bytes"
"context"
"fmt"
"io"
"os"
"sort"
"strings"
"testing"
"time"
)

var nopUsage = func(cfg Config, cmds []Command) {}

func TestRunner(t *testing.T) {
buf := &bytes.Buffer{}

Expand Down Expand Up @@ -65,13 +68,16 @@ func TestRunner(t *testing.T) {

func TestRunnerMustSetDefaults(t *testing.T) {
cmds := []Command{{Name: "foo", Do: nopFunc}}
r := RunnerOf(cmds, Config{})
r := RunnerOf(cmds, Config{
Output: io.Discard,
Usage: nopUsage,
})

err := r.Run()
if err == nil {
t.Fatal()
}
if errStr := err.Error(); !strings.Contains(errStr, "acmd: cannot run command: no such command") {
if errStr := err.Error(); !strings.Contains(errStr, "cannot run command: no such command") {
t.Fatal(err)
}

Expand All @@ -81,9 +87,6 @@ func TestRunnerMustSetDefaults(t *testing.T) {
if r.ctx == nil {
t.Fatal("context must be set")
}
if r.cfg.Output != os.Stderr {
t.Fatal("incorrect output")
}
if r.cfg.Usage == nil {
t.Fatal("usage nust be set")
}
Expand Down Expand Up @@ -225,30 +228,32 @@ func TestRunner_suggestCommand(t *testing.T) {
{Name: "bar", Do: nopFunc},
},
args: []string{"fooo"},
want: `"fooo" is not a subcommand, did you mean "foo"?` + "\n",
want: `"fooo" unknown command, did you mean "foo"?` + "\n" + `Run "ci help" for usage.` + "\n\n",
},
{
cmds: []Command{{Name: "for", Do: nopFunc}},
args: []string{"hell"},
want: `"hell" is not a subcommand, did you mean "help"?` + "\n",
want: `"hell" unknown command, did you mean "help"?` + "\n" + `Run "ci help" for usage.` + "\n\n",
},
{
cmds: []Command{{Name: "for", Do: nopFunc}},
args: []string{"verZION"},
want: "",
want: `"verZION" unknown command` + "\n" + `Run "ci help" for usage.` + "\n\n",
},
{
cmds: []Command{{Name: "for", Do: nopFunc}},
args: []string{"verZion"},
want: `"verZion" is not a subcommand, did you mean "version"?` + "\n",
want: `"verZion" unknown command, did you mean "version"?` + "\n" + `Run "ci help" for usage.` + "\n\n",
},
}

for _, tc := range testCases {
buf := &bytes.Buffer{}
r := RunnerOf(tc.cmds, Config{
Args: tc.args,
Output: buf,
Args: tc.args,
AppName: "ci",
Output: buf,
Usage: nopUsage,
})
if err := r.Run(); err != nil && !strings.Contains(err.Error(), "no such command") {
t.Fatal(err)
Expand Down
15 changes: 12 additions & 3 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ import (
"github.com/cristalhq/acmd"
)

var nopFunc = func(context.Context, []string) error { return nil }
var (
nopFunc = func(context.Context, []string) error { return nil }
nopUsage = func(cfg acmd.Config, cmds []acmd.Command) {}
)

func ExampleRunner() {
testOut := os.Stdout
testArgs := []string{"now", "--times", "3"}

const format = "15:04:05"
now, _ := time.Parse("15:04:05", "10:20:30")
now, _ := time.Parse(format, "10:20:30")

cmds := []acmd.Command{
{
Expand Down Expand Up @@ -59,6 +62,7 @@ func ExampleRunner() {
Version: "the best v0.x.y",
Output: testOut,
Args: testArgs,
Usage: nopUsage,
})

if err := r.Run(); err != nil {
Expand Down Expand Up @@ -136,6 +140,7 @@ func ExampleVersion() {
Version: "the best v0.x.y",
Output: testOut,
Args: testArgs,
Usage: nopUsage,
})

if err := r.Run(); err != nil {
Expand Down Expand Up @@ -174,6 +179,7 @@ func ExampleAlias() {
Version: "the best v0.x.y",
Output: testOut,
Args: testArgs,
Usage: nopUsage,
})

if err := r.Run(); err != nil {
Expand All @@ -198,13 +204,16 @@ func ExampleAutosuggestion() {
Version: "the best v0.x.y",
Output: testOut,
Args: testArgs,
Usage: nopUsage,
})

if err := r.Run(); err == nil {
panic("must fail with command not found")
}

// Output: "baz" is not a subcommand, did you mean "bar"?
// Output:
// "baz" unknown command, did you mean "bar"?
// Run "acmd-example help" for usage.
}

func ExampleNestedCommands() {
Expand Down

0 comments on commit a000865

Please sign in to comment.