parseopt: autocorrect mistyped subcommands

Try to autocorrect the mistyped mandatory subcommand before showing an
error and exiting. Subcommands parsed with PARSE_OPT_SUBCOMMAND_OPTIONAL
are skipped.

The subcommand autocorrection behaves the same as the command
autocorrection.

Signed-off-by: Jiamu Sun <39@barroit.sh>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Jiamu Sun
2026-04-23 10:37:57 +09:00
committed by Junio C Hamano
parent 0df5897d28
commit b9e6a2d30a
5 changed files with 112 additions and 16 deletions

View File

@@ -1,6 +1,10 @@
#ifndef AUTOCORRECT_H
#define AUTOCORRECT_H
/* An empirically derived magic number */
#define AUTOCORRECT_SIMILARITY_FLOOR 7
#define AUTOCORRECT_SIMILAR_ENOUGH(x) ((x) < AUTOCORRECT_SIMILARITY_FLOOR)
enum autocorrect_mode {
AUTOCORRECT_HINT,
AUTOCORRECT_NEVER,

13
help.c
View File

@@ -580,10 +580,6 @@ static void add_cmd_list(struct cmdnames *cmds, struct cmdnames *old)
old->cnt = 0;
}
/* An empirically derived magic number */
#define SIMILARITY_FLOOR 7
#define SIMILAR_ENOUGH(x) ((x) < SIMILARITY_FLOOR)
static const char bad_interpreter_advice[] =
N_("'%s' appears to be a git command, but we were not\n"
"able to execute it. Maybe git-%s is broken?");
@@ -659,7 +655,7 @@ char *help_unknown_cmd(const char *cmd)
if (main_cmds.cnt <= n) {
/* prefix matches with everything? that is too ambiguous */
best_similarity = SIMILARITY_FLOOR + 1;
best_similarity = AUTOCORRECT_SIMILARITY_FLOOR + 1;
} else {
/* count all the most similar ones */
for (best_similarity = main_cmds.names[n++]->len;
@@ -670,7 +666,7 @@ char *help_unknown_cmd(const char *cmd)
}
if (autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&
SIMILAR_ENOUGH(best_similarity)) {
AUTOCORRECT_SIMILAR_ENOUGH(best_similarity)) {
char *assumed = xstrdup(main_cmds.names[0]->name);
fprintf_ln(stderr,
@@ -687,11 +683,10 @@ char *help_unknown_cmd(const char *cmd)
fprintf_ln(stderr, _("git: '%s' is not a git command. See 'git --help'."), cmd);
if (SIMILAR_ENOUGH(best_similarity)) {
if (AUTOCORRECT_SIMILAR_ENOUGH(best_similarity)) {
fprintf_ln(stderr,
Q_("\nThe most similar command is",
"\nThe most similar commands are",
n));
"\nThe most similar commands are", n));
for (i = 0; i < n; i++)
fprintf(stderr, "\t%s\n", main_cmds.names[i]->name);

View File

@@ -6,6 +6,8 @@
#include "strbuf.h"
#include "string-list.h"
#include "utf8.h"
#include "autocorrect.h"
#include "levenshtein.h"
static int disallow_abbreviated_options;
@@ -622,13 +624,98 @@ static int parse_subcommand(const char *arg, const struct option *options)
return -1;
}
static void find_subcommands(struct string_list *list,
const struct option *options)
{
for (; options->type != OPTION_END; options++) {
if (options->type == OPTION_SUBCOMMAND)
string_list_append(list, options->long_name);
}
}
static int levenshtein_compare(const void *p1, const void *p2)
{
const struct string_list_item *i1 = p1, *i2 = p2;
const char *s1 = i1->string, *s2 = i2->string;
int l1 = (intptr_t)i1->util;
int l2 = (intptr_t)i2->util;
return l1 != l2 ? l1 - l2 : strcmp(s1, s2);
}
static const char *autocorrect_subcommand(const char *cmd,
struct string_list *cmds)
{
struct autocorrect autocorrect = { 0 };
unsigned int n = 0, best = 0;
struct string_list_item *cand;
autocorrect_resolve(&autocorrect);
if (autocorrect.mode == AUTOCORRECT_NEVER)
return NULL;
for_each_string_list_item(cand, cmds) {
if (starts_with(cand->string, cmd)) {
cand->util = 0;
} else {
int edit = levenshtein(cmd, cand->string,
0, 2, 1, 3) + 1;
cand->util = (void *)(intptr_t)edit;
}
}
QSORT(cmds->items, cmds->nr, levenshtein_compare);
/* Match help.c:help_unknown_cmd */
for (; n < cmds->nr && !cmds->items[n].util; n++);
if (n == cmds->nr)
/* prefix matches with every subcommands */
best = AUTOCORRECT_SIMILARITY_FLOOR + 1;
else
for (best = (intptr_t)cmds->items[n++].util;
(n < cmds->nr && best == (intptr_t)cmds->items[n].util);
n++);
if (autocorrect.mode != AUTOCORRECT_HINT && n == 1 &&
AUTOCORRECT_SIMILAR_ENOUGH(best)) {
fprintf_ln(stderr,
_("WARNING: You called a subcommand named '%s', which does not exist."),
cmd);
autocorrect_confirm(&autocorrect, cmds->items[0].string);
return cmds->items[0].string;
}
if (AUTOCORRECT_SIMILAR_ENOUGH(best)) {
error(_("'%s' is not a subcommand."), cmd);
fprintf_ln(stderr,
Q_("\nThe most similar subcommand is",
"\nThe most similar subcommands are",
n));
for (unsigned int i = 0; i < n; i++)
fprintf(stderr, "\t%s\n", cmds->items[i].string);
exit(129);
}
return NULL;
}
static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
const char *arg,
const struct option *options,
const char * const usagestr[])
{
int err = parse_subcommand(arg, options);
int err;
const char *assumed;
struct string_list cmds = STRING_LIST_INIT_NODUP;
err = parse_subcommand(arg, options);
if (!err)
return PARSE_OPT_SUBCOMMAND;
@@ -641,8 +728,17 @@ static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx,
if (ctx->flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)
return PARSE_OPT_DONE;
error(_("unknown subcommand: `%s'"), arg);
usage_with_options(usagestr, options);
find_subcommands(&cmds, options);
assumed = autocorrect_subcommand(arg, &cmds);
if (!assumed) {
error(_("unknown subcommand: `%s'"), arg);
usage_with_options(usagestr, options);
}
string_list_clear(&cmds, 0);
parse_subcommand(assumed, options);
return PARSE_OPT_SUBCOMMAND;
}
static void check_typos(const char *arg, const struct option *options)

View File

@@ -632,8 +632,9 @@ test_expect_success 'subcommand - unknown subcommand shows error and usage' '
test_expect_success 'subcommand - subcommands cannot be abbreviated' '
test_expect_code 129 test-tool parse-subcommand cmd subcmd-o 2>err &&
grep "^error: unknown subcommand: \`subcmd-o$SQ$" err &&
grep ^usage: err
grep "^The most similar subcommands are$" err &&
grep "subcmd-one$" err &&
grep "subcmd-two$" err
'
test_expect_success 'subcommand - no negated subcommands' '

View File

@@ -37,8 +37,8 @@ test_systemd_analyze_verify () {
test_expect_success 'help text' '
test_expect_code 129 git maintenance -h >actual &&
test_grep "usage: git maintenance <subcommand>" actual &&
test_expect_code 129 git maintenance barf 2>err &&
test_grep "unknown subcommand: \`barf'\''" err &&
test_expect_code 129 git maintenance abarf 2>err &&
test_grep "unknown subcommand: \`abarf'\''" err &&
test_grep "usage: git maintenance" err &&
test_expect_code 129 git maintenance 2>err &&
test_grep "error: need a subcommand" err &&