From 8af84dadb142f7321ff0ce8690385e99da8ede2f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sun, 31 Aug 2008 15:50:23 +0200 Subject: git wrapper: DWIM mistyped commands This patch introduces a modified Damerau-Levenshtein algorithm into Git's code base, and uses it with the following penalties to show some similar commands when an unknown command was encountered: swap = 0, insertion = 1, substitution = 2, deletion = 4 A typical output would now look like this: $ git sm git: 'sm' is not a git-command. See 'git --help'. Did you mean one of these? am rm The cut-off is at similarity rating 6, which was empirically determined to give sensible results. As a convenience, if there is only one candidate, Git continues under the assumption that the user mistyped it. Example: $ git reabse WARNING: You called a Git program named 'reabse', which does not exist. Continuing under the assumption that you meant 'rebase' [...] Signed-off-by: Johannes Schindelin Signed-off-by: Alex Riesen Signed-off-by: Junio C Hamano --- help.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) (limited to 'help.c') diff --git a/help.c b/help.c index 1afbac0927..b1ebca4091 100644 --- a/help.c +++ b/help.c @@ -1,6 +1,7 @@ #include "cache.h" #include "builtin.h" #include "exec_cmd.h" +#include "levenshtein.h" #include "help.h" /* most GUI terminals set COLUMNS (although some don't export it) */ @@ -37,6 +38,16 @@ void add_cmdname(struct cmdnames *cmds, const char *name, int len) cmds->names[cmds->cnt++] = ent; } +static void clean_cmdnames(struct cmdnames *cmds) +{ + int i; + for (i = 0; i < cmds->cnt; ++i) + free(cmds->names[i]); + free(cmds->names); + cmds->cnt = 0; + cmds->alloc = 0; +} + static int cmdname_compare(const void *a_, const void *b_) { struct cmdname *a = *(struct cmdname **)a_; @@ -257,9 +268,68 @@ int is_in_cmdlist(struct cmdnames *c, const char *s) return 0; } -void help_unknown_cmd(const char *cmd) +static int levenshtein_compare(const void *p1, const void *p2) +{ + const struct cmdname *const *c1 = p1, *const *c2 = p2; + const char *s1 = (*c1)->name, *s2 = (*c2)->name; + int l1 = (*c1)->len; + int l2 = (*c2)->len; + return l1 != l2 ? l1 - l2 : strcmp(s1, s2); +} + +const char *help_unknown_cmd(const char *cmd) { + int i, n, best_similarity = 0; + struct cmdnames main_cmds, other_cmds; + + memset(&main_cmds, 0, sizeof(main_cmds)); + memset(&other_cmds, 0, sizeof(main_cmds)); + + load_command_list("git-", &main_cmds, &other_cmds); + + ALLOC_GROW(main_cmds.names, main_cmds.cnt + other_cmds.cnt, + main_cmds.alloc); + memcpy(main_cmds.names + main_cmds.cnt, other_cmds.names, + other_cmds.cnt * sizeof(other_cmds.names[0])); + main_cmds.cnt += other_cmds.cnt; + free(other_cmds.names); + + /* This reuses cmdname->len for similarity index */ + for (i = 0; i < main_cmds.cnt; ++i) + main_cmds.names[i]->len = + levenshtein(cmd, main_cmds.names[i]->name, 0, 2, 1, 4); + + qsort(main_cmds.names, main_cmds.cnt, + sizeof(*main_cmds.names), levenshtein_compare); + + if (!main_cmds.cnt) + die ("Uh oh. Your system reports no Git commands at all."); + + best_similarity = main_cmds.names[0]->len; + n = 1; + while (n < main_cmds.cnt && best_similarity == main_cmds.names[n]->len) + ++n; + if (n == 1) { + const char *assumed = main_cmds.names[0]->name; + main_cmds.names[0] = NULL; + clean_cmdnames(&main_cmds); + fprintf(stderr, "WARNING: You called a Git program named '%s', " + "which does not exist.\n" + "Continuing under the assumption that you meant '%s'\n", + cmd, assumed); + return assumed; + } + fprintf(stderr, "git: '%s' is not a git-command. See 'git --help'.\n", cmd); + + if (best_similarity < 6) { + fprintf(stderr, "\nDid you mean %s?\n", + n < 2 ? "this": "one of these"); + + for (i = 0; i < n; i++) + fprintf(stderr, "\t%s\n", main_cmds.names[i]->name); + } + exit(1); } -- cgit v1.3 From f0e90716d47b429284702b75425a247c9fc41adb Mon Sep 17 00:00:00 2001 From: Alex Riesen Date: Sun, 31 Aug 2008 15:54:58 +0200 Subject: Add help.autocorrect to enable/disable autocorrecting It is off(0) by default, to avoid scaring people unless they asked to. If set to a non-0 value, wait for that amount of deciseconds before running the corrected command. Suggested by Junio, so he has a chance to hit Ctrl-C. Signed-off-by: Alex Riesen Signed-off-by: Junio C Hamano --- Documentation/config.txt | 9 +++++++++ help.c | 19 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) (limited to 'help.c') diff --git a/Documentation/config.txt b/Documentation/config.txt index af57d94304..8c644ab8f6 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -790,6 +790,15 @@ help.format:: Values 'man', 'info', 'web' and 'html' are supported. 'man' is the default. 'web' and 'html' are the same. +help.autocorrect:: + Automatically correct and execute mistyped commands after + waiting for the given number of deciseconds (0.1 sec). If more + than one command can be deduced from the entered text, nothing + will be executed. If the value of this option is negative, + the corrected command will be executed immediately. If the + value is 0 - the command will be just shown but not executed. + This is the default. + http.proxy:: Override the HTTP proxy, normally configured using the 'http_proxy' environment variable (see linkgit:curl[1]). This can be overridden diff --git a/help.c b/help.c index b1ebca4091..74499bf840 100644 --- a/help.c +++ b/help.c @@ -268,6 +268,16 @@ int is_in_cmdlist(struct cmdnames *c, const char *s) return 0; } +static int autocorrect; + +static int git_unknown_cmd_config(const char *var, const char *value, void *cb) +{ + if (!strcmp(var, "help.autocorrect")) + autocorrect = git_config_int(var,value); + + return git_default_config(var, value, cb); +} + static int levenshtein_compare(const void *p1, const void *p2) { const struct cmdname *const *c1 = p1, *const *c2 = p2; @@ -285,6 +295,8 @@ const char *help_unknown_cmd(const char *cmd) memset(&main_cmds, 0, sizeof(main_cmds)); memset(&other_cmds, 0, sizeof(main_cmds)); + git_config(git_unknown_cmd_config, NULL); + load_command_list("git-", &main_cmds, &other_cmds); ALLOC_GROW(main_cmds.names, main_cmds.cnt + other_cmds.cnt, @@ -309,7 +321,7 @@ const char *help_unknown_cmd(const char *cmd) n = 1; while (n < main_cmds.cnt && best_similarity == main_cmds.names[n]->len) ++n; - if (n == 1) { + if (autocorrect && n == 1) { const char *assumed = main_cmds.names[0]->name; main_cmds.names[0] = NULL; clean_cmdnames(&main_cmds); @@ -317,6 +329,11 @@ const char *help_unknown_cmd(const char *cmd) "which does not exist.\n" "Continuing under the assumption that you meant '%s'\n", cmd, assumed); + if (autocorrect > 0) { + fprintf(stderr, "in %0.1f seconds automatically...\n", + (float)autocorrect/10.0); + poll(NULL, 0, autocorrect * 100); + } return assumed; } -- cgit v1.3