]> git.ipfire.org Git - thirdparty/systemd.git/blobdiff - src/shared/verbs.c
verbs: make a helpful suggestion when user types unrecognized verb
[thirdparty/systemd.git] / src / shared / verbs.c
index 2d19172c6f263981aabd85803760669d36f28ec9..a01095288cc5a6d611412ba1bf15c6f9f0cb3064 100644 (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);
         }
-}