mirror of
https://github.com/spf13/cobra
synced 2024-12-27 14:57:06 +00:00
Allows command suggestions along with "unknown command" errors
This commit is contained in:
parent
4b86c66ef2
commit
b4087da7eb
4 changed files with 116 additions and 1 deletions
24
README.md
24
README.md
|
@ -418,6 +418,30 @@ func main() {
|
|||
}
|
||||
```
|
||||
|
||||
## Suggestions when "unknown command" happens
|
||||
|
||||
Cobra will print automatic suggestions when "unknown command" errors happen. This allows Cobra to behavior similarly to the `git` command when a typo happens. For example:
|
||||
|
||||
```
|
||||
$ hugo srever
|
||||
unknown command "srever" for "hugo"
|
||||
|
||||
Did you mean this?
|
||||
server
|
||||
|
||||
Run 'hugo --help' for usage.
|
||||
```
|
||||
|
||||
Suggestions are automatic based on every subcommand registered and use an implementation of Levenshtein distance. Every registered command that matches a minimum distance of 2 (ignoring case) will be displayed as a suggestion.
|
||||
|
||||
If you need to disable suggestions or tweak the string distance in your command, use:
|
||||
|
||||
command.DisableSuggestions = true
|
||||
|
||||
or
|
||||
|
||||
command.SuggestionsMinimumDistance = 1
|
||||
|
||||
## Generating markdown formatted documentation for your command
|
||||
|
||||
Cobra can generate a markdown formatted document based on the subcommands, flags, etc. A simple example of how to do this for your command can be found in [Markdown Docs](md_docs.md)
|
||||
|
|
36
cobra.go
36
cobra.go
|
@ -126,3 +126,39 @@ func tmpl(w io.Writer, text string, data interface{}) error {
|
|||
template.Must(t.Parse(text))
|
||||
return t.Execute(w, data)
|
||||
}
|
||||
|
||||
// ld compares two strings and returns the levenshtein distance between them
|
||||
func ld(s, t string, ignoreCase bool) int {
|
||||
if ignoreCase {
|
||||
s = strings.ToLower(s)
|
||||
t = strings.ToLower(t)
|
||||
}
|
||||
d := make([][]int, len(s)+1)
|
||||
for i := range d {
|
||||
d[i] = make([]int, len(t)+1)
|
||||
}
|
||||
for i := range d {
|
||||
d[i][0] = i
|
||||
}
|
||||
for j := range d[0] {
|
||||
d[0][j] = j
|
||||
}
|
||||
for j := 1; j <= len(t); j++ {
|
||||
for i := 1; i <= len(s); i++ {
|
||||
if s[i-1] == t[j-1] {
|
||||
d[i][j] = d[i-1][j-1]
|
||||
} else {
|
||||
min := d[i-1][j]
|
||||
if d[i][j-1] < min {
|
||||
min = d[i][j-1]
|
||||
}
|
||||
if d[i-1][j-1] < min {
|
||||
min = d[i-1][j-1]
|
||||
}
|
||||
d[i][j] = min + 1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return d[len(s)][len(t)]
|
||||
}
|
||||
|
|
|
@ -799,6 +799,34 @@ func TestRootUnknownCommand(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRootSuggestions(t *testing.T) {
|
||||
outputWithSuggestions := "Error: unknown command \"%s\" for \"cobra-test\"\n\nDid you mean this?\n\t%s\n\nRun 'cobra-test --help' for usage.\n"
|
||||
outputWithoutSuggestions := "Error: unknown command \"%s\" for \"cobra-test\"\nRun 'cobra-test --help' for usage.\n"
|
||||
|
||||
cmd := initializeWithRootCmd()
|
||||
cmd.AddCommand(cmdTimes)
|
||||
|
||||
tests := map[string]string{
|
||||
"time": "times",
|
||||
"tiems": "times",
|
||||
"timeS": "times",
|
||||
"rimes": "times",
|
||||
}
|
||||
|
||||
for typo, suggestion := range tests {
|
||||
cmd.DisableSuggestions = false
|
||||
result := simpleTester(cmd, typo)
|
||||
if expected := fmt.Sprintf(outputWithSuggestions, typo, suggestion); result.Output != expected {
|
||||
t.Errorf("Unexpected response.\nExpecting to be:\n %q\nGot:\n %q\n", expected, result.Output)
|
||||
}
|
||||
cmd.DisableSuggestions = true
|
||||
result = simpleTester(cmd, typo)
|
||||
if expected := fmt.Sprintf(outputWithoutSuggestions, typo); result.Output != expected {
|
||||
t.Errorf("Unexpected response.\nExpecting to be:\n %q\nGot:\n %q\n", expected, result.Output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagsBeforeCommand(t *testing.T) {
|
||||
// short without space
|
||||
x := fullSetupTest("-i10 echo")
|
||||
|
|
29
command.go
29
command.go
|
@ -106,6 +106,11 @@ type Command struct {
|
|||
helpCommand *Command // The help command
|
||||
// The global normalization function that we can use on every pFlag set and children commands
|
||||
globNormFunc func(f *flag.FlagSet, name string) flag.NormalizedName
|
||||
|
||||
// Disable the suggestions based on Levenshtein distance that go along with 'unknown command' messages
|
||||
DisableSuggestions bool
|
||||
// If displaying suggestions, allows to set the minimum levenshtein distance to display, must be > 0
|
||||
SuggestionsMinimumDistance int
|
||||
}
|
||||
|
||||
// os.Args[1:] by default, if desired, can be overridden
|
||||
|
@ -419,9 +424,31 @@ func (c *Command) Find(args []string) (*Command, []string, error) {
|
|||
if !commandFound.HasSubCommands() {
|
||||
return commandFound, a, nil
|
||||
}
|
||||
|
||||
// root command with subcommands, do subcommand checking
|
||||
if commandFound == c && len(argsWOflags) > 0 {
|
||||
return commandFound, a, fmt.Errorf("unknown command %q for %q", argsWOflags[0], commandFound.CommandPath())
|
||||
suggestions := ""
|
||||
if !c.DisableSuggestions {
|
||||
if c.SuggestionsMinimumDistance <= 0 {
|
||||
c.SuggestionsMinimumDistance = 2
|
||||
}
|
||||
similar := []string{}
|
||||
for _, cmd := range c.commands {
|
||||
if cmd.IsAvailableCommand() {
|
||||
levenshtein := ld(argsWOflags[0], cmd.Name(), true)
|
||||
if levenshtein <= c.SuggestionsMinimumDistance {
|
||||
similar = append(similar, cmd.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(similar) > 0 {
|
||||
suggestions += "\n\nDid you mean this?\n"
|
||||
for _, s := range similar {
|
||||
suggestions += fmt.Sprintf("\t%v\n", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
return commandFound, a, fmt.Errorf("unknown command %q for %q%s", argsWOflags[0], commandFound.CommandPath(), suggestions)
|
||||
}
|
||||
|
||||
return commandFound, a, nil
|
||||
|
|
Loading…
Reference in a new issue