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!
This commit is contained in:
Lennart Poettering
2023-08-21 23:19:49 +02:00
parent 7ef5b0a4d8
commit ea803614fc
+81 -22
View File
@@ -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);
}
}