mirror of
https://github.com/spf13/cobra
synced 2024-11-24 22:57:12 +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
|
## 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)
|
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))
|
template.Must(t.Parse(text))
|
||||||
return t.Execute(w, data)
|
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) {
|
func TestFlagsBeforeCommand(t *testing.T) {
|
||||||
// short without space
|
// short without space
|
||||||
x := fullSetupTest("-i10 echo")
|
x := fullSetupTest("-i10 echo")
|
||||||
|
|
29
command.go
29
command.go
|
@ -106,6 +106,11 @@ type Command struct {
|
||||||
helpCommand *Command // The help command
|
helpCommand *Command // The help command
|
||||||
// The global normalization function that we can use on every pFlag set and children commands
|
// The global normalization function that we can use on every pFlag set and children commands
|
||||||
globNormFunc func(f *flag.FlagSet, name string) flag.NormalizedName
|
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
|
// 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() {
|
if !commandFound.HasSubCommands() {
|
||||||
return commandFound, a, nil
|
return commandFound, a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// root command with subcommands, do subcommand checking
|
// root command with subcommands, do subcommand checking
|
||||||
if commandFound == c && len(argsWOflags) > 0 {
|
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
|
return commandFound, a, nil
|
||||||
|
|
Loading…
Reference in a new issue