mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
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:
2 files changed
+57
-1
No files matched your search
+41
-1
@@ -103,7 +103,7 @@ func NewRoot(deps Dependencies) *cobra.Command {
|
|||||||
root.SetIn(deps.Streams.In)
|
root.SetIn(deps.Streams.In)
|
||||||
root.SetOut(deps.Streams.Out)
|
root.SetOut(deps.Streams.Out)
|
||||||
root.SetErr(deps.Streams.ErrOut)
|
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 := root.PersistentFlags()
|
||||||
flags.StringVar(&a.options.host, "host", "", "Gitea instance `URL`, including any base path")
|
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")
|
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,
|
// 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).
|
// except on commands that filter their raw JSON themselves (annotation tea.rawjson).
|
||||||
func (a *application) checkOutputFlags(cmd *cobra.Command) error {
|
func (a *application) checkOutputFlags(cmd *cobra.Command) error {
|
||||||
|
|||||||
@@ -196,6 +196,22 @@ func TestRawJSONCommandsKeepJQToThemselves(t *testing.T) {
|
|||||||
require.ErrorContains(t, root.Execute(), "raw prints text; --json is not supported")
|
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) {
|
func TestRootHelpGroupsCommandsAndTopics(t *testing.T) {
|
||||||
deps, out, _ := testDeps(t)
|
deps, out, _ := testDeps(t)
|
||||||
require.NoError(t, Execute(context.Background(), deps, []string{"alias", "set", "co", "pr checkout"}))
|
require.NoError(t, Execute(context.Background(), deps, []string{"alias", "set", "co", "pr checkout"}))
|
||||||
|
|||||||
Reference in new issue
Block a user