-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathspell_correct_path.go
More file actions
129 lines (123 loc) · 3.71 KB
/
Copy pathspell_correct_path.go
File metadata and controls
129 lines (123 loc) · 3.71 KB
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
// Naive implementation of a spell-checker for programs that are available to run in the bash shell.
// Works by computing 1 and 2 distance edits from the input word and comparing them against all available:
// - Shell executables
// - Aliases
// - Functions
package main
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os/exec"
"strings"
)
// Used to generate suggestions, should include all valid characters for CLI names
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890._-"
func build_corpus() (map[string]bool, error) {
// Get all available symbols that are available in my bash shell
s := make(map[string]bool)
process := exec.Command("bash", "-i", "-c", "compgen -A function -ac")
stdout, err := process.StdoutPipe()
if err != nil {
return s, err
}
err = process.Start()
if err != nil {
return s, err
}
// Get the required data from the stdout of the bash invocation
data, err := ioutil.ReadAll(stdout)
if err != nil {
return s, err
}
err = process.Wait()
if err != nil {
return s, err
}
for _, program := range strings.Split(string(data), "\n") {
s[program] = true
}
return s, nil
}
func editDistance1(input string) []string {
var splits [][]string
var possibilities []string
// Create a set of partial-words split by index
for i := 0; i < len(input)+1; i++ {
splits = append(splits, []string{input[:i], input[i:]})
}
// Now generate off-by-one possibilities for each combination
for _, sp := range splits {
// Delete one character
if sp[1] != "" {
possibilities = append(possibilities, sp[0]+sp[1][1:])
}
// Transpose one character
if len(sp[1]) > 1 {
possibilities = append(possibilities, fmt.Sprintf("%s%c%c%s", sp[0], sp[1][1], sp[1][0], sp[1][2:]))
}
// Replace one character
if sp[1] != "" {
for _, ch := range alphabet {
possibilities = append(possibilities, fmt.Sprintf("%s%c%s", sp[0], ch, sp[1][1:]))
}
}
// Insert one character
for _, ch := range alphabet {
possibilities = append(possibilities, fmt.Sprintf("%s%c%s", sp[0], ch, sp[1]))
}
}
return possibilities
}
// Recursively generates the second edit distance from the input string
func editDistance2(distance1 []string) []string {
var result []string
for _, val := range distance1 {
l := editDistance1(val)
result = append(result, l...)
}
return result
}
func SpellCorrectCommand(args []string, _ io.Reader) error {
/* Ideas for improvement:
* Implement flags that select which kinds of things we should correct against (just executables, etc)
* Create a weight for each program based on how frequently it's used
* Add benchmarking/tests
* TODO fix whatever is breaking "s"
*/
// Make sure we have one cli argument, the name to attempt correction on
if len(args) < 2 {
return fmt.Errorf("Usage: %s <token to spell correct>", args[0])
}
correctionTarget := args[1]
// Load all possibilities
executables, err := build_corpus()
if err != nil {
return err
}
// If the name is within the possibilities group, return immediately
if _, ok := executables[correctionTarget]; ok {
fmt.Println(correctionTarget)
return nil
}
// Generate all off-by-1 and off-by-2 correction possibilities for name
distance_one := editDistance1(correctionTarget)
for _, val := range distance_one {
// Blindly select the first match
// TODO improve this later if necessary
if _, ok := executables[val]; ok {
fmt.Println(val)
return nil
}
}
// If we've gotten this far, none of the first-distance corrections worked. More, more I say!
for _, val := range editDistance2(distance_one) {
if _, ok := executables[val]; ok {
fmt.Println(val)
return nil
}
}
// No match found in two distances, return an exit status of 1
return errors.New("Command not found")
}