diff --git a/tools/cli/command.go b/tools/cli/command.go index 5527865b1..2a2dab134 100644 --- a/tools/cli/command.go +++ b/tools/cli/command.go @@ -301,6 +301,28 @@ func (self *Command) GetVisibleOptions() ([]string, map[string][]*Option) { return group_titles, gmap } +func (self *Command) SuggestionsForOption(name_with_hyphens string, max_distance int /* good default is 2 */) []string { + ans := make([]string, 0, 8) + q := strings.ToLower(name_with_hyphens) + self.VisitAllOptions(func(opt *Option) error { + for _, a := range opt.Aliases { + as := a.String() + if utils.LevenshteinDistance(as, q, true) <= max_distance { + ans = append(ans, as) + } + } + return nil + }) + utils.StableSort(ans, func(a, b string) bool { + la, lb := utils.LevenshteinDistance(a, q, true), utils.LevenshteinDistance(b, q, true) + if la != lb { + return la < lb + } + return a < b + }) + return ans +} + func (self *Command) FindSubCommand(name string) *Command { for _, g := range self.SubCommandGroups { c := g.FindSubCommand(name) diff --git a/tools/cli/parse-args.go b/tools/cli/parse-args.go index 1706a3000..c3b39f876 100644 --- a/tools/cli/parse-args.go +++ b/tools/cli/parse-args.go @@ -26,6 +26,10 @@ func (self *Command) parse_args(ctx *Context, args []string) error { opt = possible_options[0] opt_str = opt.MatchingAlias(NormalizeOptionName(opt_str), !strings.HasPrefix(opt_str, "--")) } else if len(possible_options) == 0 { + possibles := self.SuggestionsForOption(opt_str, 2) + if len(possibles) > 0 { + return &ParseError{Message: fmt.Sprintf("Unknown option: :yellow:`%s`. Did you mean:\n\t%s", opt_str, strings.Join(possibles, "\n\t"))} + } return &ParseError{Message: fmt.Sprintf("Unknown option: :yellow:`%s`", opt_str)} } else { ambi := make([]string, len(possible_options)) diff --git a/tools/utils/levenshtein.go b/tools/utils/levenshtein.go new file mode 100644 index 000000000..d49095cf9 --- /dev/null +++ b/tools/utils/levenshtein.go @@ -0,0 +1,46 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +package utils + +import ( + "fmt" + "strings" +) + +var _ = fmt.Print + +// compares two strings and returns the Levenshtein distance between them. +func LevenshteinDistance(s, t string, ignore_case bool) int { + if ignore_case { + 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)] +}