]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
importctl: convert to the new option and verb parsers
authorZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Fri, 10 Apr 2026 16:28:24 +0000 (18:28 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Sat, 11 Apr 2026 11:30:42 +0000 (13:30 +0200)
Co-developed-by: Claude Opus 4.6 <noreply@anthropic.com>
src/import/importctl.c

index cb6f61e89366cb09cb3079968bdc018e4ab9fc86..dceb03a6d6da5053e42f951c677b874f26f517ea 100644 (file)
@@ -1,6 +1,5 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
-#include <getopt.h>
 #include <locale.h>
 #include <unistd.h>
 
@@ -20,6 +19,7 @@
 #include "log.h"
 #include "main-func.h"
 #include "oci-util.h"
+#include "options.h"
 #include "os-util.h"
 #include "pager.h"
 #include "parse-argument.h"
@@ -261,6 +261,211 @@ static int transfer_image_common(sd_bus *bus, sd_bus_message *m) {
         return -r;
 }
 
+VERB(verb_pull_tar, "pull-tar", "URL [NAME]", 2, 3, 0, "Download a TAR container image");
+static int verb_pull_tar(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+        _cleanup_free_ char *l = NULL, *ll = NULL;
+        const char *local, *remote;
+        sd_bus *bus = ASSERT_PTR(userdata);
+        int r;
+
+        r = settle_image_class();
+        if (r < 0)
+                return r;
+
+        remote = argv[1];
+        if (!http_url_is_valid(remote) && !file_url_is_valid(remote))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "URL '%s' is not valid.", remote);
+
+        if (argc >= 3)
+                local = argv[2];
+        else {
+                r = import_url_last_component(remote, &l);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to get final component of URL: %m");
+
+                local = l;
+        }
+
+        local = empty_or_dash_to_null(local);
+
+        if (local) {
+                r = tar_strip_suffixes(local, &ll);
+                if (r < 0)
+                        return log_oom();
+
+                local = ll;
+
+                if (!image_name_is_valid(local))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                               "Local name %s is not a suitable image name.",
+                                               local);
+        }
+
+        if (arg_image_class == IMAGE_MACHINE && (arg_import_flags & ~IMPORT_FORCE) == 0) {
+                r = bus_message_new_method_call(bus, &m, bus_import_mgr, "PullTar");
+                if (r < 0)
+                        return bus_log_create_error(r);
+
+                r = sd_bus_message_append(
+                                m,
+                                "sssb",
+                                remote,
+                                local,
+                                import_verify_to_string(arg_verify),
+                                FLAGS_SET(arg_import_flags, IMPORT_FORCE));
+        } else {
+                r = bus_message_new_method_call(bus, &m, bus_import_mgr, "PullTarEx");
+                if (r < 0)
+                        return bus_log_create_error(r);
+
+                r = sd_bus_message_append(
+                                m,
+                                "sssst",
+                                remote,
+                                local,
+                                image_class_to_string(arg_image_class),
+                                import_verify_to_string(arg_verify),
+                                (uint64_t) arg_import_flags & (IMPORT_FORCE|IMPORT_READ_ONLY|IMPORT_PULL_KEEP_DOWNLOAD));
+        }
+        if (r < 0)
+                return bus_log_create_error(r);
+
+        return transfer_image_common(bus, m);
+}
+
+VERB(verb_pull_raw, "pull-raw", "URL [NAME]", 2, 3, 0, "Download a RAW container or VM image");
+static int verb_pull_raw(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+        _cleanup_free_ char *l = NULL, *ll = NULL;
+        const char *local, *remote;
+        sd_bus *bus = ASSERT_PTR(userdata);
+        int r;
+
+        r = settle_image_class();
+        if (r < 0)
+                return r;
+
+        remote = argv[1];
+        if (!http_url_is_valid(remote) && !file_url_is_valid(remote))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "URL '%s' is not valid.", remote);
+
+        if (argc >= 3)
+                local = argv[2];
+        else {
+                r = import_url_last_component(remote, &l);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to get final component of URL: %m");
+
+                local = l;
+        }
+
+        local = empty_or_dash_to_null(local);
+
+        if (local) {
+                r = raw_strip_suffixes(local, &ll);
+                if (r < 0)
+                        return log_oom();
+
+                local = ll;
+
+                if (!image_name_is_valid(local))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                               "Local name %s is not a suitable image name.",
+                                               local);
+        }
+
+        if (arg_image_class == IMAGE_MACHINE && (arg_import_flags & ~IMPORT_FORCE) == 0) {
+                r = bus_message_new_method_call(bus, &m, bus_import_mgr, "PullRaw");
+                if (r < 0)
+                        return bus_log_create_error(r);
+
+                r = sd_bus_message_append(
+                                m,
+                                "sssb",
+                                remote,
+                                local,
+                                import_verify_to_string(arg_verify),
+                                FLAGS_SET(arg_import_flags, IMPORT_FORCE));
+        } else {
+                r = bus_message_new_method_call(bus, &m, bus_import_mgr, "PullRawEx");
+                if (r < 0)
+                        return bus_log_create_error(r);
+
+                r = sd_bus_message_append(
+                                m,
+                                "sssst",
+                                remote,
+                                local,
+                                image_class_to_string(arg_image_class),
+                                import_verify_to_string(arg_verify),
+                                (uint64_t) arg_import_flags & (IMPORT_FORCE|IMPORT_READ_ONLY|IMPORT_PULL_KEEP_DOWNLOAD));
+        }
+        if (r < 0)
+                return bus_log_create_error(r);
+
+        return transfer_image_common(bus, m);
+}
+
+VERB(verb_pull_oci, "pull-oci", "REF [NAME]", 2, 3, 0, "Download an OCI container image");
+static int verb_pull_oci(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+        _cleanup_free_ char *l = NULL;
+        const char *local, *remote;
+        sd_bus *bus = ASSERT_PTR(userdata);
+        int r;
+
+        r = settle_image_class();
+        if (r < 0)
+                return r;
+
+        remote = argv[1];
+        _cleanup_free_ char *image = NULL;
+        r = oci_ref_parse(remote, /* ret_registry= */ NULL, &image, /* ret_tag= */ NULL);
+        if (r == -EINVAL)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Ref '%s' is not valid.", remote);
+        if (r < 0)
+                return log_error_errno(r, "Failed to determine if ref '%s' is valid.", remote);
+
+        if (argc >= 3)
+                local = argv[2];
+        else {
+                r = path_extract_filename(image, &l);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to get final component of reference: %m");
+
+                local = l;
+        }
+
+        local = empty_or_dash_to_null(local);
+
+        if (local) {
+                if (!image_name_is_valid(local))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                               "Local name %s is not a suitable image name.",
+                                               local);
+        }
+
+        r = bus_message_new_method_call(bus, &m, bus_import_mgr, "PullOci");
+        if (r < 0)
+                return bus_log_create_error(r);
+
+        r = sd_bus_message_append(
+                        m,
+                        "ssst",
+                        remote,
+                        local,
+                        image_class_to_string(arg_image_class),
+                        (uint64_t) arg_import_flags & (IMPORT_FORCE|IMPORT_READ_ONLY));
+        if (r < 0)
+                return bus_log_create_error(r);
+
+        return transfer_image_common(bus, m);
+}
+
+VERB(verb_import_tar, "import-tar", "FILE [NAME]", 2, 3, 0, "Import a local TAR container image");
 static int verb_import_tar(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
         _cleanup_free_ char *ll = NULL, *fn = NULL;
@@ -340,6 +545,7 @@ static int verb_import_tar(int argc, char *argv[], uintptr_t _data, void *userda
         return transfer_image_common(bus, m);
 }
 
+VERB(verb_import_raw, "import-raw", "FILE [NAME]", 2, 3, 0, "Import a local RAW container or VM image");
 static int verb_import_raw(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
         _cleanup_free_ char *ll = NULL, *fn = NULL;
@@ -419,6 +625,7 @@ static int verb_import_raw(int argc, char *argv[], uintptr_t _data, void *userda
         return transfer_image_common(bus, m);
 }
 
+VERB(verb_import_fs, "import-fs", "DIRECTORY [NAME]", 2, 3, 0, "Import a local directory container image");
 static int verb_import_fs(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
         const char *local = NULL, *path = NULL;
@@ -506,6 +713,7 @@ static void determine_compression_from_filename(const char *p) {
                 arg_format = "zstd";
 }
 
+VERB(verb_export_tar, "export-tar", "NAME [FILE]", 2, 3, 0, "Export a TAR container image locally");
 static int verb_export_tar(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
         _cleanup_close_ int fd = -EBADF;
@@ -565,6 +773,7 @@ static int verb_export_tar(int argc, char *argv[], uintptr_t _data, void *userda
         return transfer_image_common(bus, m);
 }
 
+VERB(verb_export_raw, "export-raw", "NAME [FILE]", 2, 3, 0, "Export a RAW container or VM image locally");
 static int verb_export_raw(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
         _cleanup_close_ int fd = -EBADF;
@@ -624,207 +833,7 @@ static int verb_export_raw(int argc, char *argv[], uintptr_t _data, void *userda
         return transfer_image_common(bus, m);
 }
 
-static int verb_pull_tar(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
-        _cleanup_free_ char *l = NULL, *ll = NULL;
-        const char *local, *remote;
-        sd_bus *bus = ASSERT_PTR(userdata);
-        int r;
-
-        r = settle_image_class();
-        if (r < 0)
-                return r;
-
-        remote = argv[1];
-        if (!http_url_is_valid(remote) && !file_url_is_valid(remote))
-                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                       "URL '%s' is not valid.", remote);
-
-        if (argc >= 3)
-                local = argv[2];
-        else {
-                r = import_url_last_component(remote, &l);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to get final component of URL: %m");
-
-                local = l;
-        }
-
-        local = empty_or_dash_to_null(local);
-
-        if (local) {
-                r = tar_strip_suffixes(local, &ll);
-                if (r < 0)
-                        return log_oom();
-
-                local = ll;
-
-                if (!image_name_is_valid(local))
-                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                               "Local name %s is not a suitable image name.",
-                                               local);
-        }
-
-        if (arg_image_class == IMAGE_MACHINE && (arg_import_flags & ~IMPORT_FORCE) == 0) {
-                r = bus_message_new_method_call(bus, &m, bus_import_mgr, "PullTar");
-                if (r < 0)
-                        return bus_log_create_error(r);
-
-                r = sd_bus_message_append(
-                                m,
-                                "sssb",
-                                remote,
-                                local,
-                                import_verify_to_string(arg_verify),
-                                FLAGS_SET(arg_import_flags, IMPORT_FORCE));
-        } else {
-                r = bus_message_new_method_call(bus, &m, bus_import_mgr, "PullTarEx");
-                if (r < 0)
-                        return bus_log_create_error(r);
-
-                r = sd_bus_message_append(
-                                m,
-                                "sssst",
-                                remote,
-                                local,
-                                image_class_to_string(arg_image_class),
-                                import_verify_to_string(arg_verify),
-                                (uint64_t) arg_import_flags & (IMPORT_FORCE|IMPORT_READ_ONLY|IMPORT_PULL_KEEP_DOWNLOAD));
-        }
-        if (r < 0)
-                return bus_log_create_error(r);
-
-        return transfer_image_common(bus, m);
-}
-
-static int verb_pull_raw(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
-        _cleanup_free_ char *l = NULL, *ll = NULL;
-        const char *local, *remote;
-        sd_bus *bus = ASSERT_PTR(userdata);
-        int r;
-
-        r = settle_image_class();
-        if (r < 0)
-                return r;
-
-        remote = argv[1];
-        if (!http_url_is_valid(remote) && !file_url_is_valid(remote))
-                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                       "URL '%s' is not valid.", remote);
-
-        if (argc >= 3)
-                local = argv[2];
-        else {
-                r = import_url_last_component(remote, &l);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to get final component of URL: %m");
-
-                local = l;
-        }
-
-        local = empty_or_dash_to_null(local);
-
-        if (local) {
-                r = raw_strip_suffixes(local, &ll);
-                if (r < 0)
-                        return log_oom();
-
-                local = ll;
-
-                if (!image_name_is_valid(local))
-                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                               "Local name %s is not a suitable image name.",
-                                               local);
-        }
-
-        if (arg_image_class == IMAGE_MACHINE && (arg_import_flags & ~IMPORT_FORCE) == 0) {
-                r = bus_message_new_method_call(bus, &m, bus_import_mgr, "PullRaw");
-                if (r < 0)
-                        return bus_log_create_error(r);
-
-                r = sd_bus_message_append(
-                                m,
-                                "sssb",
-                                remote,
-                                local,
-                                import_verify_to_string(arg_verify),
-                                FLAGS_SET(arg_import_flags, IMPORT_FORCE));
-        } else {
-                r = bus_message_new_method_call(bus, &m, bus_import_mgr, "PullRawEx");
-                if (r < 0)
-                        return bus_log_create_error(r);
-
-                r = sd_bus_message_append(
-                                m,
-                                "sssst",
-                                remote,
-                                local,
-                                image_class_to_string(arg_image_class),
-                                import_verify_to_string(arg_verify),
-                                (uint64_t) arg_import_flags & (IMPORT_FORCE|IMPORT_READ_ONLY|IMPORT_PULL_KEEP_DOWNLOAD));
-        }
-        if (r < 0)
-                return bus_log_create_error(r);
-
-        return transfer_image_common(bus, m);
-}
-
-static int verb_pull_oci(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
-        _cleanup_free_ char *l = NULL;
-        const char *local, *remote;
-        sd_bus *bus = ASSERT_PTR(userdata);
-        int r;
-
-        r = settle_image_class();
-        if (r < 0)
-                return r;
-
-        remote = argv[1];
-        _cleanup_free_ char *image = NULL;
-        r = oci_ref_parse(remote, /* ret_registry= */ NULL, &image, /* ret_tag= */ NULL);
-        if (r == -EINVAL)
-                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Ref '%s' is not valid.", remote);
-        if (r < 0)
-                return log_error_errno(r, "Failed to determine if ref '%s' is valid.", remote);
-
-        if (argc >= 3)
-                local = argv[2];
-        else {
-                r = path_extract_filename(image, &l);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to get final component of reference: %m");
-
-                local = l;
-        }
-
-        local = empty_or_dash_to_null(local);
-
-        if (local) {
-                if (!image_name_is_valid(local))
-                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                               "Local name %s is not a suitable image name.",
-                                               local);
-        }
-
-        r = bus_message_new_method_call(bus, &m, bus_import_mgr, "PullOci");
-        if (r < 0)
-                return bus_log_create_error(r);
-
-        r = sd_bus_message_append(
-                        m,
-                        "ssst",
-                        remote,
-                        local,
-                        image_class_to_string(arg_image_class),
-                        (uint64_t) arg_import_flags & (IMPORT_FORCE|IMPORT_READ_ONLY));
-        if (r < 0)
-                return bus_log_create_error(r);
-
-        return transfer_image_common(bus, m);
-}
-
+VERB(verb_list_transfers, "list-transfers", NULL, VERB_ANY, 1, VERB_DEFAULT, "Show list of transfers in progress");
 static int verb_list_transfers(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
@@ -929,6 +938,7 @@ static int verb_list_transfers(int argc, char *argv[], uintptr_t _data, void *us
         return 0;
 }
 
+VERB(verb_cancel_transfer, "cancel-transfer", "[ID...]", 2, VERB_ANY, 0, "Cancel a transfer");
 static int verb_cancel_transfer(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         sd_bus *bus = ASSERT_PTR(userdata);
@@ -951,6 +961,7 @@ static int verb_cancel_transfer(int argc, char *argv[], uintptr_t _data, void *u
         return 0;
 }
 
+VERB_NOARG(verb_list_images, "list-images", "Show list of installed images");
 static int verb_list_images(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
@@ -1050,6 +1061,7 @@ static int verb_list_images(int argc, char *argv[], uintptr_t _data, void *userd
 
 static int help(void) {
         _cleanup_free_ char *link = NULL;
+        _cleanup_(table_unrefp) Table *options = NULL, *verbs = NULL;
         int r;
 
         pager_open(arg_pager_flags);
@@ -1058,265 +1070,179 @@ static int help(void) {
         if (r < 0)
                 return log_oom();
 
-        printf("%1$s [OPTIONS...] COMMAND ...\n\n"
-               "%5$sDownload, import or export disk images%6$s\n"
-               "\n%3$sCommands:%4$s\n"
-               "  pull-tar URL [NAME]         Download a TAR container image\n"
-               "  pull-raw URL [NAME]         Download a RAW container or VM image\n"
-               "  pull-oci REF [NAME]         Download an OCI container image\n"
-               "  import-tar FILE [NAME]      Import a local TAR container image\n"
-               "  import-raw FILE [NAME]      Import a local RAW container or VM image\n"
-               "  import-fs DIRECTORY [NAME]  Import a local directory container image\n"
-               "  export-tar NAME [FILE]      Export a TAR container image locally\n"
-               "  export-raw NAME [FILE]      Export a RAW container or VM image locally\n"
-               "  list-transfers              Show list of transfers in progress\n"
-               "  cancel-transfer [ID...]     Cancel a transfer\n"
-               "  list-images                 Show list of installed images\n"
-               "\n%3$sOptions:%4$s\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"
-               "     --no-ask-password        Do not ask for system passwords\n"
-               "  -H --host=[USER@]HOST       Operate on remote host\n"
-               "  -M --machine=CONTAINER      Operate on local container\n"
-               "     --system                 Connect to system machine manager\n"
-               "     --user                   Connect to user machine manager\n"
-               "     --read-only              Create read-only image\n"
-               "  -q --quiet                  Suppress output\n"
-               "     --json=pretty|short|off  Generate JSON output\n"
-               "  -j                          Equvilant to --json=pretty on TTY, --json=short\n"
-               "                              otherwise\n"
-               "     --verify=MODE            Verification mode for downloaded images (no,\n"
-               "                               checksum, signature)\n"
-               "     --format=xz|gzip|bzip2|zstd\n"
-               "                              Desired output format for export\n"
-               "     --force                  Install image even if already exists\n"
-               "     --class=TYPE             Install as the specified TYPE\n"
-               "  -m                          Install as --class=machine, machine image\n"
-               "  -P                          Install as --class=portable,\n"
-               "                              portable service image\n"
-               "  -S                          Install as --class=sysext, system extension image\n"
-               "  -C                          Install as --class=confext,\n"
-               "                              configuration extension image\n"
-               "     --keep-download=BOOL     Control whether to keep pristine copy of download\n"
-               "  -N                          Same as --keep-download=no\n"
-               "\nSee the %2$s for details.\n",
+        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);
+
+        printf("%s [OPTIONS...] COMMAND ...\n\n"
+               "%sDownload, import or export disk images%s\n"
+               "\n%sCommands:%s\n",
                program_invocation_short_name,
-               link,
-               ansi_underline(),
-               ansi_normal(),
                ansi_highlight(),
+               ansi_normal(),
+               ansi_underline(),
                ansi_normal());
 
+        r = table_print_or_warn(verbs);
+        if (r < 0)
+                return r;
+
+        printf("\n%sOptions:%s\n",
+               ansi_underline(),
+               ansi_normal());
+
+        r = table_print_or_warn(options);
+        if (r < 0)
+                return r;
+
+        printf("\nSee the %s for details.\n", link);
         return 0;
 }
 
-static int verb_help(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        return help();
-}
+VERB_COMMON_HELP(help);
 
-static int parse_argv(int argc, char *argv[]) {
-
-        enum {
-                ARG_VERSION = 0x100,
-                ARG_NO_PAGER,
-                ARG_NO_LEGEND,
-                ARG_NO_ASK_PASSWORD,
-                ARG_READ_ONLY,
-                ARG_JSON,
-                ARG_VERIFY,
-                ARG_FORCE,
-                ARG_FORMAT,
-                ARG_CLASS,
-                ARG_KEEP_DOWNLOAD,
-                ARG_SYSTEM,
-                ARG_USER,
-        };
-
-        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       },
-                { "no-ask-password", no_argument,       NULL, ARG_NO_ASK_PASSWORD },
-                { "host",            required_argument, NULL, 'H'                 },
-                { "machine",         required_argument, NULL, 'M'                 },
-                { "read-only",       no_argument,       NULL, ARG_READ_ONLY       },
-                { "json",            required_argument, NULL, ARG_JSON            },
-                { "quiet",           no_argument,       NULL, 'q'                 },
-                { "verify",          required_argument, NULL, ARG_VERIFY          },
-                { "force",           no_argument,       NULL, ARG_FORCE           },
-                { "format",          required_argument, NULL, ARG_FORMAT          },
-                { "class",           required_argument, NULL, ARG_CLASS           },
-                { "keep-download",   required_argument, NULL, ARG_KEEP_DOWNLOAD   },
-                { "system",          no_argument,       NULL, ARG_SYSTEM          },
-                { "user",            no_argument,       NULL, ARG_USER            },
-                {}
-        };
-
-        int c, r;
+static int parse_argv(int argc, char *argv[], char ***ret_args) {
+        int r;
 
         assert(argc >= 0);
         assert(argv);
 
-        for (;;) {
-                c = getopt_long(argc, argv, "hH:M:jqmPSCN", options, NULL);
-                if (c < 0)
-                        break;
+        OptionParser state = { argc, argv };
+        const char *arg;
 
+        FOREACH_OPTION(&state, c, &arg, /* on_error= */ return c)
                 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_NO_ASK_PASSWORD:
+                OPTION_COMMON_NO_ASK_PASSWORD:
                         arg_ask_password = false;
                         break;
 
-                case 'H':
+                OPTION_COMMON_HOST:
                         arg_transport = BUS_TRANSPORT_REMOTE;
-                        arg_host = optarg;
+                        arg_host = arg;
                         break;
 
-                case 'M':
-                        r = parse_machine_argument(optarg, &arg_host, &arg_transport);
+                OPTION_COMMON_MACHINE:
+                        r = parse_machine_argument(arg, &arg_host, &arg_transport);
                         if (r < 0)
                                 return r;
                         break;
 
-                case ARG_READ_ONLY:
+                OPTION_LONG("system", NULL, "Connect to system machine manager"):
+                        arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
+                        break;
+
+                OPTION_LONG("user", NULL, "Connect to user machine manager"):
+                        arg_runtime_scope = RUNTIME_SCOPE_USER;
+                        break;
+
+                OPTION_LONG("read-only", NULL, "Create read-only image"):
                         arg_import_flags |= IMPORT_READ_ONLY;
                         arg_import_flags_mask |= IMPORT_READ_ONLY;
                         break;
 
-                case 'q':
+                OPTION('q', "quiet", NULL, "Suppress output"):
                         arg_quiet = true;
                         break;
 
-                case ARG_VERIFY:
-                        if (streq(optarg, "help"))
-                                return DUMP_STRING_TABLE(import_verify, ImportVerify, _IMPORT_VERIFY_MAX);
-
-                        r = import_verify_from_string(optarg);
-                        if (r < 0)
-                                return log_error_errno(r, "Failed to parse --verify= setting: %s", optarg);
-                        arg_verify = r;
+                OPTION_COMMON_JSON:
+                        r = parse_json_argument(arg, &arg_json_format_flags);
+                        if (r <= 0)
+                                return r;
+                        arg_legend = false;
                         break;
 
-                case ARG_FORCE:
-                        arg_import_flags |= IMPORT_FORCE;
-                        arg_import_flags_mask |= IMPORT_FORCE;
+                OPTION_COMMON_LOWERCASE_J:
+                        arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
+                        arg_legend = false;
                         break;
 
-                case ARG_FORMAT:
-                        if (!STR_IN_SET(optarg, "uncompressed", "xz", "gzip", "bzip2", "zstd"))
-                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                                       "Unknown format: %s", optarg);
+                OPTION_LONG("verify", "MODE",
+                            "Verification mode for downloaded images (no, checksum, signature)"):
+                        if (streq(arg, "help"))
+                                return DUMP_STRING_TABLE(import_verify, ImportVerify, _IMPORT_VERIFY_MAX);
 
-                        arg_format = optarg;
+                        r = import_verify_from_string(arg);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to parse --verify= setting: %s", arg);
+                        arg_verify = r;
                         break;
 
-                case ARG_JSON:
-                        r = parse_json_argument(optarg, &arg_json_format_flags);
-                        if (r <= 0)
-                                return r;
-
-                        arg_legend = false;
+                OPTION_LONG("format", "FORMAT",
+                            "Desired output format for export (zstd, xz, gzip, bzip2)"):
+                        if (!STR_IN_SET(arg, "uncompressed", "xz", "gzip", "bzip2", "zstd"))
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                       "Unknown format: %s", arg);
+                        arg_format = arg;
                         break;
 
-                case 'j':
-                        arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
-                        arg_legend = false;
+                OPTION_LONG("force", NULL, "Install image even if already exists"):
+                        arg_import_flags |= IMPORT_FORCE;
+                        arg_import_flags_mask |= IMPORT_FORCE;
                         break;
 
-                case ARG_CLASS:
-                        arg_image_class = image_class_from_string(optarg);
+                OPTION_LONG("class", "TYPE", "Install as the specified TYPE"):
+                        arg_image_class = image_class_from_string(arg);
                         if (arg_image_class < 0)
-                                return log_error_errno(arg_image_class, "Failed to parse --class= parameter: %s", optarg);
+                                return log_error_errno(arg_image_class, "Failed to parse --class= parameter: %s", arg);
                         break;
 
-                case 'm':
+                OPTION_SHORT('m', NULL, "Install as --class=machine, machine image"):
                         arg_image_class = IMAGE_MACHINE;
                         break;
 
-                case 'P':
+                OPTION_SHORT('P', NULL, "Install as --class=portable, portable service image"):
                         arg_image_class = IMAGE_PORTABLE;
                         break;
 
-                case 'S':
+                OPTION_SHORT('S', NULL, "Install as --class=sysext, system extension image"):
                         arg_image_class = IMAGE_SYSEXT;
                         break;
 
-                case 'C':
+                OPTION_SHORT('C', NULL, "Install as --class=confext, configuration extension image"):
                         arg_image_class = IMAGE_CONFEXT;
                         break;
 
-                case ARG_KEEP_DOWNLOAD:
-                        r = parse_boolean(optarg);
+                OPTION_LONG("keep-download", "BOOL",
+                            "Control whether to keep pristine copy of download"):
+                        r = parse_boolean(arg);
                         if (r < 0)
-                                return log_error_errno(r, "Failed to parse --keep-download= value: %s", optarg);
+                                return log_error_errno(r, "Failed to parse --keep-download= value: %s", arg);
 
                         SET_FLAG(arg_import_flags, IMPORT_PULL_KEEP_DOWNLOAD, r);
                         arg_import_flags_mask |= IMPORT_PULL_KEEP_DOWNLOAD;
                         break;
 
-                case 'N':
+                OPTION_SHORT('N', NULL, "Same as --keep-download=no"):
                         arg_import_flags &= ~IMPORT_PULL_KEEP_DOWNLOAD;
                         arg_import_flags_mask |= IMPORT_PULL_KEEP_DOWNLOAD;
                         break;
-
-                case ARG_USER:
-                        arg_runtime_scope = RUNTIME_SCOPE_USER;
-                        break;
-
-                case ARG_SYSTEM:
-                        arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
-                        break;
-
-                case '?':
-                        return -EINVAL;
-
-                default:
-                        assert_not_reached();
                 }
-        }
 
+        *ret_args = option_parser_get_args(&state);
         return 1;
 }
 
-static int importctl_main(int argc, char *argv[], sd_bus *bus) {
-
-        static const Verb verbs[] = {
-                { "help",            VERB_ANY, VERB_ANY, 0,            verb_help            },
-                { "import-tar",      2,        3,        0,            verb_import_tar      },
-                { "import-raw",      2,        3,        0,            verb_import_raw      },
-                { "import-fs",       2,        3,        0,            verb_import_fs       },
-                { "export-tar",      2,        3,        0,            verb_export_tar      },
-                { "export-raw",      2,        3,        0,            verb_export_raw      },
-                { "pull-tar",        2,        3,        0,            verb_pull_tar        },
-                { "pull-oci",        2,        3,        0,            verb_pull_oci        },
-                { "pull-raw",        2,        3,        0,            verb_pull_raw        },
-                { "list-transfers",  VERB_ANY, 1,        VERB_DEFAULT, verb_list_transfers  },
-                { "cancel-transfer", 2,        VERB_ANY, 0,            verb_cancel_transfer },
-                { "list-images",     VERB_ANY, 1,        0,            verb_list_images     },
-                {}
-        };
-
-        return dispatch_verb(argc, argv, verbs, bus);
-}
-
 static int run(int argc, char *argv[]) {
         _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
         int r;
@@ -1324,7 +1250,8 @@ static int run(int argc, char *argv[]) {
         setlocale(LC_ALL, "");
         log_setup();
 
-        r = parse_argv(argc, argv);
+        char **args = NULL;
+        r = parse_argv(argc, argv, &args);
         if (r <= 0)
                 return r;
 
@@ -1334,7 +1261,7 @@ static int run(int argc, char *argv[]) {
 
         (void) sd_bus_set_allow_interactive_authorization(bus, arg_ask_password);
 
-        return importctl_main(argc, argv, bus);
+        return dispatch_verb_with_args(args, bus);
 }
 
 DEFINE_MAIN_FUNCTION(run);