Repository: urfave/cli
Pull request #1396 (2022-05-17 13:04:02)

Introduce override hooks for suggestions
## What type of PR is this?

- feature

## What this PR does / why we need it:

Allow override of flag and command suggestion capability.

Related to https://github.com/urfave/cli/pull/1390#discussion_r871398659


Patch:
diff --git a/app.go b/app.go
index 463437a..dd7c9e4 100644
--- a/app.go
+++ b/app.go
@@ -11,6 +11,8 @@ import (
 	"time"
 )
 
+const suggestDidYouMeanTemplate = "Did you mean %q?"
+
 var (
 	changeLogURL            = "https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md"
 	appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL)
@@ -18,6 +20,10 @@ var (
 	errInvalidActionType    = NewExitError("ERROR invalid Action type. "+
 		fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error).  %s", contactSysadmin)+
 		fmt.Sprintf("See %s", appActionDeprecationURL), 2)
+
+	SuggestFlag               SuggestFlagFunc    = suggestFlag
+	SuggestCommand            SuggestCommandFunc = suggestCommand
+	SuggestDidYouMeanTemplate string             = suggestDidYouMeanTemplate
 )
 
 // App is the main structure of a cli application. It is recommended that
@@ -100,6 +106,10 @@ type App struct {
 	didSetup bool
 }
 
+type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string
+
+type SuggestCommandFunc func(commands []*Command, provided string) string
+
 // Tries to find out when this binary was compiled.
 // Returns the current time if it fails to find it.
 func compileTime() time.Time {
@@ -332,6 +342,29 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
 	return err
 }
 
+func (a *App) suggestFlagFromError(err error, command string) (string, error) {
+	flag, parseErr := flagFromError(err)
+	if parseErr != nil {
+		return "", err
+	}
+
+	flags := a.Flags
+	if command != "" {
+		cmd := a.Command(command)
+		if cmd == nil {
+			return "", err
+		}
+		flags = cmd.Flags
+	}
+
+	suggestion := SuggestFlag(flags, flag, a.HideHelp)
+	if len(suggestion) == 0 {
+		return "", err
+	}
+
+	return fmt.Sprintf(SuggestDidYouMeanTemplate+"\n\n", suggestion), nil
+}
+
 // RunAndExitOnError calls .Run() and exits non-zero if an error was returned
 //
 // Deprecated: instead you should return an error that fulfills cli.ExitCoder
diff --git a/godoc-current.txt b/godoc-current.txt
index 90700eb..acdac5e 100644
--- a/godoc-current.txt
+++ b/godoc-current.txt
@@ -26,6 +26,11 @@ application:
 
 VARIABLES
 
+var (
+	SuggestFlag               SuggestFlagFunc    = suggestFlag
+	SuggestCommand            SuggestCommandFunc = suggestCommand
+	SuggestDidYouMeanTemplate string             = suggestDidYouMeanTemplate
+)
 var AppHelpTemplate = `NAME:
    {{.Name}}{{if .Usage}} - {{.Usage}}{{end}}
 
@@ -1539,6 +1544,10 @@ func (f *StringSliceFlag) String() string
 func (f *StringSliceFlag) TakesValue() bool
     TakesValue returns true of the flag takes a value, otherwise false
 
+type SuggestCommandFunc func(commands []*Command, provided string) string
+
+type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string
+
 type Timestamp struct {
 	// Has unexported fields.
 }
diff --git a/help.go b/help.go
index 5103392..1ab7a6a 100644
--- a/help.go
+++ b/help.go
@@ -221,7 +221,7 @@ func ShowCommandHelp(ctx *Context, command string) error {
 	if ctx.App.CommandNotFound == nil {
 		errMsg := fmt.Sprintf("No help topic for '%v'", command)
 		if ctx.App.Suggest {
-			if suggestion := suggestCommand(ctx.App.Commands, command); suggestion != "" {
+			if suggestion := SuggestCommand(ctx.App.Commands, command); suggestion != "" {
 				errMsg += ". " + suggestion
 			}
 		}
diff --git a/suggestions.go b/suggestions.go
index 476af4d..65bb3cf 100644
--- a/suggestions.go
+++ b/suggestions.go
@@ -6,37 +6,13 @@ import (
 	"github.com/antzucaro/matchr"
 )
 
-const didYouMeanTemplate = "Did you mean '%s'?"
-
-func (a *App) suggestFlagFromError(err error, command string) (string, error) {
-	flag, parseErr := flagFromError(err)
-	if parseErr != nil {
-		return "", err
-	}
-
-	flags := a.Flags
-	if command != "" {
-		cmd := a.Command(command)
-		if cmd == nil {
-			return "", err
-		}
-		flags = cmd.Flags
-	}
-
-	suggestion := a.suggestFlag(flags, flag)
-	if len(suggestion) == 0 {
-		return "", err
-	}
-
-	return fmt.Sprintf(didYouMeanTemplate+"\n\n", suggestion), nil
-}
-
-func (a *App) suggestFlag(flags []Flag, provided string) (suggestion string) {
+func suggestFlag(flags []Flag, provided string, hideHelp bool) string {
 	distance := 0.0
+	suggestion := ""
 
 	for _, flag := range flags {
 		flagNames := flag.Names()
-		if !a.HideHelp {
+		if !hideHelp {
 			flagNames = append(flagNames, HelpFlag.Names()...)
 		}
 		for _, name := range flagNames {
@@ -71,5 +47,5 @@ func suggestCommand(commands []*Command, provided string) (suggestion string) {
 		}
 	}
 
-	return fmt.Sprintf(didYouMeanTemplate, suggestion)
+	return fmt.Sprintf(SuggestDidYouMeanTemplate, suggestion)
 }


Test patch:
diff --git a/suggestions_test.go b/suggestions_test.go
index 4ebe9c0..4c0984e 100644
--- a/suggestions_test.go
+++ b/suggestions_test.go
@@ -20,7 +20,7 @@ func TestSuggestFlag(t *testing.T) {
 		{"s", "-s"},
 	} {
 		// When
-		res := app.suggestFlag(app.Flags, testCase.provided)
+		res := suggestFlag(app.Flags, testCase.provided, false)
 
 		// Then
 		expect(t, res, testCase.expected)
@@ -30,10 +30,9 @@ func TestSuggestFlag(t *testing.T) {
 func TestSuggestFlagHideHelp(t *testing.T) {
 	// Given
 	app := testApp()
-	app.HideHelp = true
 
 	// When
-	res := app.suggestFlag(app.Flags, "hlp")
+	res := suggestFlag(app.Flags, "hlp", true)
 
 	// Then
 	expect(t, res, "--fl")
@@ -57,7 +56,7 @@ func TestSuggestFlagFromError(t *testing.T) {
 		)
 
 		// Then
-		expect(t, res, fmt.Sprintf(didYouMeanTemplate+"\n\n", testCase.expected))
+		expect(t, res, fmt.Sprintf(SuggestDidYouMeanTemplate+"\n\n", testCase.expected))
 	}
 }
 
@@ -117,7 +116,7 @@ func TestSuggestCommand(t *testing.T) {
 		res := suggestCommand(app.Commands, testCase.provided)
 
 		// Then
-		expect(t, res, fmt.Sprintf(didYouMeanTemplate, testCase.expected))
+		expect(t, res, fmt.Sprintf(SuggestDidYouMeanTemplate, testCase.expected))
 	}
 }
 
@@ -141,7 +140,7 @@ func ExampleApp_Suggest() {
 	// Output:
 	// Incorrect Usage. flag provided but not defined: -nema
 	//
-	// Did you mean '--name'?
+	// Did you mean "--name"?
 	//
 	// (this space intentionally left blank)
 }
@@ -182,7 +181,7 @@ func ExampleApp_Suggest_command() {
 	// Output:
 	// Incorrect Usage: flag provided but not defined: -sliming
 	//
-	// Did you mean '--smiling'?
+	// Did you mean "--smiling"?
 	//
 	// (this space intentionally left blank)
 }
diff --git a/testdata/godoc-v2.x.txt b/testdata/godoc-v2.x.txt
index 90700eb..acdac5e 100644
--- a/testdata/godoc-v2.x.txt
+++ b/testdata/godoc-v2.x.txt
@@ -26,6 +26,11 @@ application:
 
 VARIABLES
 
+var (
+	SuggestFlag               SuggestFlagFunc    = suggestFlag
+	SuggestCommand            SuggestCommandFunc = suggestCommand
+	SuggestDidYouMeanTemplate string             = suggestDidYouMeanTemplate
+)
 var AppHelpTemplate = `NAME:
    {{.Name}}{{if .Usage}} - {{.Usage}}{{end}}
 
@@ -1539,6 +1544,10 @@ func (f *StringSliceFlag) String() string
 func (f *StringSliceFlag) TakesValue() bool
     TakesValue returns true of the flag takes a value, otherwise false
 
+type SuggestCommandFunc func(commands []*Command, provided string) string
+
+type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string
+
 type Timestamp struct {
 	// Has unexported fields.
 }