From: Lennart Poettering Date: Mon, 21 Aug 2023 12:44:29 +0000 (+0200) Subject: verbs: make a helpful suggestion when user types unrecognized verb X-Git-Tag: v255-rc1~677^2 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=ea803614fcc61b6669cbde68b87cd305c62a9c10;p=thirdparty%2Fsystemd.git verbs: make a helpful suggestion when user types unrecognized verb I have been mistyping commands too often myself, and I think the tools could simply be more helpful, by suggesting to me what I probably wanted to write. Copy/Paste FTW, after all! --- diff --git a/src/shared/verbs.c b/src/shared/verbs.c index 2d19172c6f2..a01095288cc 100644 --- a/src/shared/verbs.c +++ b/src/shared/verbs.c @@ -46,15 +46,73 @@ bool running_in_chroot_or_offline(void) { } const Verb* verbs_find_verb(const char *name, const Verb verbs[]) { + assert(verbs); + for (size_t i = 0; verbs[i].dispatch; i++) - if (streq_ptr(name, verbs[i].verb) || - (!name && FLAGS_SET(verbs[i].flags, VERB_DEFAULT))) - return &verbs[i]; + if (name ? streq(name, verbs[i].verb) : FLAGS_SET(verbs[i].flags, VERB_DEFAULT)) + return verbs + i; /* At the end of the list? */ return NULL; } +static const Verb* verbs_find_prefix_verb(const char *name, const Verb verbs[]) { + size_t best_distance = SIZE_MAX; + const Verb *best = NULL; + + assert(verbs); + + if (!name) + return NULL; + + for (size_t i = 0; verbs[i].dispatch; i++) { + const char *e; + size_t l; + + e = startswith(verbs[i].verb, name); + if (!e) + continue; + + l = strlen(e); + if (l < best_distance) { + best_distance = l; + best = verbs + i; + } + } + + return best; +} + +static const Verb* verbs_find_closest_verb(const char *name, const Verb verbs[]) { + ssize_t best_distance = SSIZE_MAX; + const Verb *best = NULL; + + assert(verbs); + + if (!name) + return NULL; + + for (size_t i = 0; verbs[i].dispatch; i++) { + ssize_t distance; + + distance = strlevenshtein(verbs[i].verb, name); + if (distance < 0) { + log_debug_errno(distance, "Failed to determine Levenshtein distance between %s and %s: %m", verbs[i].verb, name); + return NULL; + } + + if (distance > 5) /* If the distance is just too far off, don't make a bad suggestion */ + continue; + + if (distance < best_distance) { + best_distance = distance; + best = verbs + i; + } + } + + return best; +} + int dispatch_verb(int argc, char *argv[], const Verb verbs[], void *userdata) { const Verb *verb; const char *name; @@ -73,12 +131,21 @@ int dispatch_verb(int argc, char *argv[], const Verb verbs[], void *userdata) { verb = verbs_find_verb(name, verbs); if (!verb) { - if (name) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Unknown command verb %s.", name); - else - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Command verb required."); + if (name) { + /* Be helperful to the user, and give a hint what the user might have wanted to + * type. We search with two mechanisms: a simple prefix match and – if that didn't + * yield results –, a Levenshtein word distance based match. */ + verb = verbs_find_prefix_verb(name, verbs); + if (!verb) + verb = verbs_find_closest_verb(name, verbs); + if (verb) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unknown command verb '%s', did you mean '%s'?", name, verb->verb); + + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown command verb '%s'.", name); + } + + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Command verb required."); } if (!name) @@ -86,27 +153,19 @@ int dispatch_verb(int argc, char *argv[], const Verb verbs[], void *userdata) { if (verb->min_args != VERB_ANY && (unsigned) left < verb->min_args) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Too few arguments."); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too few arguments."); if (verb->max_args != VERB_ANY && (unsigned) left > verb->max_args) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Too many arguments."); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too many arguments."); if ((verb->flags & VERB_ONLINE_ONLY) && running_in_chroot_or_offline()) { log_info("Running in chroot, ignoring command '%s'", name ?: verb->verb); return 0; } - if (name) - return verb->dispatch(left, argv, userdata); - else { - char* fake[2] = { - (char*) verb->verb, - NULL - }; + if (!name) + return verb->dispatch(1, STRV_MAKE(verb->verb), userdata); - return verb->dispatch(1, fake, userdata); + return verb->dispatch(left, argv, userdata); } -}