Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions pkg/cmd/suggest.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,22 @@ func jaroWinkler(a, b string) float64 {
return jaroDist + 0.1*prefixMatch*(1.0-jaroDist)
}

// suggestionThreshold is the minimum jaro-winkler similarity required before we
// will print a "Did you mean" suggestion. Below this, the closest match is too
// dissimilar to be a useful guess, so we stay silent rather than mislead.
// 0.7 matches the boostThreshold used by jaroWinkler above — the prefix boost
// only kicks in past that, so it's a natural "plausibly the same word" cutoff.
const suggestionThreshold = 0.7

// suggestCommand takes a list of commands and a provided string to suggest a
// command name
// command name. Returns an empty string when no command is sufficiently
// similar; the upstream urfave/cli error formatter omits the suggestion clause
// in that case.
func suggestCommand(commands []*cli.Command, provided string) string {
distance := 0.0
if provided == "" {
return ""
}
distance := suggestionThreshold
var lineage []*cli.Command
for _, command := range commands {
for _, name := range command.Names() {
Expand All @@ -112,6 +124,9 @@ func suggestCommand(commands []*cli.Command, provided string) string {
}
}
}
if lineage == nil {
return ""
}

var parts []string
for _, command := range lineage {
Expand Down
70 changes: 70 additions & 0 deletions pkg/cmd/suggest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package cmd

import (
"testing"

"github.com/urfave/cli/v3"
)

func TestSuggestCommand(t *testing.T) {
commands := []*cli.Command{
{Name: "create"},
{Name: "retrieve"},
{Name: "list"},
{Name: "delete"},
{Name: "chat:completions"},
{Name: "completions"},
}

tests := []struct {
name string
provided string
want string
}{
{
name: "close typo suggests the corrected command",
provided: "creat",
want: "Did you mean 'create'?",
},
{
name: "near-exact suggests the corrected command",
provided: "chat:completion",
want: "Did you mean 'chat:completions'?",
},
{
name: "exact match still suggests itself",
provided: "create",
want: "Did you mean 'create'?",
},
{
name: "unrelated input returns no suggestion",
provided: "zzzzz",
want: "",
},
{
name: "low-similarity input returns no suggestion",
provided: "totallybogus",
want: "",
},
{
name: "empty input returns no suggestion",
provided: "",
want: "",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := suggestCommand(commands, tc.provided)
if got != tc.want {
t.Errorf("suggestCommand(%q) = %q, want %q", tc.provided, got, tc.want)
}
})
}
}

func TestSuggestCommandEmptyCommands(t *testing.T) {
if got := suggestCommand(nil, "anything"); got != "" {
t.Errorf("suggestCommand(nil, %q) = %q, want empty string", "anything", got)
}
}