From: Jiamu Sun <39@barroit.sh> Date: Sun, 08 Mar 2026 12:17:23 GMT Subject: [PATCH 3/5] parseopt: autocorrect mistyped subcommands Message-ID: In-Reply-To: Enable autocorrection for mandatory subcommands parsed through parse-options. AUTOCORR_HINTONLY is equivalent to AUTOCORR_NEVER here, because builtins have a limited number of subcommands. They won't become too large. Displaying all subcommands via usage_with_options() is already good enough. This keeps the autocorrection handling simple, too. Also, use a dynamic threshold for similar_enough(), which can yield more accurate typo correction results. Although subcommands are often short, they can still vary across builtins. And a fixed threshold won't do better on both short and long subcommands at the same time. Signed-off-by: Jiamu Sun <39@barroit.sh> --- parse-options.c | 76 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/parse-options.c b/parse-options.c index 6bb0c5697099..cbbb04b9997a 100644 --- a/parse-options.c +++ b/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; @@ -621,6 +623,64 @@ 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 similar_enough(const char *cmd, unsigned int dist) +{ + size_t len = strlen(cmd); + unsigned int threshold = len < 3 ? 1 : len < 6 ? 3 : 6; + + return dist < threshold; +} + +static const char *autocorrect_subcmd(const char *cmd, + struct string_list *cmds) +{ + struct autocorr autocorr = { 0 }; + + autocorr_resolve(&autocorr); + + if (autocorr.mode == AUTOCORR_NEVER || + autocorr.mode == AUTOCORR_HINTONLY) + return NULL; + + unsigned int min = -1; + unsigned int ties = 0; + struct string_list_item *cand; + struct string_list_item *best = NULL; + + for_each_string_list_item(cand, cmds) { + unsigned int dist = levenshtein(cmd, cand->string, 0, 2, 1, 3); + + if (dist < min) { + min = dist; + best = cand; + ties = 0; + + } else if (dist == min) { + ties++; + } + } + + if (ties == 0 && similar_enough(cmd, min)) { + fprintf_ln(stderr, + _("WARNING: You called a subcommand named '%s', which does not exist."), + cmd); + + autocorr_prompt_or_delay(&autocorr, best->string); + return best->string; + } + + return NULL; +} + static enum parse_opt_result handle_subcommand(struct parse_opt_ctx_t *ctx, const char *arg, const struct option *options, @@ -640,8 +700,20 @@ 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); + struct string_list cmds = STRING_LIST_INIT_NODUP; + + find_subcommands(&cmds, options); + + const char *cmd = autocorrect_subcmd(arg, &cmds); + + if (!cmd) { + error(_("unknown subcommand: `%s'"), arg); + usage_with_options(usagestr, options); + } + + string_list_clear(&cmds, 0); + parse_subcommand(cmd, options); + return PARSE_OPT_SUBCOMMAND; } static void check_typos(const char *arg, const struct option *options) -- 2.53.0