From afa0f3734c5a8a267155eb044c0cc2279dbf4eac Mon Sep 17 00:00:00 2001 From: Tugdual Saunier Date: Thu, 18 Sep 2025 09:14:39 +0200 Subject: [PATCH] feat: suggests commands on abbreviation match failure --- application.go | 20 +++++++++++++++----- command_test.go | 30 ++++++++++++++++++------------ help.go | 2 +- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/application.go b/application.go index cd870f5..b2d2884 100644 --- a/application.go +++ b/application.go @@ -117,7 +117,11 @@ func (a *Application) Run(arguments []string) (err error) { args := context.Args() if args.Present() { name := args.first() - context.Command = a.BestCommand(name) + context.Command, err = a.BestCommand(name) + if err != nil { + HandleExitCoder(err) + return err + } } if a.Before != nil { @@ -175,10 +179,10 @@ func (a *Application) Command(name string) *Command { // BestCommand returns the named command on App or a command fuzzy matching if // there is only one. Returns nil if the command does not exist of if the fuzzy // matching find more than one. -func (a *Application) BestCommand(name string) *Command { +func (a *Application) BestCommand(name string) (*Command, error) { name = strings.ToLower(name) if c := a.Command(name); c != nil { - return c + return c, nil } // fuzzy match? @@ -190,9 +194,15 @@ func (a *Application) BestCommand(name string) *Command { } if len(matches) == 1 { matches[0].UserName = name - return matches[0] + return matches[0], nil + } else if len(matches) > 1 { + suggestions := "" + for _, m := range matches { + suggestions += fmt.Sprintf("\n %s", m.FullName()) + } + return nil, fmt.Errorf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s", name, suggestions) } - return nil + return nil, nil } // Category returns the named CommandCategory on App. Returns nil if the category does not exist diff --git a/command_test.go b/command_test.go index 8d8ee1f..9045196 100644 --- a/command_test.go +++ b/command_test.go @@ -124,22 +124,22 @@ func TestCaseInsensitiveCommandNames(t *testing.T) { app.setup() - if c := app.BestCommand("project:list"); c != projectList { + if c, _ := app.BestCommand("project:list"); c != projectList { t.Fatalf("expected project:list, got %v", c) } - if c := app.BestCommand("Project:lISt"); c != projectList { + if c, _ := app.BestCommand("Project:lISt"); c != projectList { t.Fatalf("expected project:list, got %v", c) } - if c := app.BestCommand("project:link"); c != projectLink { + if c, _ := app.BestCommand("project:link"); c != projectLink { t.Fatalf("expected project:link, got %v", c) } - if c := app.BestCommand("project:Link"); c != projectLink { + if c, _ := app.BestCommand("project:Link"); c != projectLink { t.Fatalf("expected project:link, got %v", c) } - if c := app.BestCommand("foo"); c != projectList { + if c, _ := app.BestCommand("foo"); c != projectList { t.Fatalf("expected project:link, got %v", c) } - if c := app.BestCommand("FoO"); c != projectList { + if c, _ := app.BestCommand("FoO"); c != projectList { t.Fatalf("expected project:link, got %v", c) } } @@ -154,30 +154,36 @@ func TestFuzzyCommandNames(t *testing.T) { projectLink, } - c := app.BestCommand("project:list") + c, _ := app.BestCommand("project:list") if c != projectList { t.Fatalf("expected project:list, got %v", c) } - c = app.BestCommand("project:link") + c, _ = app.BestCommand("project:link") if c != projectLink { t.Fatalf("expected project:link, got %v", c) } - c = app.BestCommand("pro:list") + c, _ = app.BestCommand("pro:list") if c != projectList { t.Fatalf("expected project:list, got %v", c) } - c = app.BestCommand("pro:lis") + c, _ = app.BestCommand("pro:lis") if c != projectList { t.Fatalf("expected project:list, got %v", c) } - c = app.BestCommand("p:lis") + c, _ = app.BestCommand("p:lis") if c != projectList { t.Fatalf("expected project:list, got %v", c) } - c = app.BestCommand("p:li") + c, err := app.BestCommand("p:li") if c != nil { t.Fatalf("expected no matches, got %v", c) } + if err == nil { + t.Fatal("expected an error message, got none") + } + if !strings.Contains(err.Error(), `Command "p:li" is ambiguous`) { + t.Fatalf("error message does not match, got %v", err.Error()) + } } func TestCommandWithNoNames(t *testing.T) { diff --git a/help.go b/help.go index e0ccb98..6c4b939 100644 --- a/help.go +++ b/help.go @@ -147,7 +147,7 @@ func ShowAppHelp(c *Context) error { // ShowCommandHelp prints help for the given command func ShowCommandHelp(ctx *Context, command string) error { - if c := ctx.App.BestCommand(command); c != nil { + if c, _ := ctx.App.BestCommand(command); c != nil { if c.DescriptionFunc != nil { c.Description = c.DescriptionFunc(c, ctx.App) }