Skip to content

Commit b51ef3c

Browse files
authored
feat: suggest common mistaken subcommands (#334)
This PR adds a way for dbc to suggest commands when the user enters a command that doesn't exist. It adds a hook to our argument parser's error handler to augment the error message with a suggestion. I started with list -> search because I notice that users and/or AI agents like to hallucinate 'dbc list' and I think this will help by at least reducing the number of turns it takes for the agent to learn about dbc search. We might consider a fuzzy suggestion feature (saerch -> search) like other CLI tools use as a follow-on to this.
1 parent ab33d4e commit b51ef3c

File tree

2 files changed

+63
-1
lines changed

2 files changed

+63
-1
lines changed

cmd/dbc/main.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"fmt"
2020
"os"
2121
"slices"
22+
"strings"
2223

2324
tea "charm.land/bubbletea/v2"
2425
"charm.land/lipgloss/v2"
@@ -172,6 +173,36 @@ func formatErr(err error) string {
172173
}
173174
}
174175

176+
var subcommandSuggestions = map[string]string{
177+
"list": "search",
178+
}
179+
180+
func failSubcommandAndSuggest(p *arg.Parser, msg string, subcommand ...string) {
181+
// Extract the invalid command from os.Args by scanning for the first non-flag
182+
// arg
183+
var invalidCmd string
184+
if len(os.Args) > 1 {
185+
for _, arg := range os.Args[1:] {
186+
if !strings.HasPrefix(arg, "-") {
187+
invalidCmd = arg
188+
break
189+
}
190+
}
191+
}
192+
193+
p.WriteUsageForSubcommand(os.Stdout, subcommand...)
194+
fmt.Fprintf(os.Stdout, "error: %s", msg)
195+
196+
// Optionally add suggestion
197+
if invalidCmd != "" {
198+
if suggestion, ok := subcommandSuggestions[invalidCmd]; ok {
199+
fmt.Fprintf(os.Stderr, ". Did you mean: dbc %s?\n", suggestion)
200+
}
201+
}
202+
203+
os.Exit(2)
204+
}
205+
175206
func main() {
176207
var (
177208
args cmds
@@ -195,7 +226,7 @@ func main() {
195226
fmt.Println(dbc.Version)
196227
os.Exit(0)
197228
default:
198-
p.FailSubcommand(err.Error(), p.SubcommandNames()...)
229+
failSubcommandAndSuggest(p, err.Error(), p.SubcommandNames()...)
199230
}
200231
}
201232

cmd/dbc/main_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,34 @@ func TestInstallHelpMentionsVersionConstraints(t *testing.T) {
134134
require.Contains(t, out, `dbc install "mysql>=1,<2"`)
135135
require.Contains(t, out, "https://docs.columnar.tech/dbc/guides/installing/#version-constraints")
136136
}
137+
138+
func TestSubcommandSuggestions(t *testing.T) {
139+
tests := []struct {
140+
name string
141+
invalidCmd string
142+
wantSuggestion string
143+
hasSuggestion bool
144+
}{
145+
{
146+
name: "list suggests search",
147+
invalidCmd: "list",
148+
wantSuggestion: "search",
149+
hasSuggestion: true,
150+
},
151+
{
152+
name: "unknown command has no suggestion",
153+
invalidCmd: "foobar",
154+
hasSuggestion: false,
155+
},
156+
}
157+
158+
for _, tt := range tests {
159+
t.Run(tt.name, func(t *testing.T) {
160+
suggestion, ok := subcommandSuggestions[tt.invalidCmd]
161+
require.Equal(t, tt.hasSuggestion, ok, "expected hasSuggestion=%v for command %q", tt.hasSuggestion, tt.invalidCmd)
162+
if tt.hasSuggestion {
163+
require.Equal(t, tt.wantSuggestion, suggestion, "wrong suggestion for command %q", tt.invalidCmd)
164+
}
165+
})
166+
}
167+
}

0 commit comments

Comments
 (0)