Better error message on unrecognized command
Closes issue #1436 License: MIT Signed-off-by: Shaun Bruce <shaun.m.bruce@gmail.com>
Shaun Bruce committed
Jul 12, 2015 at 16:37 UTC
c175700dea01c9da6dcdc0c31e8ae74900b82642
6 files changed
+341
-3
Godeps/Godeps.json
+4
@@ -279,6 +279,10 @@
279
"ImportPath": "github.com/syndtr/gosnappy/snappy",
280
"Rev": "156a073208e131d7d2e212cb749feae7c339e846"
281
},
282
+ {
283
+ "ImportPath": "github.com/texttheater/golang-levenshtein/levenshtein",
284
+ "Rev": "dfd657628c58d3eeaa26391097853b2473c8b94e"
285
+ },
286
{
287
"ImportPath": "github.com/whyrusleeping/go-metrics",
288
"Rev": "1cd8009604ec2238b5a71305a0ecd974066e0e16"
Godeps/_workspace/src/github.com/texttheater/golang-levenshtein/levenshtein/levenshtein.go
new
+166
@@ -0,0 +1,166 @@
1
+package levenshtein
2
+
3
+import (
4
+ "fmt"
5
+ "os"
6
+)
7
+
8
+type EditOperation int
9
+
10
+const (
11
+ Ins = iota
12
+ Del
13
+ Sub
14
+ Match
15
+)
16
+
17
+type EditScript []EditOperation
18
+
19
+type MatchFunction func(rune, rune) bool
20
+
21
+type Options struct {
22
+ InsCost int
23
+ DelCost int
24
+ SubCost int
25
+ Matches MatchFunction
26
+}
27
+
28
+// DefaultOptions is the default options: insertion cost is 1, deletion cost is
29
+// 1, substitution cost is 2, and two runes match iff they are the same.
30
+var DefaultOptions Options = Options{
31
+ InsCost: 1,
32
+ DelCost: 1,
33
+ SubCost: 2,
34
+ Matches: func(sourceCharacter rune, targetCharacter rune) bool {
35
+ return sourceCharacter == targetCharacter
36
+ },
37
+}
38
+
39
+func (operation EditOperation) String() string {
40
+ if operation == Match {
41
+ return "match"
42
+ } else if operation == Ins {
43
+ return "ins"
44
+ } else if operation == Sub {
45
+ return "sub"
46
+ }
47
+ return "del"
48
+}
49
+
50
+// DistanceForStrings returns the edit distance between source and target.
51
+func DistanceForStrings(source []rune, target []rune, op Options) int {
52
+ return DistanceForMatrix(MatrixForStrings(source, target, op))
53
+}
54
+
55
+// DistanceForMatrix reads the edit distance off the given Levenshtein matrix.
56
+func DistanceForMatrix(matrix [][]int) int {
57
+ return matrix[len(matrix)-1][len(matrix[0])-1]
58
+}
59
+
60
+// MatrixForStrings generates a 2-D array representing the dynamic programming
61
+// table used by the Levenshtein algorithm, as described e.g. here:
62
+// http://www.let.rug.nl/kleiweg/lev/
63
+// The reason for putting the creation of the table into a separate function is
64
+// that it cannot only be used for reading of the edit distance between two
65
+// strings, but also e.g. to backtrace an edit script that provides an
66
+// alignment between the characters of both strings.
67
+func MatrixForStrings(source []rune, target []rune, op Options) [][]int {
68
+ // Make a 2-D matrix. Rows correspond to prefixes of source, columns to
69
+ // prefixes of target. Cells will contain edit distances.
70
+ // Cf. http://www.let.rug.nl/~kleiweg/lev/levenshtein.html
71
+ height := len(source) + 1
72
+ width := len(target) + 1
73
+ matrix := make([][]int, height)
74
+
75
+ // Initialize trivial distances (from/to empty string). That is, fill
76
+ // the left column and the top row with row/column indices.
77
+ for i := 0; i < height; i++ {
78
+ matrix[i] = make([]int, width)
79
+ matrix[i][0] = i
80
+ }
81
+ for j := 1; j < width; j++ {
82
+ matrix[0][j] = j
83
+ }
84
+
85
+ // Fill in the remaining cells: for each prefix pair, choose the
86
+ // (edit history, operation) pair with the lowest cost.
87
+ for i := 1; i < height; i++ {
88
+ for j := 1; j < width; j++ {
89
+ delCost := matrix[i-1][j] + op.DelCost
90
+ matchSubCost := matrix[i-1][j-1]
91
+ if !op.Matches(source[i-1], target[j-1]) {
92
+ matchSubCost += op.SubCost
93
+ }
94
+ insCost := matrix[i][j-1] + op.InsCost
95
+ matrix[i][j] = min(delCost, min(matchSubCost,
96
+ insCost))
97
+ }
98
+ }
99
+ //LogMatrix(source, target, matrix)
100
+ return matrix
101
+}
102
+
103
+// EditScriptForStrings returns an optimal edit script to turn source into
104
+// target.
105
+func EditScriptForStrings(source []rune, target []rune, op Options) EditScript {
106
+ return backtrace(len(source), len(target),
107
+ MatrixForStrings(source, target, op), op)
108
+}
109
+
110
+// EditScriptForMatrix returns an optimal edit script based on the given
111
+// Levenshtein matrix.
112
+func EditScriptForMatrix(matrix [][]int, op Options) EditScript {
113
+ return backtrace(len(matrix[0])-1, len(matrix)-1, matrix, op)
114
+}
115
+
116
+// LogMatrix outputs a visual representation of the given matrix for the given
117
+// strings on os.Stderr.
118
+func LogMatrix(source []rune, target []rune, matrix [][]int) {
119
+ fmt.Fprintf(os.Stderr, " ")
120
+ for _, targetRune := range target {
121
+ fmt.Fprintf(os.Stderr, " %c", targetRune)
122
+ }
123
+ fmt.Fprintf(os.Stderr, "\n")
124
+ fmt.Fprintf(os.Stderr, " %2d", matrix[0][0])
125
+ for j, _ := range target {
126
+ fmt.Fprintf(os.Stderr, " %2d", matrix[0][j+1])
127
+ }
128
+ fmt.Fprintf(os.Stderr, "\n")
129
+ for i, sourceRune := range source {
130
+ fmt.Fprintf(os.Stderr, "%c %2d", sourceRune, matrix[i+1][0])
131
+ for j, _ := range target {
132
+ fmt.Fprintf(os.Stderr, " %2d", matrix[i+1][j+1])
133
+ }
134
+ fmt.Fprintf(os.Stderr, "\n")
135
+ }
136
+}
137
+
138
+func backtrace(i int, j int, matrix [][]int, op Options) EditScript {
139
+ if i > 0 && matrix[i-1][j]+op.DelCost == matrix[i][j] {
140
+ return append(backtrace(i-1, j, matrix, op), Del)
141
+ }
142
+ if j > 0 && matrix[i][j-1]+op.InsCost == matrix[i][j] {
143
+ return append(backtrace(i, j-1, matrix, op), Ins)
144
+ }
145
+ if i > 0 && j > 0 && matrix[i-1][j-1]+op.SubCost == matrix[i][j] {
146
+ return append(backtrace(i-1, j-1, matrix, op), Sub)
147
+ }
148
+ if i > 0 && j > 0 && matrix[i-1][j-1] == matrix[i][j] {
149
+ return append(backtrace(i-1, j-1, matrix, op), Match)
150
+ }
151
+ return []EditOperation{}
152
+}
153
+
154
+func min(a int, b int) int {
155
+ if b < a {
156
+ return b
157
+ }
158
+ return a
159
+}
160
+
161
+func max(a int, b int) int {
162
+ if b > a {
163
+ return b
164
+ }
165
+ return a
166
+}
Godeps/_workspace/src/github.com/texttheater/golang-levenshtein/levenshtein/levenshtein_test.go
new
+46
@@ -0,0 +1,46 @@
1
+package levenshtein_test
2
+
3
+import (
4
+ "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/texttheater/golang-levenshtein/levenshtein"
5
+ "testing"
6
+)
7
+
8
+var testCases = []struct {
9
+ source string
10
+ target string
11
+ distance int
12
+}{
13
+ {"", "a", 1},
14
+ {"a", "aa", 1},
15
+ {"a", "aaa", 2},
16
+ {"", "", 0},
17
+ {"a", "b", 2},
18
+ {"aaa", "aba", 2},
19
+ {"aaa", "ab", 3},
20
+ {"a", "a", 0},
21
+ {"ab", "ab", 0},
22
+ {"a", "", 1},
23
+ {"aa", "a", 1},
24
+ {"aaa", "a", 2},
25
+}
26
+
27
+func TestLevenshtein(t *testing.T) {
28
+ for _, testCase := range testCases {
29
+ distance := levenshtein.DistanceForStrings(
30
+ []rune(testCase.source),
31
+ []rune(testCase.target),
32
+ levenshtein.DefaultOptions)
33
+ if distance != testCase.distance {
34
+ t.Log(
35
+ "Distance between",
36
+ testCase.source,
37
+ "and",
38
+ testCase.target,
39
+ "computed as",
40
+ distance,
41
+ ", should be",
42
+ testCase.distance)
43
+ t.Fail()
44
+ }
45
+ }
46
+}
commands/cli/cmd_suggestion.go
new
+71
@@ -0,0 +1,71 @@
1
+package cli
2
+
3
+import (
4
+ "sort"
5
+ "strings"
6
+
7
+ levenshtein "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/texttheater/golang-levenshtein/levenshtein"
8
+ cmds "github.com/ipfs/go-ipfs/commands"
9
+)
10
+
11
+// Make a custom slice that can be sorted by its levenshtein value
12
+type suggestionSlice []*suggestion
13
+
14
+type suggestion struct {
15
+ cmd string
16
+ levenshtein int
17
+}
18
+
19
+func (s suggestionSlice) Len() int {
20
+ return len(s)
21
+}
22
+
23
+func (s suggestionSlice) Swap(i, j int) {
24
+ s[i], s[j] = s[j], s[i]
25
+}
26
+
27
+func (s suggestionSlice) Less(i, j int) bool {
28
+ return s[i].levenshtein < s[j].levenshtein
29
+}
30
+
31
+func suggestUnknownCmd(args []string, root *cmds.Command) []string {
32
+ arg := args[0]
33
+ var suggestions []string
34
+ sortableSuggestions := make(suggestionSlice, 0)
35
+ var sFinal []string
36
+ const MIN_LEVENSHTEIN = 3
37
+
38
+ var options levenshtein.Options = levenshtein.Options{
39
+ InsCost: 1,
40
+ DelCost: 3,
41
+ SubCost: 2,
42
+ Matches: func(sourceCharacter rune, targetCharacter rune) bool {
43
+ return sourceCharacter == targetCharacter
44
+ },
45
+ }
46
+
47
+ // Start with a simple strings.Contains check
48
+ for name, _ := range root.Subcommands {
49
+ if strings.Contains(arg, name) {
50
+ suggestions = append(suggestions, name)
51
+ }
52
+ }
53
+
54
+ // If the string compare returns a match, return
55
+ if len(suggestions) > 0 {
56
+ return suggestions
57
+ }
58
+
59
+ for name, _ := range root.Subcommands {
60
+ lev := levenshtein.DistanceForStrings([]rune(arg), []rune(name), options)
61
+ if lev <= MIN_LEVENSHTEIN {
62
+ sortableSuggestions = append(sortableSuggestions, &suggestion{name, lev})
63
+ }
64
+ }
65
+ sort.Sort(sortableSuggestions)
66
+
67
+ for _, j := range sortableSuggestions {
68
+ sFinal = append(sFinal, j.cmd)
69
+ }
70
+ return sFinal
71
+}
commands/cli/parse.go
+11
-3
@@ -41,7 +41,7 @@ func Parse(input []string, stdin *os.File, root *cmds.Command) (cmds.Request, *c
41
}
42
}
43
44
- stringArgs, fileArgs, err := parseArgs(stringVals, stdin, cmd.Arguments, recursive)
44
+ stringArgs, fileArgs, err := parseArgs(stringVals, stdin, cmd.Arguments, recursive, root)
45
if err != nil {
46
return req, cmd, path, err
47
}
@@ -196,7 +196,7 @@ func parseOpts(args []string, root *cmds.Command) (
196
return
197
}
198
199
-func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursive bool) ([]string, []files.File, error) {
199
+func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursive bool, root *cmds.Command) ([]string, []files.File, error) {
200
// ignore stdin on Windows
201
if runtime.GOOS == "windows" {
202
stdin = nil
@@ -231,7 +231,15 @@ func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursi
231
// and the last arg definition is not variadic (or there are no definitions), return an error
232
notVariadic := len(argDefs) == 0 || !argDefs[len(argDefs)-1].Variadic
233
if notVariadic && len(inputs) > len(argDefs) {
234
- return nil, nil, fmt.Errorf("Expected %v arguments, got %v: %v", len(argDefs), len(inputs), inputs)
234
+ suggestions := suggestUnknownCmd(inputs, root)
235
+
236
+ if len(suggestions) > 1 {
237
+ return nil, nil, fmt.Errorf("Unknown Command \"%s\"\n\nDid you mean any of these?\n\n\t%s", inputs[0], strings.Join(suggestions, "\n\t"))
238
+ } else if len(suggestions) > 0 {
239
+ return nil, nil, fmt.Errorf("Unknown Command \"%s\"\n\nDid you mean this?\n\n\t%s", inputs[0], suggestions[0])
240
+ } else {
241
+ return nil, nil, fmt.Errorf("Unknown Command \"%s\"\n", inputs[0])
242
+ }
243
}
244
245
stringArgs := make([]string, 0, numInputs)
test/sharness/t0150-clisuggest.sh
new
+43
@@ -0,0 +1,43 @@
1
+#!/bin/sh
2
+
3
+test_description="Test ipfs cli cmd suggest"
4
+
5
+. lib/test-lib.sh
6
+
7
+test_suggest() {
8
+
9
+
10
+ test_expect_success "test command fails" '
11
+ test_must_fail ipfs kog 2>actual
12
+ '
13
+
14
+ test_expect_success "test one command is suggested" '
15
+ grep "Did you mean this?" actual &&
16
+ grep "log" actual ||
17
+ test_fsh cat actual
18
+ '
19
+
20
+ test_expect_success "test command fails" '
21
+ test_must_fail ipfs lis 2>actual
22
+ '
23
+
24
+ test_expect_success "test multiple commands are suggested" '
25
+ grep "Did you mean any of these?" actual &&
26
+ grep "ls" actual &&
27
+ grep "id" actual ||
28
+ test_fsh cat actual
29
+ '
30
+
31
+}
32
+
33
+test_init_ipfs
34
+
35
+test_suggest
36
+
37
+test_launch_ipfs_daemon
38
+
39
+test_suggest
40
+
41
+test_kill_ipfs_daemon
42
+
43
+test_done