]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
userdbctl: convert to OPTION and VERB macros
authorZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Fri, 8 May 2026 06:25:10 +0000 (08:25 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Fri, 8 May 2026 13:52:10 +0000 (15:52 +0200)
The situation with --chain is complicated. The old code tried to use "+…"
in getopt_long() to stop option parsing. But it didn't actually work.
This logic was originally added in 8072a7e6a9eaf2de120797dd16c5e0baea606219.
ef9c12b157a50d63e8a8eb710c013d16c2cea319 added an comment about 'optind=0'
which explains why the code doesn't work, but the code wasn't changed.

To wit:
$ userdbctl.old --no-pager --chain ssh-authorized-keys zbyszek -- /bin/echo --asdf
--asdf
$ userdbctl.old --no-pager --chain ssh-authorized-keys zbyszek /bin/echo -- --asdf
--asdf
$ userdbctl.old --no-pager --chain ssh-authorized-keys zbyszek /bin/echo --asdf
userdbctl.old: unrecognized option '--asdf'
(Basically, if "--" is used, it can be anywhere, since getopt_long() doesn't do
anything special after --chain and looks for the next option. There were some
tests of --chain, but they all used the username as the positional argument, so
it wasn't misinterpreted as an option.)

This behaviour is preserved in the conversion.

--help is generally the same except for expected formatting changes.
--json= is moved above between --output= and -j. For some reason it was
further down.

Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
src/userdb/userdbctl.c

index 75b30215b1da314c81cdd981ebfc67313b4e5262..60a6ff051a3b832b627368a6a9ab58e269e4b836 100644 (file)
@@ -1,10 +1,10 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
-#include <getopt.h>
 #include <stdlib.h>
 #include <unistd.h>
 
 #include "alloc-util.h"
+#include "ansi-color.h"
 #include "bitfield.h"
 #include "build.h"
 #include "copy.h"
 #include "format-table.h"
 #include "format-util.h"
 #include "fs-util.h"
+#include "glyph-util.h"
+#include "help-util.h"
 #include "io-util.h"
 #include "json-util.h"
 #include "log.h"
 #include "main-func.h"
 #include "mkdir.h"
+#include "options.h"
 #include "pager.h"
 #include "parse-argument.h"
-#include "pretty-print.h"
 #include "recurse-dir.h"
 #include "socket-util.h"
 #include "string-table.h"
@@ -408,6 +410,8 @@ static int table_add_uid_map(
         return n_added;
 }
 
+VERB(verb_display_user, "user", "[USER…]", VERB_ANY, VERB_ANY, VERB_DEFAULT,
+     "Inspect user");
 static int verb_display_user(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(table_unrefp) Table *table = NULL;
         bool draw_separator = false;
@@ -751,6 +755,8 @@ static int add_unavailable_gid(Table *table, uid_t start, uid_t end) {
         return 2;
 }
 
+VERB(verb_display_group, "group", "[GROUP…]", VERB_ANY, VERB_ANY, 0,
+     "Inspect group");
 static int verb_display_group(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(table_unrefp) Table *table = NULL;
         bool draw_separator = false;
@@ -952,6 +958,10 @@ static int show_membership(const char *user, const char *group, Table *table) {
         return 0;
 }
 
+VERB(verb_display_memberships, "users-in-group", "[GROUP…]", VERB_ANY, VERB_ANY, 0,
+     "Show users that are members of specified groups");
+VERB(verb_display_memberships, "groups-of-user", "[USER…]", VERB_ANY, VERB_ANY, 0,
+     "Show groups the specified users are members of");
 static int verb_display_memberships(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(table_unrefp) Table *table = NULL;
         int ret = 0, r;
@@ -1048,6 +1058,8 @@ static int verb_display_memberships(int argc, char *argv[], uintptr_t _data, voi
         return ret;
 }
 
+VERB(verb_display_services, "services", NULL, VERB_ANY, 1, 0,
+     "Show enabled database services");
 static int verb_display_services(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(table_unrefp) Table *t = NULL;
         _cleanup_closedir_ DIR *d = NULL;
@@ -1111,8 +1123,11 @@ static int verb_display_services(int argc, char *argv[], uintptr_t _data, void *
         return 0;
 }
 
+VERB(verb_ssh_authorized_keys, "ssh-authorized-keys", "USER", 2, VERB_ANY, 0,
+     "Show SSH authorized keys for user");
 static int verb_ssh_authorized_keys(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+        const char *username;
         char **chain_invocation;
         int r;
 
@@ -1121,6 +1136,8 @@ static int verb_ssh_authorized_keys(int argc, char *argv[], uintptr_t _data, voi
         if (arg_from_file)
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--from-file= not supported when showing SSH authorized keys, refusing.");
 
+        username = argv[1];
+
         if (arg_chain) {
                 /* If --chain is specified, the rest of the command line is the chain command */
 
@@ -1147,18 +1164,18 @@ static int verb_ssh_authorized_keys(int argc, char *argv[], uintptr_t _data, voi
                 chain_invocation = NULL;
         }
 
-        r = userdb_by_name(argv[1], /* match= */ NULL, arg_userdb_flags, &ur);
+        r = userdb_by_name(username, /* match= */ NULL, arg_userdb_flags, &ur);
         if (r == -ESRCH)
-                log_error_errno(r, "User %s does not exist.", argv[1]);
+                log_error_errno(r, "User %s does not exist.", username);
         else if (r == -EHOSTDOWN)
                 log_error_errno(r, "Selected user database service is not available for this request.");
         else if (r == -EINVAL)
-                log_error_errno(r, "Failed to find user %s: %m (Invalid user name?)", argv[1]);
+                log_error_errno(r, "Failed to find user %s: %m (Invalid user name?)", username);
         else if (r < 0)
-                log_error_errno(r, "Failed to find user %s: %m", argv[1]);
+                log_error_errno(r, "Failed to find user %s: %m", username);
         else {
                 if (strv_isempty(ur->ssh_authorized_keys))
-                        log_debug("User record for %s has no public SSH keys.", argv[1]);
+                        log_debug("User record for %s has no public SSH keys.", username);
                 else
                         STRV_FOREACH(i, ur->ssh_authorized_keys)
                                 printf("%s\n", *i);
@@ -1494,6 +1511,8 @@ static int load_credential_one(
         return 0;
 }
 
+VERB(verb_load_credentials, "load-credentials", NULL, VERB_ANY, 1, 0,
+     "Write static user/group records from credentials");
 static int verb_load_credentials(int argc, char *argv[], uintptr_t _data, void *userdata) {
         int r;
 
@@ -1530,66 +1549,39 @@ static int verb_load_credentials(int argc, char *argv[], uintptr_t _data, void *
 }
 
 static int help(void) {
-        _cleanup_free_ char *link = NULL;
+        _cleanup_(table_unrefp) Table *verbs = NULL, *options = NULL;
         int r;
 
+        r = verbs_get_help_table(&verbs);
+        if (r < 0)
+                return r;
+
+        r = option_parser_get_help_table(&options);
+        if (r < 0)
+                return r;
+
+        (void) table_sync_column_widths(0, verbs, options);
+
         pager_open(arg_pager_flags);
 
-        r = terminal_urlify_man("userdbctl", "1", &link);
+        help_cmdline("[OPTIONS…] COMMAND …");
+        help_abstract("Show user and group information.");
+
+        help_section("Commands");
+        r = table_print_or_warn(verbs);
         if (r < 0)
-                return log_oom();
+                return r;
 
-        printf("%s [OPTIONS...] COMMAND ...\n\n"
-               "%sShow user and group information.%s\n"
-               "\nCommands:\n"
-               "  user [USER…]               Inspect user\n"
-               "  group [GROUP…]             Inspect group\n"
-               "  users-in-group [GROUP…]    Show users that are members of specified groups\n"
-               "  groups-of-user [USER…]     Show groups the specified users are members of\n"
-               "  services                   Show enabled database services\n"
-               "  ssh-authorized-keys USER   Show SSH authorized keys for user\n"
-               "  load-credentials           Write static user/group records from credentials\n"
-               "\nOptions:\n"
-               "  -h --help                  Show this help\n"
-               "     --version               Show package version\n"
-               "     --no-pager              Do not pipe output into a pager\n"
-               "     --no-legend             Do not show the headers and footers\n"
-               "     --output=MODE           Select output mode (classic, friendly, table, json)\n"
-               "  -j                         Equivalent to --output=json\n"
-               "  -s --service=SERVICE[:SERVICE…]\n"
-               "                             Query the specified service\n"
-               "     --with-nss=BOOL         Control whether to include glibc NSS data\n"
-               "  -N                         Do not synthesize or include glibc NSS data\n"
-               "                             (Same as --synthesize=no --with-nss=no)\n"
-               "     --synthesize=BOOL       Synthesize root/nobody user\n"
-               "     --with-dropin=BOOL      Control whether to include drop-in records\n"
-               "     --with-varlink=BOOL     Control whether to talk to services at all\n"
-               "     --multiplexer=BOOL      Control whether to use the multiplexer\n"
-               "     --json=pretty|short     JSON output mode\n"
-               "     --chain                 Chain another command\n"
-               "     --uid-min=ID            Filter by minimum UID/GID (default 0)\n"
-               "     --uid-max=ID            Filter by maximum UID/GID (default 4294967294)\n"
-               "     --uuid=UUID             Filter by UUID\n"
-               "  -z --fuzzy                 Do a fuzzy name search\n"
-               "     --disposition=VALUE     Filter by disposition\n"
-               "  -I                         Equivalent to --disposition=intrinsic\n"
-               "  -S                         Equivalent to --disposition=system\n"
-               "  -R                         Equivalent to --disposition=regular\n"
-               "     --boundaries=BOOL       Show/hide UID/GID range boundaries in output\n"
-               "  -B                         Equivalent to --boundaries=no\n"
-               "  -F --from-file=PATH        Read JSON record from file\n"
-               "\nSee the %s for details.\n",
-               program_invocation_short_name,
-               ansi_highlight(),
-               ansi_normal(),
-               link);
+        help_section("Options");
+        r = table_print_or_warn(options);
+        if (r < 0)
+                return r;
 
+        help_man_page_reference("userdbctl", "1");
         return 0;
 }
 
-static int verb_help(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        return help();
-}
+VERB_COMMON_HELP_HIDDEN(help);
 
 static int parse_from_file(const char *arg, sd_json_variant **ret) {
         sd_json_variant *v = NULL;
@@ -1615,56 +1607,13 @@ static int parse_from_file(const char *arg, sd_json_variant **ret) {
         return 0;
 }
 
-static int parse_argv(int argc, char *argv[]) {
-
-        enum {
-                ARG_VERSION = 0x100,
-                ARG_NO_PAGER,
-                ARG_NO_LEGEND,
-                ARG_OUTPUT,
-                ARG_WITH_NSS,
-                ARG_WITH_DROPIN,
-                ARG_WITH_VARLINK,
-                ARG_SYNTHESIZE,
-                ARG_MULTIPLEXER,
-                ARG_JSON,
-                ARG_CHAIN,
-                ARG_UID_MIN,
-                ARG_UID_MAX,
-                ARG_UUID,
-                ARG_DISPOSITION,
-                ARG_BOUNDARIES,
-        };
-
-        static const struct option options[] = {
-                { "help",         no_argument,       NULL, 'h'              },
-                { "version",      no_argument,       NULL, ARG_VERSION      },
-                { "no-pager",     no_argument,       NULL, ARG_NO_PAGER     },
-                { "no-legend",    no_argument,       NULL, ARG_NO_LEGEND    },
-                { "output",       required_argument, NULL, ARG_OUTPUT       },
-                { "service",      required_argument, NULL, 's'              },
-                { "with-nss",     required_argument, NULL, ARG_WITH_NSS     },
-                { "with-dropin",  required_argument, NULL, ARG_WITH_DROPIN  },
-                { "with-varlink", required_argument, NULL, ARG_WITH_VARLINK },
-                { "synthesize",   required_argument, NULL, ARG_SYNTHESIZE   },
-                { "multiplexer",  required_argument, NULL, ARG_MULTIPLEXER  },
-                { "json",         required_argument, NULL, ARG_JSON         },
-                { "chain",        no_argument,       NULL, ARG_CHAIN        },
-                { "uid-min",      required_argument, NULL, ARG_UID_MIN      },
-                { "uid-max",      required_argument, NULL, ARG_UID_MAX      },
-                { "uuid",         required_argument, NULL, ARG_UUID         },
-                { "fuzzy",        no_argument,       NULL, 'z'              },
-                { "disposition",  required_argument, NULL, ARG_DISPOSITION  },
-                { "boundaries",   required_argument, NULL, ARG_BOUNDARIES   },
-                { "from-file",    required_argument, NULL, 'F'              },
-                {}
-        };
-
+static int parse_argv(int argc, char *argv[], char ***remaining_args) {
         const char *e;
         int r;
 
         assert(argc >= 0);
         assert(argv);
+        assert(remaining_args);
 
         /* We are going to update this environment variable with our own, hence let's first read what is already set */
         e = getenv("SYSTEMD_ONLY_USERDB");
@@ -1679,122 +1628,137 @@ static int parse_argv(int argc, char *argv[]) {
                 arg_services = l;
         }
 
-        /* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long()
-         * that checks for GNU extensions in optstring ('-' or '+' at the beginning). */
-        optind = 0;
-
-        for (;;) {
-                int c;
-
-                c = getopt_long(argc, argv,
-                                arg_chain ? "+hjs:NISRzBF:" : "hjs:NISRzBF:", /* When --chain was used disable parsing of further switches */
-                                options, NULL);
-                if (c < 0)
-                        break;
+        OptionParser opts = { argc, argv };
 
+        FOREACH_OPTION_OR_RETURN(c, &opts)
                 switch (c) {
 
-                case 'h':
+                OPTION_COMMON_HELP:
                         return help();
 
-                case ARG_VERSION:
+                OPTION_COMMON_VERSION:
                         return version();
 
-                case ARG_NO_PAGER:
+                OPTION_COMMON_NO_PAGER:
                         arg_pager_flags |= PAGER_DISABLE;
                         break;
 
-                case ARG_NO_LEGEND:
+                OPTION_COMMON_NO_LEGEND:
                         arg_legend = false;
                         break;
 
-                case ARG_OUTPUT:
-                        if (streq(optarg, "help"))
+                OPTION_LONG("output", "MODE",
+                            "Select output mode (classic, friendly, table, json)"):
+                        if (streq(opts.arg, "help"))
                                 return DUMP_STRING_TABLE(output, Output, _OUTPUT_MAX);
 
-                        arg_output = output_from_string(optarg);
+                        arg_output = output_from_string(opts.arg);
                         if (arg_output < 0)
-                                return log_error_errno(arg_output, "Invalid --output= mode: %s", optarg);
+                                return log_error_errno(arg_output, "Invalid --output= mode: %s", opts.arg);
 
                         arg_json_format_flags = arg_output == OUTPUT_JSON ? SD_JSON_FORMAT_PRETTY|SD_JSON_FORMAT_COLOR_AUTO : SD_JSON_FORMAT_OFF;
                         break;
 
-                case ARG_JSON:
-                        r = parse_json_argument(optarg, &arg_json_format_flags);
+                OPTION_COMMON_JSON:
+                        r = parse_json_argument(opts.arg, &arg_json_format_flags);
                         if (r <= 0)
                                 return r;
 
                         arg_output = sd_json_format_enabled(arg_json_format_flags) ? OUTPUT_JSON : _OUTPUT_INVALID;
                         break;
 
-                case 'j':
+                OPTION_SHORT('j', NULL, "Equivalent to --output=json"):
                         arg_json_format_flags = SD_JSON_FORMAT_PRETTY|SD_JSON_FORMAT_COLOR_AUTO;
                         arg_output = OUTPUT_JSON;
                         break;
 
-                case 's':
-                        if (isempty(optarg))
+                OPTION('s', "service", "SERVICE[:SERVICE…]", "Query the specified service"):
+                        if (isempty(opts.arg))
                                 arg_services = strv_free(arg_services);
                         else {
-                                r = strv_split_and_extend(&arg_services, optarg, ":", /* filter_duplicates= */ true);
+                                r = strv_split_and_extend(&arg_services, opts.arg, ":", /* filter_duplicates= */ true);
                                 if (r < 0)
                                         return log_error_errno(r, "Failed to parse -s/--service= argument: %m");
                         }
 
                         break;
 
-                case 'N':
+                OPTION_LONG("with-nss", "BOOL", "Control whether to include glibc NSS data"):
+                        r = parse_boolean_argument("--with-nss=", opts.arg, NULL);
+                        if (r < 0)
+                                return r;
+
+                        SET_FLAG(arg_userdb_flags, USERDB_EXCLUDE_NSS, !r);
+                        break;
+
+                OPTION_SHORT('N', NULL,
+                             "Do not synthesize or include glibc NSS data "
+                             "(Same as --synthesize=no --with-nss=no)"):
                         arg_userdb_flags |= USERDB_EXCLUDE_NSS|USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN;
                         break;
 
-                case ARG_WITH_NSS:
-                        r = parse_boolean_argument("--with-nss=", optarg, NULL);
+                OPTION_LONG("synthesize", "BOOL", "Synthesize root/nobody user"):
+                        r = parse_boolean_argument("--synthesize=", opts.arg, NULL);
                         if (r < 0)
                                 return r;
 
-                        SET_FLAG(arg_userdb_flags, USERDB_EXCLUDE_NSS, !r);
+                        SET_FLAG(arg_userdb_flags, USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, !r);
                         break;
 
-                case ARG_WITH_DROPIN:
-                        r = parse_boolean_argument("--with-dropin=", optarg, NULL);
+                OPTION_LONG("with-dropin", "BOOL", "Control whether to include drop-in records"):
+                        r = parse_boolean_argument("--with-dropin=", opts.arg, NULL);
                         if (r < 0)
                                 return r;
 
                         SET_FLAG(arg_userdb_flags, USERDB_EXCLUDE_DROPIN, !r);
                         break;
 
-                case ARG_WITH_VARLINK:
-                        r = parse_boolean_argument("--with-varlink=", optarg, NULL);
+                OPTION_LONG("with-varlink", "BOOL", "Control whether to talk to services at all"):
+                        r = parse_boolean_argument("--with-varlink=", opts.arg, NULL);
                         if (r < 0)
                                 return r;
 
                         SET_FLAG(arg_userdb_flags, USERDB_EXCLUDE_VARLINK, !r);
                         break;
 
-                case ARG_SYNTHESIZE:
-                        r = parse_boolean_argument("--synthesize=", optarg, NULL);
+                OPTION_LONG("multiplexer", "BOOL", "Control whether to use the multiplexer"):
+                        r = parse_boolean_argument("--multiplexer=", opts.arg, NULL);
                         if (r < 0)
                                 return r;
 
-                        SET_FLAG(arg_userdb_flags, USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, !r);
+                        SET_FLAG(arg_userdb_flags, USERDB_AVOID_MULTIPLEXER, !r);
                         break;
 
-                case ARG_MULTIPLEXER:
-                        r = parse_boolean_argument("--multiplexer=", optarg, NULL);
+                OPTION_LONG("chain", NULL, "Chain another command"):
+                        arg_chain = true;
+                        break;
+
+                OPTION_LONG("uid-min", "ID", "Filter by minimum UID/GID (default 0)"):
+                        r = parse_uid(opts.arg, &arg_uid_min);
                         if (r < 0)
-                                return r;
+                                return log_error_errno(r, "Failed to parse --uid-min= value: %s", opts.arg);
+                        break;
 
-                        SET_FLAG(arg_userdb_flags, USERDB_AVOID_MULTIPLEXER, !r);
+                OPTION_LONG("uid-max", "ID", "Filter by maximum UID/GID (default 4294967294)"):
+                        r = parse_uid(opts.arg, &arg_uid_max);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to parse --uid-max= value: %s", opts.arg);
                         break;
 
-                case ARG_CHAIN:
-                        arg_chain = true;
+                OPTION_LONG("uuid", "UUID", "Filter by UUID"):
+                        r = sd_id128_from_string(opts.arg, &arg_uuid);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to parse --uuid= value: %s", opts.arg);
+                        break;
+
+                OPTION('z', "fuzzy", NULL, "Do a fuzzy name search"):
+                        arg_fuzzy = true;
                         break;
 
-                case ARG_DISPOSITION: {
-                        UserDisposition d = user_disposition_from_string(optarg);
+                OPTION_LONG("disposition", "VALUE", "Filter by disposition"): {
+                        UserDisposition d = user_disposition_from_string(opts.arg);
                         if (d < 0)
-                                return log_error_errno(d, "Unknown user disposition: %s", optarg);
+                                return log_error_errno(d, "Unknown user disposition: %s", opts.arg);
 
                         if (arg_disposition_mask == UINT64_MAX)
                                 arg_disposition_mask = 0;
@@ -1803,80 +1767,54 @@ static int parse_argv(int argc, char *argv[]) {
                         break;
                 }
 
-                case 'I':
+                OPTION_SHORT('I', NULL, "Equivalent to --disposition=intrinsic"):
                         if (arg_disposition_mask == UINT64_MAX)
                                 arg_disposition_mask = 0;
 
                         arg_disposition_mask |= UINT64_C(1) << USER_INTRINSIC;
                         break;
 
-                case 'S':
+                OPTION_SHORT('S', NULL, "Equivalent to --disposition=system"):
                         if (arg_disposition_mask == UINT64_MAX)
                                 arg_disposition_mask = 0;
 
                         arg_disposition_mask |= UINT64_C(1) << USER_SYSTEM;
                         break;
 
-                case 'R':
+                OPTION_SHORT('R', NULL, "Equivalent to --disposition=regular"):
                         if (arg_disposition_mask == UINT64_MAX)
                                 arg_disposition_mask = 0;
 
                         arg_disposition_mask |= UINT64_C(1) << USER_REGULAR;
                         break;
 
-                case ARG_UID_MIN:
-                        r = parse_uid(optarg, &arg_uid_min);
-                        if (r < 0)
-                                return log_error_errno(r, "Failed to parse --uid-min= value: %s", optarg);
-                        break;
-
-                case ARG_UID_MAX:
-                        r = parse_uid(optarg, &arg_uid_max);
-                        if (r < 0)
-                                return log_error_errno(r, "Failed to parse --uid-max= value: %s", optarg);
-                        break;
-
-                case ARG_UUID:
-                        r = sd_id128_from_string(optarg, &arg_uuid);
-                        if (r < 0)
-                                return log_error_errno(r, "Failed to parse --uuid= value: %s", optarg);
-                        break;
-
-                case 'z':
-                        arg_fuzzy = true;
-                        break;
-
-                case ARG_BOUNDARIES:
-                        r = parse_boolean_argument("boundaries", optarg, &arg_boundaries);
+                OPTION_LONG("boundaries", "BOOL",
+                            "Show/hide UID/GID range boundaries in output"):
+                        r = parse_boolean_argument("boundaries", opts.arg, &arg_boundaries);
                         if (r < 0)
                                 return r;
                         break;
 
-                case 'B':
+                OPTION_SHORT('B', NULL, "Equivalent to --boundaries=no"):
                         arg_boundaries = false;
                         break;
 
-                case 'F': {
+                OPTION('F', "from-file", "PATH", "Read JSON record from file"): {
                         sd_json_variant *v = NULL;  /* initialization to appease gcc-14 */
 
-                        r = parse_from_file(optarg, &v);
+                        r = parse_from_file(opts.arg, &v);
                         if (r < 0)
                                 return r;
 
                         json_variant_unref_and_replace(arg_from_file, v);
                         break;
                 }
-
-                case '?':
-                        return -EINVAL;
-
-                default:
-                        assert_not_reached();
                 }
-        }
 
         if (arg_uid_min > arg_uid_max)
-                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Minimum UID/GID " UID_FMT " is above maximum UID/GID " UID_FMT ", refusing.", arg_uid_min, arg_uid_max);
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "Minimum UID/GID " UID_FMT " is above maximum UID/GID " UID_FMT ", refusing.",
+                                       arg_uid_min, arg_uid_max);
 
         /* If not mask was specified, use the all bits on mask */
         if (arg_disposition_mask == UINT64_MAX)
@@ -1885,27 +1823,17 @@ static int parse_argv(int argc, char *argv[]) {
         if (arg_from_file)
                 arg_boundaries = false;
 
+        *remaining_args = option_parser_get_args(&opts);
         return 1;
 }
 
 static int run(int argc, char *argv[]) {
-        static const Verb verbs[] = {
-                { "help",                VERB_ANY, VERB_ANY, 0,            verb_help                },
-                { "user",                VERB_ANY, VERB_ANY, VERB_DEFAULT, verb_display_user        },
-                { "group",               VERB_ANY, VERB_ANY, 0,            verb_display_group       },
-                { "users-in-group",      VERB_ANY, VERB_ANY, 0,            verb_display_memberships },
-                { "groups-of-user",      VERB_ANY, VERB_ANY, 0,            verb_display_memberships },
-                { "services",            VERB_ANY, 1,        0,            verb_display_services    },
-                { "ssh-authorized-keys", 2,        VERB_ANY, 0,            verb_ssh_authorized_keys },
-                { "load-credentials",    VERB_ANY, 1,        0,            verb_load_credentials    },
-                {}
-        };
-
+        char **args = NULL;
         int r;
 
         log_setup();
 
-        r = parse_argv(argc, argv);
+        r = parse_argv(argc, argv, &args);
         if (r <= 0)
                 return r;
 
@@ -1923,7 +1851,7 @@ static int run(int argc, char *argv[]) {
         } else
                 assert_se(unsetenv("SYSTEMD_ONLY_USERDB") == 0);
 
-        return dispatch_verb(argc, argv, verbs, NULL);
+        return dispatch_verb_with_args(args, NULL);
 }
 
 DEFINE_MAIN_FUNCTION(run);