mirror of
https://github.com/git-for-windows/git.git
synced 2026-06-23 15:55:21 -05:00
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:
committed by
Junio C Hamano
parent
0df5897d28
commit
b9e6a2d30a
@@ -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
13
help.c
@@ -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);
|
||||
|
||||
102
parse-options.c
102
parse-options.c
@@ -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)
|
||||
|
||||
@@ -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' '
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
Reference in New Issue
Block a user