1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
|
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
}
|