Merge pull request #28903 from poettering/verbs-levenshtein
verbs: give helpful hint when people mistype command verbs on the cmdline of our tools
This commit is contained in:
@@ -1446,3 +1446,67 @@ bool version_is_valid_versionspec(const char *s) {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ssize_t strlevenshtein(const char *x, const char *y) {
|
||||
_cleanup_free_ size_t *t0 = NULL, *t1 = NULL, *t2 = NULL;
|
||||
size_t xl, yl;
|
||||
|
||||
/* This is inspired from the Linux kernel's Levenshtein implementation */
|
||||
|
||||
if (streq_ptr(x, y))
|
||||
return 0;
|
||||
|
||||
xl = strlen_ptr(x);
|
||||
if (xl > SSIZE_MAX)
|
||||
return -E2BIG;
|
||||
|
||||
yl = strlen_ptr(y);
|
||||
if (yl > SSIZE_MAX)
|
||||
return -E2BIG;
|
||||
|
||||
if (isempty(x))
|
||||
return yl;
|
||||
if (isempty(y))
|
||||
return xl;
|
||||
|
||||
t0 = new0(size_t, yl + 1);
|
||||
if (!t0)
|
||||
return -ENOMEM;
|
||||
t1 = new0(size_t, yl + 1);
|
||||
if (!t1)
|
||||
return -ENOMEM;
|
||||
t2 = new0(size_t, yl + 1);
|
||||
if (!t2)
|
||||
return -ENOMEM;
|
||||
|
||||
for (size_t i = 0; i <= yl; i++)
|
||||
t1[i] = i;
|
||||
|
||||
for (size_t i = 0; i < xl; i++) {
|
||||
t2[0] = i + 1;
|
||||
|
||||
for (size_t j = 0; j < yl; j++) {
|
||||
/* Substitution */
|
||||
t2[j+1] = t1[j] + (x[i] != y[j]);
|
||||
|
||||
/* Swap */
|
||||
if (i > 0 && j > 0 && x[i-1] == y[j] && x[i] == y[j-1] && t2[j+1] > t0[j-1] + 1)
|
||||
t2[j+1] = t0[j-1] + 1;
|
||||
|
||||
/* Deletion */
|
||||
if (t2[j+1] > t1[j+1] + 1)
|
||||
t2[j+1] = t1[j+1] + 1;
|
||||
|
||||
/* Insertion */
|
||||
if (t2[j+1] > t2[j] + 1)
|
||||
t2[j+1] = t2[j] + 1;
|
||||
}
|
||||
|
||||
size_t *dummy = t0;
|
||||
t0 = t1;
|
||||
t1 = t2;
|
||||
t2 = dummy;
|
||||
}
|
||||
|
||||
return t1[yl];
|
||||
}
|
||||
|
||||
@@ -284,3 +284,5 @@ char *startswith_strv(const char *string, char **strv);
|
||||
bool version_is_valid(const char *s);
|
||||
|
||||
bool version_is_valid_versionspec(const char *s);
|
||||
|
||||
ssize_t strlevenshtein(const char *x, const char *y);
|
||||
|
||||
+81
-22
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1292,4 +1292,28 @@ TEST(strextendn) {
|
||||
x = mfree(x);
|
||||
}
|
||||
|
||||
TEST(strlevenshtein) {
|
||||
assert_se(strlevenshtein(NULL, NULL) == 0);
|
||||
assert_se(strlevenshtein("", "") == 0);
|
||||
assert_se(strlevenshtein("", NULL) == 0);
|
||||
assert_se(strlevenshtein(NULL, "") == 0);
|
||||
|
||||
assert_se(strlevenshtein("a", "a") == 0);
|
||||
assert_se(strlevenshtein("a", "b") == 1);
|
||||
assert_se(strlevenshtein("b", "a") == 1);
|
||||
assert_se(strlevenshtein("a", "") == 1);
|
||||
assert_se(strlevenshtein("", "a") == 1);
|
||||
|
||||
assert_se(strlevenshtein("xxx", "xxx") == 0);
|
||||
assert_se(strlevenshtein("xxx", "yyy") == 3);
|
||||
assert_se(strlevenshtein("yyy", "xxx") == 3);
|
||||
assert_se(strlevenshtein("xx", "xxx") == 1);
|
||||
assert_se(strlevenshtein("xxx", "xx") == 1);
|
||||
assert_se(strlevenshtein("x", "xxx") == 2);
|
||||
assert_se(strlevenshtein("xxx", "x") == 2);
|
||||
|
||||
assert_se(strlevenshtein("sitting", "kitten") == 3);
|
||||
assert_se(strlevenshtein("sunday", "saturday") == 3);
|
||||
}
|
||||
|
||||
DEFINE_TEST_MAIN(LOG_DEBUG);
|
||||
|
||||
Reference in New Issue
Block a user