148 lines
3.4 KiB
Go
148 lines
3.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"math"
|
|
)
|
|
|
|
const suggestDidYouMeanTemplate = "Did you mean %q?"
|
|
|
|
var (
|
|
SuggestFlag SuggestFlagFunc = suggestFlag
|
|
SuggestCommand SuggestCommandFunc = suggestCommand
|
|
SuggestDidYouMeanTemplate string = suggestDidYouMeanTemplate
|
|
)
|
|
|
|
type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string
|
|
|
|
type SuggestCommandFunc func(commands []*Command, provided string) string
|
|
|
|
// jaroDistance is the measure of similarity between two strings. It returns a
|
|
// value between 0 and 1, where 1 indicates identical strings and 0 indicates
|
|
// completely different strings.
|
|
//
|
|
// Adapted from https://github.com/xrash/smetrics/blob/5f08fbb34913bc8ab95bb4f2a89a0637ca922666/jaro.go.
|
|
func jaroDistance(a, b string) float64 {
|
|
if len(a) == 0 && len(b) == 0 {
|
|
return 1
|
|
}
|
|
if len(a) == 0 || len(b) == 0 {
|
|
return 0
|
|
}
|
|
|
|
lenA := float64(len(a))
|
|
lenB := float64(len(b))
|
|
hashA := make([]bool, len(a))
|
|
hashB := make([]bool, len(b))
|
|
maxDistance := int(math.Max(0, math.Floor(math.Max(lenA, lenB)/2.0)-1))
|
|
|
|
var matches float64
|
|
for i := 0; i < len(a); i++ {
|
|
start := int(math.Max(0, float64(i-maxDistance)))
|
|
end := int(math.Min(lenB-1, float64(i+maxDistance)))
|
|
|
|
for j := start; j <= end; j++ {
|
|
if hashB[j] {
|
|
continue
|
|
}
|
|
if a[i] == b[j] {
|
|
hashA[i] = true
|
|
hashB[j] = true
|
|
matches++
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if matches == 0 {
|
|
return 0
|
|
}
|
|
|
|
var transpositions float64
|
|
var j int
|
|
for i := 0; i < len(a); i++ {
|
|
if !hashA[i] {
|
|
continue
|
|
}
|
|
for !hashB[j] {
|
|
j++
|
|
}
|
|
if a[i] != b[j] {
|
|
transpositions++
|
|
}
|
|
j++
|
|
}
|
|
|
|
transpositions /= 2
|
|
return ((matches / lenA) + (matches / lenB) + ((matches - transpositions) / matches)) / 3.0
|
|
}
|
|
|
|
// jaroWinkler is more accurate when strings have a common prefix up to a
|
|
// defined maximum length.
|
|
//
|
|
// Adapted from https://github.com/xrash/smetrics/blob/5f08fbb34913bc8ab95bb4f2a89a0637ca922666/jaro-winkler.go.
|
|
func jaroWinkler(a, b string) float64 {
|
|
const (
|
|
boostThreshold = 0.7
|
|
prefixSize = 4
|
|
)
|
|
jaroDist := jaroDistance(a, b)
|
|
if jaroDist <= boostThreshold {
|
|
return jaroDist
|
|
}
|
|
|
|
prefix := int(math.Min(float64(len(a)), math.Min(float64(prefixSize), float64(len(b)))))
|
|
|
|
var prefixMatch float64
|
|
for i := 0; i < prefix; i++ {
|
|
if a[i] == b[i] {
|
|
prefixMatch++
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
return jaroDist + 0.1*prefixMatch*(1.0-jaroDist)
|
|
}
|
|
|
|
func suggestFlag(flags []Flag, provided string, hideHelp bool) string {
|
|
distance := 0.0
|
|
suggestion := ""
|
|
|
|
for _, flag := range flags {
|
|
flagNames := flag.Names()
|
|
if !hideHelp && HelpFlag != nil {
|
|
flagNames = append(flagNames, HelpFlag.Names()...)
|
|
}
|
|
for _, name := range flagNames {
|
|
newDistance := jaroWinkler(name, provided)
|
|
if newDistance > distance {
|
|
distance = newDistance
|
|
suggestion = name
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(suggestion) == 1 {
|
|
suggestion = "-" + suggestion
|
|
} else if len(suggestion) > 1 {
|
|
suggestion = "--" + suggestion
|
|
}
|
|
|
|
return suggestion
|
|
}
|
|
|
|
// suggestCommand takes a list of commands and a provided string to suggest a
|
|
// command name
|
|
func suggestCommand(commands []*Command, provided string) (suggestion string) {
|
|
distance := 0.0
|
|
for _, command := range commands {
|
|
for _, name := range append(command.Names(), helpName, helpAlias) {
|
|
newDistance := jaroWinkler(name, provided)
|
|
if newDistance > distance {
|
|
distance = newDistance
|
|
suggestion = name
|
|
}
|
|
}
|
|
}
|
|
|
|
return suggestion
|
|
}
|