The new option `app.Suggest` enables command and flag suggestions via the jaro-winkler distance algorithm. Flags are scoped to their appropriate commands whereas command suggestions are scoped to the current command level. Signed-off-by: Sascha Grunert <sgrunert@suse.com>main
parent
1b7e4e00c7
commit
002bde2233
@ -0,0 +1,75 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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) {
|
||||||
|
distance := 0.0
|
||||||
|
|
||||||
|
for _, flag := range flags {
|
||||||
|
flagNames := flag.Names()
|
||||||
|
if !a.HideHelp {
|
||||||
|
flagNames = append(flagNames, HelpFlag.Names()...)
|
||||||
|
}
|
||||||
|
for _, name := range flagNames {
|
||||||
|
newDistance := matchr.JaroWinkler(name, provided, true)
|
||||||
|
if newDistance > distance {
|
||||||
|
distance = newDistance
|
||||||
|
suggestion = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(suggestion) == 1 {
|
||||||
|
suggestion = "-" + suggestion
|
||||||
|
} else if len(suggestion) > 1 {
|
||||||
|
suggestion = "--" + suggestion
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestion
|
||||||
|
}
|
||||||
|
|
||||||
|
// suggestCommand takes a list of commands and a provided string to suggest a
|
||||||
|
// command name
|
||||||
|
func suggestCommand(commands []*Command, provided string) (suggestion string) {
|
||||||
|
distance := 0.0
|
||||||
|
for _, command := range commands {
|
||||||
|
for _, name := range append(command.Names(), helpName, helpAlias) {
|
||||||
|
newDistance := matchr.JaroWinkler(name, provided, true)
|
||||||
|
if newDistance > distance {
|
||||||
|
distance = newDistance
|
||||||
|
suggestion = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(didYouMeanTemplate, suggestion)
|
||||||
|
}
|
@ -0,0 +1,122 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSuggestFlag(t *testing.T) {
|
||||||
|
// Given
|
||||||
|
app := testApp()
|
||||||
|
|
||||||
|
for _, testCase := range []struct {
|
||||||
|
provided, expected string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{"a", "--another-flag"},
|
||||||
|
{"hlp", "--help"},
|
||||||
|
{"k", ""},
|
||||||
|
{"s", "-s"},
|
||||||
|
} {
|
||||||
|
// When
|
||||||
|
res := app.suggestFlag(app.Flags, testCase.provided)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(t, res, testCase.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSuggestFlagHideHelp(t *testing.T) {
|
||||||
|
// Given
|
||||||
|
app := testApp()
|
||||||
|
app.HideHelp = true
|
||||||
|
|
||||||
|
// When
|
||||||
|
res := app.suggestFlag(app.Flags, "hlp")
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(t, res, "--fl")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSuggestFlagFromError(t *testing.T) {
|
||||||
|
// Given
|
||||||
|
app := testApp()
|
||||||
|
|
||||||
|
for _, testCase := range []struct {
|
||||||
|
command, provided, expected string
|
||||||
|
}{
|
||||||
|
{"", "hel", "--help"},
|
||||||
|
{"", "soccer", "--socket"},
|
||||||
|
{"config", "anot", "--another-flag"},
|
||||||
|
} {
|
||||||
|
// When
|
||||||
|
res, _ := app.suggestFlagFromError(
|
||||||
|
errors.New(providedButNotDefinedErrMsg+testCase.provided),
|
||||||
|
testCase.command,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(t, res, fmt.Sprintf(didYouMeanTemplate+"\n\n", testCase.expected))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSuggestFlagFromErrorWrongError(t *testing.T) {
|
||||||
|
// Given
|
||||||
|
app := testApp()
|
||||||
|
|
||||||
|
// When
|
||||||
|
_, err := app.suggestFlagFromError(errors.New("invalid"), "")
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(t, true, err != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSuggestFlagFromErrorWrongCommand(t *testing.T) {
|
||||||
|
// Given
|
||||||
|
app := testApp()
|
||||||
|
|
||||||
|
// When
|
||||||
|
_, err := app.suggestFlagFromError(
|
||||||
|
errors.New(providedButNotDefinedErrMsg+"flag"),
|
||||||
|
"invalid",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(t, true, err != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSuggestFlagFromErrorNoSuggestion(t *testing.T) {
|
||||||
|
// Given
|
||||||
|
app := testApp()
|
||||||
|
|
||||||
|
// When
|
||||||
|
_, err := app.suggestFlagFromError(
|
||||||
|
errors.New(providedButNotDefinedErrMsg+""),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(t, true, err != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSuggestCommand(t *testing.T) {
|
||||||
|
// Given
|
||||||
|
app := testApp()
|
||||||
|
|
||||||
|
for _, testCase := range []struct {
|
||||||
|
provided, expected string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{"conf", "config"},
|
||||||
|
{"i", "i"},
|
||||||
|
{"information", "info"},
|
||||||
|
{"not-existing", "info"},
|
||||||
|
} {
|
||||||
|
// When
|
||||||
|
res := suggestCommand(app.Commands, testCase.provided)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(t, res, fmt.Sprintf(didYouMeanTemplate, testCase.expected))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue