mirror of
https://github.com/ipfs/kubo.git
synced 2025-06-29 17:36:38 +08:00
Merge pull request #1468 from sbruce/better-cli-parser-errors
Better error message on unrecognized command
This commit is contained in:
4
Godeps/Godeps.json
generated
4
Godeps/Godeps.json
generated
@ -279,6 +279,10 @@
|
|||||||
"ImportPath": "github.com/syndtr/gosnappy/snappy",
|
"ImportPath": "github.com/syndtr/gosnappy/snappy",
|
||||||
"Rev": "156a073208e131d7d2e212cb749feae7c339e846"
|
"Rev": "156a073208e131d7d2e212cb749feae7c339e846"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/texttheater/golang-levenshtein/levenshtein",
|
||||||
|
"Rev": "dfd657628c58d3eeaa26391097853b2473c8b94e"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/whyrusleeping/go-metrics",
|
"ImportPath": "github.com/whyrusleeping/go-metrics",
|
||||||
"Rev": "1cd8009604ec2238b5a71305a0ecd974066e0e16"
|
"Rev": "1cd8009604ec2238b5a71305a0ecd974066e0e16"
|
||||||
|
166
Godeps/_workspace/src/github.com/texttheater/golang-levenshtein/levenshtein/levenshtein.go
generated
vendored
Normal file
166
Godeps/_workspace/src/github.com/texttheater/golang-levenshtein/levenshtein/levenshtein.go
generated
vendored
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
package levenshtein
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EditOperation int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Ins = iota
|
||||||
|
Del
|
||||||
|
Sub
|
||||||
|
Match
|
||||||
|
)
|
||||||
|
|
||||||
|
type EditScript []EditOperation
|
||||||
|
|
||||||
|
type MatchFunction func(rune, rune) bool
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
InsCost int
|
||||||
|
DelCost int
|
||||||
|
SubCost int
|
||||||
|
Matches MatchFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultOptions is the default options: insertion cost is 1, deletion cost is
|
||||||
|
// 1, substitution cost is 2, and two runes match iff they are the same.
|
||||||
|
var DefaultOptions Options = Options{
|
||||||
|
InsCost: 1,
|
||||||
|
DelCost: 1,
|
||||||
|
SubCost: 2,
|
||||||
|
Matches: func(sourceCharacter rune, targetCharacter rune) bool {
|
||||||
|
return sourceCharacter == targetCharacter
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (operation EditOperation) String() string {
|
||||||
|
if operation == Match {
|
||||||
|
return "match"
|
||||||
|
} else if operation == Ins {
|
||||||
|
return "ins"
|
||||||
|
} else if operation == Sub {
|
||||||
|
return "sub"
|
||||||
|
}
|
||||||
|
return "del"
|
||||||
|
}
|
||||||
|
|
||||||
|
// DistanceForStrings returns the edit distance between source and target.
|
||||||
|
func DistanceForStrings(source []rune, target []rune, op Options) int {
|
||||||
|
return DistanceForMatrix(MatrixForStrings(source, target, op))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DistanceForMatrix reads the edit distance off the given Levenshtein matrix.
|
||||||
|
func DistanceForMatrix(matrix [][]int) int {
|
||||||
|
return matrix[len(matrix)-1][len(matrix[0])-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatrixForStrings generates a 2-D array representing the dynamic programming
|
||||||
|
// table used by the Levenshtein algorithm, as described e.g. here:
|
||||||
|
// http://www.let.rug.nl/kleiweg/lev/
|
||||||
|
// The reason for putting the creation of the table into a separate function is
|
||||||
|
// that it cannot only be used for reading of the edit distance between two
|
||||||
|
// strings, but also e.g. to backtrace an edit script that provides an
|
||||||
|
// alignment between the characters of both strings.
|
||||||
|
func MatrixForStrings(source []rune, target []rune, op Options) [][]int {
|
||||||
|
// Make a 2-D matrix. Rows correspond to prefixes of source, columns to
|
||||||
|
// prefixes of target. Cells will contain edit distances.
|
||||||
|
// Cf. http://www.let.rug.nl/~kleiweg/lev/levenshtein.html
|
||||||
|
height := len(source) + 1
|
||||||
|
width := len(target) + 1
|
||||||
|
matrix := make([][]int, height)
|
||||||
|
|
||||||
|
// Initialize trivial distances (from/to empty string). That is, fill
|
||||||
|
// the left column and the top row with row/column indices.
|
||||||
|
for i := 0; i < height; i++ {
|
||||||
|
matrix[i] = make([]int, width)
|
||||||
|
matrix[i][0] = i
|
||||||
|
}
|
||||||
|
for j := 1; j < width; j++ {
|
||||||
|
matrix[0][j] = j
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in the remaining cells: for each prefix pair, choose the
|
||||||
|
// (edit history, operation) pair with the lowest cost.
|
||||||
|
for i := 1; i < height; i++ {
|
||||||
|
for j := 1; j < width; j++ {
|
||||||
|
delCost := matrix[i-1][j] + op.DelCost
|
||||||
|
matchSubCost := matrix[i-1][j-1]
|
||||||
|
if !op.Matches(source[i-1], target[j-1]) {
|
||||||
|
matchSubCost += op.SubCost
|
||||||
|
}
|
||||||
|
insCost := matrix[i][j-1] + op.InsCost
|
||||||
|
matrix[i][j] = min(delCost, min(matchSubCost,
|
||||||
|
insCost))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//LogMatrix(source, target, matrix)
|
||||||
|
return matrix
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditScriptForStrings returns an optimal edit script to turn source into
|
||||||
|
// target.
|
||||||
|
func EditScriptForStrings(source []rune, target []rune, op Options) EditScript {
|
||||||
|
return backtrace(len(source), len(target),
|
||||||
|
MatrixForStrings(source, target, op), op)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditScriptForMatrix returns an optimal edit script based on the given
|
||||||
|
// Levenshtein matrix.
|
||||||
|
func EditScriptForMatrix(matrix [][]int, op Options) EditScript {
|
||||||
|
return backtrace(len(matrix[0])-1, len(matrix)-1, matrix, op)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogMatrix outputs a visual representation of the given matrix for the given
|
||||||
|
// strings on os.Stderr.
|
||||||
|
func LogMatrix(source []rune, target []rune, matrix [][]int) {
|
||||||
|
fmt.Fprintf(os.Stderr, " ")
|
||||||
|
for _, targetRune := range target {
|
||||||
|
fmt.Fprintf(os.Stderr, " %c", targetRune)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " %2d", matrix[0][0])
|
||||||
|
for j, _ := range target {
|
||||||
|
fmt.Fprintf(os.Stderr, " %2d", matrix[0][j+1])
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "\n")
|
||||||
|
for i, sourceRune := range source {
|
||||||
|
fmt.Fprintf(os.Stderr, "%c %2d", sourceRune, matrix[i+1][0])
|
||||||
|
for j, _ := range target {
|
||||||
|
fmt.Fprintf(os.Stderr, " %2d", matrix[i+1][j+1])
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func backtrace(i int, j int, matrix [][]int, op Options) EditScript {
|
||||||
|
if i > 0 && matrix[i-1][j]+op.DelCost == matrix[i][j] {
|
||||||
|
return append(backtrace(i-1, j, matrix, op), Del)
|
||||||
|
}
|
||||||
|
if j > 0 && matrix[i][j-1]+op.InsCost == matrix[i][j] {
|
||||||
|
return append(backtrace(i, j-1, matrix, op), Ins)
|
||||||
|
}
|
||||||
|
if i > 0 && j > 0 && matrix[i-1][j-1]+op.SubCost == matrix[i][j] {
|
||||||
|
return append(backtrace(i-1, j-1, matrix, op), Sub)
|
||||||
|
}
|
||||||
|
if i > 0 && j > 0 && matrix[i-1][j-1] == matrix[i][j] {
|
||||||
|
return append(backtrace(i-1, j-1, matrix, op), Match)
|
||||||
|
}
|
||||||
|
return []EditOperation{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(a int, b int) int {
|
||||||
|
if b < a {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func max(a int, b int) int {
|
||||||
|
if b > a {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
46
Godeps/_workspace/src/github.com/texttheater/golang-levenshtein/levenshtein/levenshtein_test.go
generated
vendored
Normal file
46
Godeps/_workspace/src/github.com/texttheater/golang-levenshtein/levenshtein/levenshtein_test.go
generated
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package levenshtein_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/texttheater/golang-levenshtein/levenshtein"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testCases = []struct {
|
||||||
|
source string
|
||||||
|
target string
|
||||||
|
distance int
|
||||||
|
}{
|
||||||
|
{"", "a", 1},
|
||||||
|
{"a", "aa", 1},
|
||||||
|
{"a", "aaa", 2},
|
||||||
|
{"", "", 0},
|
||||||
|
{"a", "b", 2},
|
||||||
|
{"aaa", "aba", 2},
|
||||||
|
{"aaa", "ab", 3},
|
||||||
|
{"a", "a", 0},
|
||||||
|
{"ab", "ab", 0},
|
||||||
|
{"a", "", 1},
|
||||||
|
{"aa", "a", 1},
|
||||||
|
{"aaa", "a", 2},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLevenshtein(t *testing.T) {
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
distance := levenshtein.DistanceForStrings(
|
||||||
|
[]rune(testCase.source),
|
||||||
|
[]rune(testCase.target),
|
||||||
|
levenshtein.DefaultOptions)
|
||||||
|
if distance != testCase.distance {
|
||||||
|
t.Log(
|
||||||
|
"Distance between",
|
||||||
|
testCase.source,
|
||||||
|
"and",
|
||||||
|
testCase.target,
|
||||||
|
"computed as",
|
||||||
|
distance,
|
||||||
|
", should be",
|
||||||
|
testCase.distance)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
commands/cli/cmd_suggestion.go
Normal file
71
commands/cli/cmd_suggestion.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
levenshtein "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/texttheater/golang-levenshtein/levenshtein"
|
||||||
|
cmds "github.com/ipfs/go-ipfs/commands"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Make a custom slice that can be sorted by its levenshtein value
|
||||||
|
type suggestionSlice []*suggestion
|
||||||
|
|
||||||
|
type suggestion struct {
|
||||||
|
cmd string
|
||||||
|
levenshtein int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s suggestionSlice) Len() int {
|
||||||
|
return len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s suggestionSlice) Swap(i, j int) {
|
||||||
|
s[i], s[j] = s[j], s[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s suggestionSlice) Less(i, j int) bool {
|
||||||
|
return s[i].levenshtein < s[j].levenshtein
|
||||||
|
}
|
||||||
|
|
||||||
|
func suggestUnknownCmd(args []string, root *cmds.Command) []string {
|
||||||
|
arg := args[0]
|
||||||
|
var suggestions []string
|
||||||
|
sortableSuggestions := make(suggestionSlice, 0)
|
||||||
|
var sFinal []string
|
||||||
|
const MIN_LEVENSHTEIN = 3
|
||||||
|
|
||||||
|
var options levenshtein.Options = levenshtein.Options{
|
||||||
|
InsCost: 1,
|
||||||
|
DelCost: 3,
|
||||||
|
SubCost: 2,
|
||||||
|
Matches: func(sourceCharacter rune, targetCharacter rune) bool {
|
||||||
|
return sourceCharacter == targetCharacter
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with a simple strings.Contains check
|
||||||
|
for name, _ := range root.Subcommands {
|
||||||
|
if strings.Contains(arg, name) {
|
||||||
|
suggestions = append(suggestions, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the string compare returns a match, return
|
||||||
|
if len(suggestions) > 0 {
|
||||||
|
return suggestions
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, _ := range root.Subcommands {
|
||||||
|
lev := levenshtein.DistanceForStrings([]rune(arg), []rune(name), options)
|
||||||
|
if lev <= MIN_LEVENSHTEIN {
|
||||||
|
sortableSuggestions = append(sortableSuggestions, &suggestion{name, lev})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Sort(sortableSuggestions)
|
||||||
|
|
||||||
|
for _, j := range sortableSuggestions {
|
||||||
|
sFinal = append(sFinal, j.cmd)
|
||||||
|
}
|
||||||
|
return sFinal
|
||||||
|
}
|
@ -41,7 +41,7 @@ func Parse(input []string, stdin *os.File, root *cmds.Command) (cmds.Request, *c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stringArgs, fileArgs, err := parseArgs(stringVals, stdin, cmd.Arguments, recursive)
|
stringArgs, fileArgs, err := parseArgs(stringVals, stdin, cmd.Arguments, recursive, root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return req, cmd, path, err
|
return req, cmd, path, err
|
||||||
}
|
}
|
||||||
@ -196,7 +196,7 @@ func parseOpts(args []string, root *cmds.Command) (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursive bool) ([]string, []files.File, error) {
|
func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursive bool, root *cmds.Command) ([]string, []files.File, error) {
|
||||||
// ignore stdin on Windows
|
// ignore stdin on Windows
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
stdin = nil
|
stdin = nil
|
||||||
@ -231,7 +231,15 @@ func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursi
|
|||||||
// and the last arg definition is not variadic (or there are no definitions), return an error
|
// and the last arg definition is not variadic (or there are no definitions), return an error
|
||||||
notVariadic := len(argDefs) == 0 || !argDefs[len(argDefs)-1].Variadic
|
notVariadic := len(argDefs) == 0 || !argDefs[len(argDefs)-1].Variadic
|
||||||
if notVariadic && len(inputs) > len(argDefs) {
|
if notVariadic && len(inputs) > len(argDefs) {
|
||||||
return nil, nil, fmt.Errorf("Expected %v arguments, got %v: %v", len(argDefs), len(inputs), inputs)
|
suggestions := suggestUnknownCmd(inputs, root)
|
||||||
|
|
||||||
|
if len(suggestions) > 1 {
|
||||||
|
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"))
|
||||||
|
} else if len(suggestions) > 0 {
|
||||||
|
return nil, nil, fmt.Errorf("Unknown Command \"%s\"\n\nDid you mean this?\n\n\t%s", inputs[0], suggestions[0])
|
||||||
|
} else {
|
||||||
|
return nil, nil, fmt.Errorf("Unknown Command \"%s\"\n", inputs[0])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stringArgs := make([]string, 0, numInputs)
|
stringArgs := make([]string, 0, numInputs)
|
||||||
|
43
test/sharness/t0150-clisuggest.sh
Executable file
43
test/sharness/t0150-clisuggest.sh
Executable file
@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
test_description="Test ipfs cli cmd suggest"
|
||||||
|
|
||||||
|
. lib/test-lib.sh
|
||||||
|
|
||||||
|
test_suggest() {
|
||||||
|
|
||||||
|
|
||||||
|
test_expect_success "test command fails" '
|
||||||
|
test_must_fail ipfs kog 2>actual
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success "test one command is suggested" '
|
||||||
|
grep "Did you mean this?" actual &&
|
||||||
|
grep "log" actual ||
|
||||||
|
test_fsh cat actual
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success "test command fails" '
|
||||||
|
test_must_fail ipfs lis 2>actual
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success "test multiple commands are suggested" '
|
||||||
|
grep "Did you mean any of these?" actual &&
|
||||||
|
grep "ls" actual &&
|
||||||
|
grep "id" actual ||
|
||||||
|
test_fsh cat actual
|
||||||
|
'
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
test_init_ipfs
|
||||||
|
|
||||||
|
test_suggest
|
||||||
|
|
||||||
|
test_launch_ipfs_daemon
|
||||||
|
|
||||||
|
test_suggest
|
||||||
|
|
||||||
|
test_kill_ipfs_daemon
|
||||||
|
|
||||||
|
test_done
|
Reference in New Issue
Block a user