feat: suggest similar flags for an unknown flag

tea issue list --lable bug now fails with "unknown flag: --lable; did
you mean --label?". Suggestions are the command's visible flags that
start with the typed name or are at most two edits away from it.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-11 22:55:27 +02:00
1 parent f03d0fdf54
commit 04d5f2aed0
2 files changed
+57 -1

No files matched your search

+41 -1
View File
@@ -103,7 +103,7 @@ func NewRoot(deps Dependencies) *cobra.Command {
root.SetIn(deps.Streams.In)
root.SetOut(deps.Streams.Out)
root.SetErr(deps.Streams.ErrOut)
root.SetFlagErrorFunc(func(_ *cobra.Command, err error) error { return clierr.Wrap(clierr.Usage, err) })
root.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { return clierr.Wrap(clierr.Usage, suggestFlags(cmd, err)) })
flags := root.PersistentFlags()
flags.StringVar(&a.options.host, "host", "", "Gitea instance `URL`, including any base path")
flags.StringVar(&a.options.user, "user", "", "Account `name` on the selected instance")
@@ -215,6 +215,46 @@ func flagAliases(cmd *cobra.Command, aliases map[string]string) {
})
}
// suggestFlags adds the flags of cmd with a similar name to an unknown flag error, as in
// "unknown flag: --lable; did you mean --label?".
func suggestFlags(cmd *cobra.Command, err error) error {
name, unknown := strings.CutPrefix(err.Error(), "unknown flag: --")
if !unknown || len(name) < 3 {
return err
}
var suggestions []string
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
if !flag.Hidden && (strings.HasPrefix(flag.Name, name) || editDistance(name, flag.Name) <= 2) {
suggestions = append(suggestions, "--"+flag.Name)
}
})
if len(suggestions) == 0 {
return err
}
return fmt.Errorf("%w; did you mean %s?", err, strings.Join(suggestions, " or "))
}
// editDistance counts the single-character insertions, deletions and substitutions that turn a into b.
func editDistance(a, b string) int {
row := make([]int, len(b)+1)
for j := range row {
row[j] = j
}
for i := 1; i <= len(a); i++ {
diagonal := row[0]
row[0] = i
for j := 1; j <= len(b); j++ {
substitution := diagonal
if a[i-1] != b[j-1] {
substitution++
}
diagonal = row[j]
row[j] = min(row[j]+1, row[j-1]+1, substitution)
}
}
return row[len(b)]
}
// checkOutputFlags validates --json and --jq before a command runs. --jq alone selects every field,
// except on commands that filter their raw JSON themselves (annotation tea.rawjson).
func (a *application) checkOutputFlags(cmd *cobra.Command) error {
+16
View File
@@ -196,6 +196,22 @@ func TestRawJSONCommandsKeepJQToThemselves(t *testing.T) {
require.ErrorContains(t, root.Execute(), "raw prints text; --json is not supported")
}
func TestUnknownFlagsSuggestSimilarFlags(t *testing.T) {
requireUsageBeforeRequests(t, "unknown flag: --lable; did you mean --label?", "issue", "list", "--lable", "bug")
requireUsageBeforeRequests(t, "unknown flag: --stat; did you mean --state?", "issue", "list", "--stat", "open")
requireUsageBeforeRequests(t, "unknown flag: --add-label; did you mean --add-labels?", "issue", "edit", "1", "--add-label", "bug")
deps, _, _ := testDeps(t)
err := Execute(context.Background(), deps, []string{"issue", "list", "--zzzzzz"})
require.Equal(t, clierr.Usage, clierr.ExitCode(err))
require.EqualError(t, err, "unknown flag: --zzzzzz")
for _, tc := range []struct {
a, b string
distance int
}{{"kitten", "sitting", 3}, {"", "abc", 3}, {"label", "label", 0}, {"lable", "label", 2}} {
require.Equal(t, tc.distance, editDistance(tc.a, tc.b), tc)
}
}
func TestRootHelpGroupsCommandsAndTopics(t *testing.T) {
deps, out, _ := testDeps(t)
require.NoError(t, Execute(context.Background(), deps, []string{"alias", "set", "co", "pr checkout"}))