Merge pull request #163 from fabianofranz/siblings_improvements

Improve suggestions
This commit is contained in:
Eric Paris 2015-09-29 11:27:22 -04:00
commit 046a673252
3 changed files with 68 additions and 31 deletions

View file

@ -442,6 +442,18 @@ or
command.SuggestionsMinimumDistance = 1
You can also explicitly set names for which a given command will be suggested using the `SuggestFor` attribute. This allows suggestions for strings that are not close in terms of string distance, but makes sense in your set of commands and for some which you don't want aliases. Example:
```
$ hugo delete
unknown command "delete" for "hugo"
Did you mean this?
remove
Run 'hugo --help' for usage.
```
## 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)

View file

@ -82,9 +82,10 @@ var cmdDeprecated = &Command{
}
var cmdTimes = &Command{
Use: "times [# times] [string to echo]",
Short: "Echo anything to the screen more times",
Long: `a slightly useless command for testing.`,
Use: "times [# times] [string to echo]",
SuggestFor: []string{"counts"},
Short: "Echo anything to the screen more times",
Long: `a slightly useless command for testing.`,
PersistentPreRun: func(cmd *Command, args []string) {
timesPersPre = args
},
@ -816,22 +817,33 @@ func TestRootSuggestions(t *testing.T) {
cmd.AddCommand(cmdTimes)
tests := map[string]string{
"time": "times",
"tiems": "times",
"timeS": "times",
"rimes": "times",
"time": "times",
"tiems": "times",
"tims": "times",
"timeS": "times",
"rimes": "times",
"ti": "times",
"t": "times",
"timely": "times",
"ri": "",
"timezone": "",
"foo": "",
"counts": "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)
for _, suggestionsDisabled := range []bool{false, true} {
cmd.DisableSuggestions = suggestionsDisabled
result := simpleTester(cmd, typo)
expected := ""
if len(suggestion) == 0 || suggestionsDisabled {
expected = fmt.Sprintf(outputWithoutSuggestions, typo)
} else {
expected = fmt.Sprintf(outputWithSuggestions, typo, suggestion)
}
if result.Output != expected {
t.Errorf("Unexpected response.\nExpecting to be:\n %q\nGot:\n %q\n", expected, result.Output)
}
}
}
}

View file

@ -39,6 +39,8 @@ type Command struct {
Use string
// An array of aliases that can be used instead of the first word in Use.
Aliases []string
// An array of command names for which this command will be suggested - similar to aliases but only suggests.
SuggestFor []string
// The short description shown in the 'help' output.
Short string
// The long message shown in the 'help <this-command>' output.
@ -429,33 +431,44 @@ func (c *Command) Find(args []string) (*Command, []string, error) {
// root command with subcommands, do subcommand checking
if commandFound == c && len(argsWOflags) > 0 {
suggestions := ""
suggestionsString := ""
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)
if suggestions := c.SuggestionsFor(argsWOflags[0]); len(suggestions) > 0 {
suggestionsString += "\n\nDid you mean this?\n"
for _, s := range suggestions {
suggestionsString += 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, fmt.Errorf("unknown command %q for %q%s", argsWOflags[0], commandFound.CommandPath(), suggestionsString)
}
return commandFound, a, nil
}
func (c *Command) SuggestionsFor(typedName string) []string {
suggestions := []string{}
for _, cmd := range c.commands {
if cmd.IsAvailableCommand() {
levenshteinDistance := ld(typedName, cmd.Name(), true)
suggestByLevenshtein := levenshteinDistance <= c.SuggestionsMinimumDistance
suggestByPrefix := strings.HasPrefix(strings.ToLower(cmd.Name()), strings.ToLower(typedName))
if suggestByLevenshtein || suggestByPrefix {
suggestions = append(suggestions, cmd.Name())
}
for _, explicitSuggestion := range cmd.SuggestFor {
if strings.EqualFold(typedName, explicitSuggestion) {
suggestions = append(suggestions, cmd.Name())
}
}
}
}
return suggestions
}
func (c *Command) Root() *Command {
var findRoot func(*Command) *Command