@cryptotaxi247 / kubo / commits / c175700de

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