Merge pull request #149 from fabianofranz/siblings
Allows command suggestions along with "unknown command" errors
This commit is contained in:
		
							
								
								
									
										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
									
									
									
									
									
								
							@ -132,3 +132,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
 | 
				
			||||||
@ -421,9 +426,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
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user