diff --git a/src/bootctl/bootctl.c b/src/bootctl/bootctl.c index 68478612ed..ef1116ced7 100644 --- a/src/bootctl/bootctl.c +++ b/src/bootctl/bootctl.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include "sd-varlink.h" @@ -23,12 +22,14 @@ #include "efivars.h" #include "escape.h" #include "find-esp.h" +#include "format-table.h" #include "image-policy.h" #include "log.h" #include "loop-util.h" #include "main-func.h" #include "mount-util.h" #include "openssl-util.h" +#include "options.h" #include "pager.h" #include "parse-argument.h" #include "path-util.h" @@ -254,296 +255,270 @@ static int help(void) { if (r < 0) return log_oom(); - printf("%1$s [OPTIONS...] COMMAND ...\n" - "\n%5$sControl EFI firmware boot settings and manage boot loader.%6$s\n" - "\n%3$sGeneric EFI Firmware/Boot Loader Commands:%4$s\n" - " status Show status of installed boot loader and EFI variables\n" - " reboot-to-firmware [BOOL]\n" - " Query or set reboot-to-firmware EFI flag\n" - "\n%3$sBoot Loader Specification Commands:%4$s\n" - " list List boot loader entries\n" - " unlink ID Remove boot loader entry\n" - " cleanup Remove files in ESP not referenced in any boot entry\n" - "\n%3$sBoot Loader Interface Commands:%4$s\n" - " set-default ID Set default boot loader entry\n" - " set-oneshot ID Set default boot loader entry, for next boot only\n" - " set-sysfail ID Set boot loader entry used in case of a system failure\n" - " set-timeout SECONDS Set the menu timeout\n" - " set-timeout-oneshot SECONDS\n" - " Set the menu timeout for the next boot only\n" - "\n%3$ssystemd-boot Commands:%4$s\n" - " install Install systemd-boot to the ESP and EFI variables\n" - " update Update systemd-boot in the ESP and EFI variables\n" - " remove Remove systemd-boot from the ESP and EFI variables\n" - " is-installed Test whether systemd-boot is installed in the ESP\n" - " random-seed Initialize or refresh random seed in ESP and EFI\n" - " variables\n" - "\n%3$sKernel Image Commands:%4$s\n" - " kernel-identify KERNEL-IMAGE\n" - " Identify kernel image type\n" - " kernel-inspect KERNEL-IMAGE\n" - " Prints details about the kernel image\n" - "\n%3$sBlock Device Discovery Commands:%4$s\n" - " -p --print-esp-path Print path to the EFI System Partition mount point\n" - " -x --print-boot-path Print path to the $BOOT partition mount point\n" - " --print-loader-path\n" - " Print path to currently booted boot loader binary\n" - " --print-stub-path Print path to currently booted unified kernel binary\n" - " -R --print-root-device\n" - " Print path to the block device node backing the\n" - " root file system (returns e.g. /dev/nvme0n1p5)\n" - " -RR Print path to the whole disk block device node\n" - " backing the root FS (returns e.g. /dev/nvme0n1)\n" - "\n%3$sOptions:%4$s\n" - " -h --help Show this help\n" - " --version Print version\n" - " --esp-path=PATH Path to the EFI System Partition (ESP)\n" - " --boot-path=PATH Path to the $BOOT partition\n" - " --root=PATH Operate on an alternate filesystem root\n" - " --image=PATH Operate on disk image as filesystem root\n" - " --image-policy=POLICY\n" - " Specify disk image dissection policy\n" - " --install-source=auto|image|host\n" - " Where to pick files when using --root=/--image=\n" - " --variables=yes|no\n" - " Whether to modify EFI variables\n" - " --random-seed=yes|no\n" - " Whether to create random-seed file during install\n" - " --no-pager Do not pipe output into a pager\n" - " --graceful Don't fail when the ESP cannot be found or EFI\n" - " variables cannot be written\n" - " -q --quiet Suppress output\n" - " --make-entry-directory=yes|no|auto\n" - " Create $BOOT/ENTRY-TOKEN/ directory\n" - " --entry-token=machine-id|os-id|os-image-id|auto|literal:…\n" - " Entry token to use for this installation\n" - " --json=pretty|short|off\n" - " Generate JSON output\n" - " --all-architectures\n" - " Install all supported EFI architectures\n" - " --efi-boot-option-description=DESCRIPTION\n" - " Description of the entry in the boot option list\n" - " --efi-boot-option-description-with-device=yes\n" - " Suffix description with disk vendor/model/serial\n" - " --dry-run Dry run (unlink and cleanup)\n" - " --secure-boot-auto-enroll=yes|no\n" - " Set up secure boot auto-enrollment\n" - " --private-key=PATH|URI\n" - " Private key to use when setting up secure boot\n" - " auto-enrollment or an engine or provider specific\n" - " designation if --private-key-source= is used\n" - " --private-key-source=file|provider:PROVIDER|engine:ENGINE\n" - " Specify how to use KEY for --private-key=. Allows\n" - " an OpenSSL engine/provider to be used when setting\n" - " up secure boot auto-enrollment\n" - " --certificate=PATH|URI\n" - " PEM certificate to use when setting up Secure Boot\n" - " auto-enrollment, or a provider specific designation\n" - " if --certificate-source= is used\n" - " --certificate-source=file|provider:PROVIDER\n" - " Specify how to interpret the certificate from\n" - " --certificate=. Allows the certificate to be loaded\n" - " from an OpenSSL provider\n" - "\nSee the %2$s for details.\n", + static const char *const verb_groups[] = { + "Generic EFI Firmware/Boot Loader Commands", + "Boot Loader Specification Commands", + "Boot Loader Interface Commands", + "systemd-boot Commands", + "Kernel Image Commands", + }; + + static const char *const option_groups[] = { + "Block Device Discovery Commands", + "Options", + }; + + _cleanup_(table_unref_many) Table *verb_tables[ELEMENTSOF(verb_groups) + 1] = {}; + _cleanup_(table_unref_many) Table *option_tables[ELEMENTSOF(option_groups) + 1] = {}; + + for (size_t i = 0; i < ELEMENTSOF(verb_groups); i++) { + r = verbs_get_help_table_group(verb_groups[i], &verb_tables[i]); + if (r < 0) + return r; + } + + for (size_t i = 0; i < ELEMENTSOF(option_groups); i++) { + r = option_parser_get_help_table_group(option_groups[i], &option_tables[i]); + if (r < 0) + return r; + } + + (void) table_sync_column_widths(0, + verb_tables[0], verb_tables[1], verb_tables[2], + verb_tables[3], verb_tables[4], + option_tables[0], option_tables[1]); + + printf("%s [OPTIONS...] COMMAND ...\n" + "\n%sControl EFI firmware boot settings and manage boot loader.%s\n", program_invocation_short_name, - link, - ansi_underline(), - ansi_normal(), ansi_highlight(), ansi_normal()); + for (size_t i = 0; i < ELEMENTSOF(verb_groups); i++) { + printf("\n%s%s:%s\n", ansi_underline(), verb_groups[i], ansi_normal()); + + r = table_print_or_warn(verb_tables[i]); + if (r < 0) + return r; + } + + for (size_t i = 0; i < ELEMENTSOF(option_groups); i++) { + printf("\n%s%s:%s\n", ansi_underline(), option_groups[i], ansi_normal()); + + r = table_print_or_warn(option_tables[i]); + 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_ESP_PATH = 0x100, - ARG_BOOT_PATH, - ARG_ROOT, - ARG_IMAGE, - ARG_IMAGE_POLICY, - ARG_INSTALL_SOURCE, - ARG_VERSION, - ARG_VARIABLES, - ARG_NO_VARIABLES, - ARG_RANDOM_SEED, - ARG_NO_PAGER, - ARG_GRACEFUL, - ARG_MAKE_ENTRY_DIRECTORY, - ARG_ENTRY_TOKEN, - ARG_JSON, - ARG_ARCH_ALL, - ARG_EFI_BOOT_OPTION_DESCRIPTION, - ARG_EFI_BOOT_OPTION_DESCRIPTION_WITH_DEVICE, - ARG_DRY_RUN, - ARG_PRINT_LOADER_PATH, - ARG_PRINT_STUB_PATH, - ARG_SECURE_BOOT_AUTO_ENROLL, - ARG_CERTIFICATE, - ARG_CERTIFICATE_SOURCE, - ARG_PRIVATE_KEY, - ARG_PRIVATE_KEY_SOURCE, - }; +VERB_GROUP("Generic EFI Firmware/Boot Loader Commands"); - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "esp-path", required_argument, NULL, ARG_ESP_PATH }, - { "path", required_argument, NULL, ARG_ESP_PATH }, /* Compatibility alias */ - { "boot-path", required_argument, NULL, ARG_BOOT_PATH }, - { "root", required_argument, NULL, ARG_ROOT }, - { "image", required_argument, NULL, ARG_IMAGE }, - { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, - { "install-source", required_argument, NULL, ARG_INSTALL_SOURCE }, - { "print-esp-path", no_argument, NULL, 'p' }, - { "print-path", no_argument, NULL, 'p' }, /* Compatibility alias */ - { "print-boot-path", no_argument, NULL, 'x' }, - { "print-loader-path", no_argument, NULL, ARG_PRINT_LOADER_PATH }, - { "print-stub-path", no_argument, NULL, ARG_PRINT_STUB_PATH }, - { "print-root-device", no_argument, NULL, 'R' }, - { "variables", required_argument, NULL, ARG_VARIABLES }, - { "no-variables", no_argument, NULL, ARG_NO_VARIABLES }, /* Compatibility alias */ - { "random-seed", required_argument, NULL, ARG_RANDOM_SEED }, - { "no-pager", no_argument, NULL, ARG_NO_PAGER }, - { "graceful", no_argument, NULL, ARG_GRACEFUL }, - { "quiet", no_argument, NULL, 'q' }, - { "make-entry-directory", required_argument, NULL, ARG_MAKE_ENTRY_DIRECTORY }, - { "make-machine-id-directory", required_argument, NULL, ARG_MAKE_ENTRY_DIRECTORY }, /* Compatibility alias */ - { "entry-token", required_argument, NULL, ARG_ENTRY_TOKEN }, - { "json", required_argument, NULL, ARG_JSON }, - { "all-architectures", no_argument, NULL, ARG_ARCH_ALL }, - { "efi-boot-option-description", required_argument, NULL, ARG_EFI_BOOT_OPTION_DESCRIPTION }, - { "efi-boot-option-description-with-device", required_argument, NULL, ARG_EFI_BOOT_OPTION_DESCRIPTION_WITH_DEVICE }, - { "dry-run", no_argument, NULL, ARG_DRY_RUN }, - { "secure-boot-auto-enroll", required_argument, NULL, ARG_SECURE_BOOT_AUTO_ENROLL }, - { "certificate", required_argument, NULL, ARG_CERTIFICATE }, - { "certificate-source", required_argument, NULL, ARG_CERTIFICATE_SOURCE }, - { "private-key", required_argument, NULL, ARG_PRIVATE_KEY }, - { "private-key-source", required_argument, NULL, ARG_PRIVATE_KEY_SOURCE }, - {} - }; +VERB_SCOPE(, verb_status, "status", NULL, VERB_ANY, 1, VERB_DEFAULT, + "Show status of installed boot loader and EFI variables"); - int c, r; +VERB_SCOPE(, verb_reboot_to_firmware, "reboot-to-firmware", "[BOOL]", VERB_ANY, 2, 0, + "Query or set reboot-to-firmware EFI flag"); + +VERB_GROUP("Boot Loader Specification Commands"); + +VERB_SCOPE_NOARG(, verb_list, "list", + "List boot loader entries"); + +VERB_SCOPE(, verb_unlink, "unlink", "ID", 2, 2, 0, + "Remove boot loader entry"); + +VERB_SCOPE_NOARG(, verb_cleanup, "cleanup", + "Remove files in ESP not referenced in any boot entry"); + +VERB_GROUP("Boot Loader Interface Commands"); + +VERB_SCOPE(, verb_set_efivar, "set-default", "ID", 2, 2, 0, + "Set default boot loader entry"); + +VERB_SCOPE(, verb_set_efivar, "set-oneshot", "ID", 2, 2, 0, + "Set default boot loader entry, for next boot only"); + +VERB_SCOPE(, verb_set_efivar, "set-sysfail", "ID", 2, 2, 0, + "Set boot loader entry used in case of a system failure"); + +VERB_SCOPE(, verb_set_efivar, "set-timeout", "SECONDS", 2, 2, 0, + "Set the menu timeout"); + +VERB_SCOPE(, verb_set_efivar, "set-timeout-oneshot", "SECONDS", 2, 2, 0, + "Set the menu timeout for the next boot only"); + +VERB_SCOPE(, verb_set_efivar, "set-preferred", "ID", 2, 2, 0, + /* help= */ NULL); + +VERB_GROUP("systemd-boot Commands"); + +VERB_SCOPE(, verb_install, "install", NULL, VERB_ANY, 1, 0, + "Install systemd-boot to the ESP and EFI variables"); + +VERB_SCOPE(, verb_install, "update", NULL, VERB_ANY, 1, 0, + "Update systemd-boot in the ESP and EFI variables"); + +VERB_SCOPE_NOARG(, verb_remove, "remove", + "Remove systemd-boot from the ESP and EFI variables"); + +VERB_SCOPE_NOARG(, verb_is_installed, "is-installed", + "Test whether systemd-boot is installed in the ESP"); + +VERB_SCOPE_NOARG(, verb_random_seed, "random-seed", + "Initialize or refresh random seed in ESP and EFI variables"); + +VERB_GROUP("Kernel Image Commands"); + +VERB_SCOPE(, verb_kernel_identify, "kernel-identify", "KERNEL-IMAGE", 2, 2, 0, + "Identify kernel image type"); + +VERB_SCOPE(, verb_kernel_inspect, "kernel-inspect", "KERNEL-IMAGE", 2, 2, 0, + "Prints details about the kernel image"); + +static int parse_argv(int argc, char *argv[], char ***ret_args) { + int r; assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "hpxRq", options, NULL)) >= 0) + OptionParser state = { argc, argv }; + const char *arg; + + FOREACH_OPTION(&state, c, &arg, /* on_error= */ return c) switch (c) { - case 'h': + OPTION_GROUP("Block Device Discovery Commands"): + break; + + OPTION('p', "print-esp-path", NULL, "Print path to the EFI System Partition mount point"): {} + OPTION_LONG("print-path", NULL, /* help= */ NULL): /* Compatibility alias */ + arg_print_esp_path = true; + break; + + OPTION('x', "print-boot-path", NULL, "Print path to the $BOOT partition mount point"): + arg_print_dollar_boot_path = true; + break; + + OPTION_LONG("print-loader-path", NULL, "Print path to currently booted boot loader binary"): + arg_print_loader_path = true; + break; + + OPTION_LONG("print-stub-path", NULL, "Print path to currently booted unified kernel binary"): + arg_print_stub_path = true; + break; + + OPTION('R', "print-root-device", NULL, + "Print path to the block device node backing the root file system" + " (returns e.g. /dev/nvme0n1p5)"): {} + OPTION_HELP_VERBATIM("-RR", + "Print path to the whole disk block device node backing the root FS" + " (returns e.g. /dev/nvme0n1)"): + arg_print_root_device++; + break; + + OPTION_GROUP("Options"): + break; + + OPTION_COMMON_HELP: return help(); - case ARG_VERSION: + OPTION_COMMON_VERSION: return version(); - case ARG_ESP_PATH: - r = free_and_strdup(&arg_esp_path, optarg); + OPTION_LONG("esp-path", "PATH", "Path to the EFI System Partition (ESP)"): {} + OPTION_LONG("path", "PATH", /* help= */ NULL): /* Compatibility alias */ + r = free_and_strdup(&arg_esp_path, arg); if (r < 0) return log_oom(); break; - case ARG_BOOT_PATH: - r = free_and_strdup(&arg_xbootldr_path, optarg); + OPTION_LONG("boot-path", "PATH", "Path to the $BOOT partition"): + r = free_and_strdup(&arg_xbootldr_path, arg); if (r < 0) return log_oom(); break; - case ARG_ROOT: - r = parse_path_argument(optarg, /* suppress_root= */ true, &arg_root); + OPTION_LONG("root", "PATH", "Operate on an alternate filesystem root"): + r = parse_path_argument(arg, /* suppress_root= */ true, &arg_root); if (r < 0) return r; break; - case ARG_IMAGE: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image); + OPTION_LONG("image", "PATH", "Operate on disk image as filesystem root"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_image); if (r < 0) return r; break; - case ARG_IMAGE_POLICY: - r = parse_image_policy_argument(optarg, &arg_image_policy); + OPTION_LONG("image-policy", "POLICY", "Specify disk image dissection policy"): + r = parse_image_policy_argument(arg, &arg_image_policy); if (r < 0) return r; break; - case ARG_INSTALL_SOURCE: { - InstallSource is = install_source_from_string(optarg); + OPTION_LONG("install-source", "SOURCE", + "Where to pick files when using --root=/--image= (auto, image, host)"): { + InstallSource is = install_source_from_string(arg); if (is < 0) - return log_error_errno(is, "Unexpected parameter for --install-source=: %s", optarg); + return log_error_errno(is, "Unexpected parameter for --install-source=: %s", arg); arg_install_source = is; break; } - case 'p': - arg_print_esp_path = true; - break; - - case 'x': - arg_print_dollar_boot_path = true; - break; - - case ARG_PRINT_LOADER_PATH: - arg_print_loader_path = true; - break; - - case ARG_PRINT_STUB_PATH: - arg_print_stub_path = true; - break; - - case 'R': - arg_print_root_device++; - break; - - case ARG_VARIABLES: - r = parse_tristate_argument_with_auto("--variables=", optarg, &arg_touch_variables); + OPTION_LONG("variables", "BOOL", "Whether to modify EFI variables"): + r = parse_tristate_argument_with_auto("--variables=", arg, &arg_touch_variables); if (r < 0) return r; #if !ENABLE_EFI if (arg_touch_variables > 0) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), - "Compiled without support for EFI, --variables=%s cannot be specified.", optarg); + "Compiled without support for EFI, --variables=%s cannot be specified.", arg); #endif break; - case ARG_NO_VARIABLES: + OPTION_LONG("no-variables", NULL, /* help= */ NULL): /* Compatibility alias */ arg_touch_variables = false; break; - case ARG_RANDOM_SEED: - r = parse_boolean_argument("--random-seed=", optarg, &arg_install_random_seed); + OPTION_LONG("random-seed", "BOOL", "Whether to create random-seed file during install"): + r = parse_boolean_argument("--random-seed=", arg, &arg_install_random_seed); if (r < 0) return r; break; - case ARG_NO_PAGER: + OPTION_COMMON_NO_PAGER: arg_pager_flags |= PAGER_DISABLE; break; - case ARG_GRACEFUL: + OPTION_LONG("graceful", NULL, + "Don't fail when the ESP cannot be found or EFI variables cannot be written"): _arg_graceful = ARG_GRACEFUL_YES; break; - case 'q': + OPTION('q', "quiet", NULL, "Suppress output"): arg_quiet = true; break; - case ARG_ENTRY_TOKEN: - r = parse_boot_entry_token_type(optarg, &arg_entry_token_type, &arg_entry_token); + OPTION_LONG("entry-token", "TOKEN", + "Entry token to use for this installation" + " (machine-id, os-id, os-image-id, auto, literal:…)"): + r = parse_boot_entry_token_type(arg, &arg_entry_token_type, &arg_entry_token); if (r < 0) return r; break; - case ARG_MAKE_ENTRY_DIRECTORY: - if (streq(optarg, "auto")) /* retained for backwards compatibility */ + OPTION_LONG("make-entry-directory", "yes|no|auto", "Create $BOOT/ENTRY-TOKEN/ directory"): {} + OPTION_LONG("make-machine-id-directory", "BOOL", /* help= */ NULL): /* Compatibility alias */ + if (streq(arg, "auto")) /* retained for backwards compatibility */ arg_make_entry_directory = -1; /* yes if machine-id is permanent */ else { - r = parse_boolean_argument("--make-entry-directory=", optarg, NULL); + r = parse_boolean_argument("--make-entry-directory=", arg, NULL); if (r < 0) return r; @@ -551,95 +526,99 @@ static int parse_argv(int argc, char *argv[]) { } break; - case ARG_JSON: - r = parse_json_argument(optarg, &arg_json_format_flags); + OPTION_COMMON_JSON: + r = parse_json_argument(arg, &arg_json_format_flags); if (r <= 0) return r; break; - case ARG_ARCH_ALL: + OPTION_LONG("all-architectures", NULL, "Install all supported EFI architectures"): arg_arch_all = true; break; - case ARG_EFI_BOOT_OPTION_DESCRIPTION: - if (isempty(optarg) || !(string_is_safe(optarg) && utf8_is_valid(optarg))) { - _cleanup_free_ char *escaped = cescape(optarg); + OPTION_LONG("efi-boot-option-description", "DESCRIPTION", + "Description of the entry in the boot option list"): + if (isempty(arg) || !(string_is_safe(arg) && utf8_is_valid(arg))) { + _cleanup_free_ char *escaped = cescape(arg); return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid --efi-boot-option-description=: %s", strna(escaped)); } - if (strlen(optarg) > EFI_BOOT_OPTION_DESCRIPTION_MAX) + if (strlen(arg) > EFI_BOOT_OPTION_DESCRIPTION_MAX) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--efi-boot-option-description= too long: %zu > %zu", - strlen(optarg), EFI_BOOT_OPTION_DESCRIPTION_MAX); - r = free_and_strdup_warn(&arg_efi_boot_option_description, optarg); + strlen(arg), EFI_BOOT_OPTION_DESCRIPTION_MAX); + r = free_and_strdup_warn(&arg_efi_boot_option_description, arg); if (r < 0) return r; break; - case ARG_EFI_BOOT_OPTION_DESCRIPTION_WITH_DEVICE: - r = parse_boolean_argument("--efi-boot-option-description-with-device=", optarg, &arg_efi_boot_option_description_with_device); + OPTION_LONG("efi-boot-option-description-with-device", "BOOL", + "Suffix description with disk vendor/model/serial"): + r = parse_boolean_argument("--efi-boot-option-description-with-device=", arg, + &arg_efi_boot_option_description_with_device); if (r < 0) return r; - break; - case ARG_DRY_RUN: + OPTION_LONG("dry-run", NULL, "Dry run (unlink and cleanup)"): arg_dry_run = true; break; - case ARG_SECURE_BOOT_AUTO_ENROLL: - r = parse_boolean_argument("--secure-boot-auto-enroll=", optarg, &arg_secure_boot_auto_enroll); + OPTION_LONG("secure-boot-auto-enroll", "BOOL", "Set up secure boot auto-enrollment"): + r = parse_boolean_argument("--secure-boot-auto-enroll=", arg, + &arg_secure_boot_auto_enroll); if (r < 0) return r; break; - case ARG_CERTIFICATE: - r = free_and_strdup_warn(&arg_certificate, optarg); + OPTION_LONG("private-key", "PATH|URI", + "Private key for Secure Boot auto-enrollment"): + r = free_and_strdup_warn(&arg_private_key, arg); if (r < 0) return r; break; - case ARG_CERTIFICATE_SOURCE: - r = parse_openssl_certificate_source_argument( - optarg, - &arg_certificate_source, - &arg_certificate_source_type); + OPTION_LONG("private-key-source", "SOURCE", + "Specify how to use the private key " + "(file, provider:PROVIDER, engine:ENGINE)"): + r = parse_openssl_key_source_argument(arg, + &arg_private_key_source, + &arg_private_key_source_type); if (r < 0) return r; break; - case ARG_PRIVATE_KEY: { - r = free_and_strdup_warn(&arg_private_key, optarg); + OPTION_LONG("certificate", "PATH|URI", + "PEM certificate to use when setting up Secure Boot auto-enrollment, " + "or a provider specific designation if --certificate-source= is used"): + r = free_and_strdup_warn(&arg_certificate, arg); + if (r < 0) + return r; + break; + + OPTION_LONG("certificate-source", "SOURCE", + "Specify how to interpret the certificate from --certificate=. " + "Allows the certificate to be loaded from an OpenSSL provider " + "(file, provider:PROVIDER)"): + r = parse_openssl_certificate_source_argument(arg, + &arg_certificate_source, + &arg_certificate_source_type); if (r < 0) return r; break; } - case ARG_PRIVATE_KEY_SOURCE: - r = parse_openssl_key_source_argument( - optarg, - &arg_private_key_source, - &arg_private_key_source_type); - if (r < 0) - return r; - break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); - } + char **args = option_parser_get_args(&state); if (!!arg_print_esp_path + !!arg_print_dollar_boot_path + (arg_print_root_device > 0) + arg_print_loader_path + arg_print_stub_path > 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--print-esp-path/-p, --print-boot-path/-x, --print-root-device=/-R, --print-loader-path, --print-stub-path cannot be combined."); - if ((arg_root || arg_image) && argv[optind] && !STR_IN_SET(argv[optind], "status", "list", + if ((arg_root || arg_image) && args[0] && !STR_IN_SET(args[0], "status", "list", "install", "update", "remove", "is-installed", "random-seed", "unlink", "cleanup")) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Options --root= and --image= are not supported with verb %s.", - argv[optind]); + args[0]); if (arg_root && arg_image) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported."); @@ -647,7 +626,7 @@ static int parse_argv(int argc, char *argv[]) { if (arg_install_source != INSTALL_SOURCE_AUTO && !arg_root && !arg_image) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--install-from-host is only supported with --root= or --image=."); - if (arg_dry_run && argv[optind] && !STR_IN_SET(argv[optind], "unlink", "cleanup")) + if (arg_dry_run && args[0] && !STR_IN_SET(args[0], "unlink", "cleanup")) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--dry-run is only supported with --unlink or --cleanup"); if (arg_secure_boot_auto_enroll) { @@ -670,36 +649,10 @@ static int parse_argv(int argc, char *argv[]) { arg_pager_flags |= PAGER_DISABLE; } + *ret_args = args; return 1; } -static int bootctl_main(int argc, char *argv[]) { - static const Verb verbs[] = { - { "help", VERB_ANY, VERB_ANY, 0, verb_help }, - { "status", VERB_ANY, 1, VERB_DEFAULT, verb_status }, - { "install", VERB_ANY, 1, 0, verb_install }, - { "update", VERB_ANY, 1, 0, verb_install }, - { "remove", VERB_ANY, 1, 0, verb_remove }, - { "is-installed", VERB_ANY, 1, 0, verb_is_installed }, - { "kernel-identify", 2, 2, 0, verb_kernel_identify }, - { "kernel-inspect", 2, 2, 0, verb_kernel_inspect }, - { "list", VERB_ANY, 1, 0, verb_list }, - { "unlink", 2, 2, 0, verb_unlink }, - { "cleanup", VERB_ANY, 1, 0, verb_cleanup }, - { "set-default", 2, 2, 0, verb_set_efivar }, - { "set-preferred", 2, 2, 0, verb_set_efivar }, - { "set-oneshot", 2, 2, 0, verb_set_efivar }, - { "set-timeout", 2, 2, 0, verb_set_efivar }, - { "set-timeout-oneshot", 2, 2, 0, verb_set_efivar }, - { "set-sysfail", 2, 2, 0, verb_set_efivar }, - { "random-seed", VERB_ANY, 1, 0, verb_random_seed }, - { "reboot-to-firmware", VERB_ANY, 2, 0, verb_reboot_to_firmware }, - {} - }; - - return dispatch_verb(argc, argv, verbs, NULL); -} - static int vl_server(void) { _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL; int r; @@ -740,7 +693,8 @@ static int run(int argc, char *argv[]) { log_setup(); - r = parse_argv(argc, argv); + char **args = NULL; + r = parse_argv(argc, argv, &args); if (r <= 0) return r; @@ -800,7 +754,7 @@ static int run(int argc, char *argv[]) { return log_oom(); } - return bootctl_main(argc, argv); + return dispatch_verb_with_args(args, NULL); } DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c index de907c4ad6..e9bb27c254 100644 --- a/src/cryptenroll/cryptenroll.c +++ b/src/cryptenroll/cryptenroll.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include "sd-device.h" @@ -19,9 +18,11 @@ #include "cryptsetup-util.h" #include "extract-word.h" #include "fileio.h" +#include "format-table.h" #include "libfido2-util.h" #include "log.h" #include "main-func.h" +#include "options.h" #include "pager.h" #include "parse-argument.h" #include "parse-util.h" @@ -30,6 +31,7 @@ #include "process-util.h" #include "string-table.h" #include "string-util.h" +#include "strv.h" #include "tpm2-pcr.h" #include "tpm2-util.h" @@ -231,207 +233,96 @@ static int help(void) { if (r < 0) return log_oom(); - printf("%1$s [OPTIONS...] [BLOCK-DEVICE]\n\n" - "%5$sEnroll a security token or authentication credential to a LUKS volume.%6$s\n\n" - " -h --help Show this help\n" - " --version Show package version\n" - " --no-pager Do not spawn a pager\n" - " --list-devices List candidate block devices to operate on\n" - " --wipe-slot=SLOT1,SLOT2,…\n" - " Wipe specified slots\n" - "\n%3$sUnlocking:%4$s\n" - " --unlock-key-file=PATH\n" - " Use a file to unlock the volume\n" - " --unlock-fido2-device=PATH\n" - " Use a FIDO2 device to unlock the volume\n" - " --unlock-tpm2-device=PATH\n" - " Use a TPM2 device to unlock the volume\n" - "\n%3$sSimple Enrollment:%4$s\n" - " --password Enroll a user-supplied password\n" - " --recovery-key Enroll a recovery key\n" - "\n%3$sPKCS#11 Enrollment:%4$s\n" - " --pkcs11-token-uri=URI|auto|list\n" - " Enroll a PKCS#11 security token or list them\n" - "\n%3$sFIDO2 Enrollment:%4$s\n" - " --fido2-device=PATH|auto|list\n" - " Enroll a FIDO2-HMAC security token or list them\n" - " --fido2-salt-file=PATH\n" - " Use salt from a file instead of generating one\n" - " --fido2-parameters-in-header=BOOL\n" - " Whether to store FIDO2 parameters in the LUKS2 header\n" - " --fido2-credential-algorithm=STRING\n" - " Specify COSE algorithm for FIDO2 credential\n" - " --fido2-with-client-pin=BOOL\n" - " Whether to require entering a PIN to unlock the volume\n" - " --fido2-with-user-presence=BOOL\n" - " Whether to require user presence to unlock the volume\n" - " --fido2-with-user-verification=BOOL\n" - " Whether to require user verification to unlock the volume\n" - "\n%3$sTPM2 Enrollment:%4$s\n" - " --tpm2-device=PATH|auto|list\n" - " Enroll a TPM2 device or list them\n" - " --tpm2-device-key=PATH\n" - " Enroll a TPM2 device using its public key\n" - " --tpm2-seal-key-handle=HANDLE\n" - " Specify handle of key to use for sealing\n" - " --tpm2-pcrs=PCR1+PCR2+PCR3+…\n" - " Specify TPM2 PCRs to seal against\n" - " --tpm2-public-key=PATH\n" - " Enroll signed TPM2 PCR policy against PEM public key\n" - " --tpm2-public-key-pcrs=PCR1+PCR2+PCR3+…\n" - " Enroll signed TPM2 PCR policy for specified TPM2 PCRs\n" - " --tpm2-signature=PATH\n" - " Validate public key enrollment works with JSON signature\n" - " file\n" - " --tpm2-pcrlock=PATH\n" - " Specify pcrlock policy to lock against\n" - " --tpm2-with-pin=BOOL\n" - " Whether to require entering a PIN to unlock the volume\n" - "\nSee the %2$s for details.\n", + static const char* const groups[] = { + NULL, + "Unlocking", + "Simple Enrollment", + "PKCS#11 Enrollment", + "FIDO2 Enrollment", + "TPM2 Enrollment", + }; + + _cleanup_(table_unref_many) Table *tables[ELEMENTSOF(groups) + 1] = {}; + + for (size_t i = 0; i < ELEMENTSOF(groups); i++) { + r = option_parser_get_help_table_group(groups[i], &tables[i]); + if (r < 0) + return r; + } + + (void) table_sync_column_widths(0, tables[0], tables[1], tables[2], tables[3], tables[4], tables[5]); + + printf("%s [OPTIONS...] [BLOCK-DEVICE]\n\n" + "%sEnroll a security token or authentication credential to a LUKS volume.%s\n", program_invocation_short_name, - link, - ansi_underline(), - ansi_normal(), ansi_highlight(), ansi_normal()); + for (size_t i = 0; i < ELEMENTSOF(groups); i++) { + printf("\n%s%s:%s\n", ansi_underline(), groups[i] ?: "Options", ansi_normal()); + + r = table_print_or_warn(tables[i]); + if (r < 0) + return r; + } + + printf("\nSee the %s for details.\n", link); return 0; } static int parse_argv(int argc, char *argv[]) { - enum { - ARG_VERSION = 0x100, - ARG_NO_PAGER, - ARG_PASSWORD, - ARG_RECOVERY_KEY, - ARG_UNLOCK_KEYFILE, - ARG_UNLOCK_FIDO2_DEVICE, - ARG_UNLOCK_TPM2_DEVICE, - ARG_PKCS11_TOKEN_URI, - ARG_FIDO2_DEVICE, - ARG_FIDO2_SALT_FILE, - ARG_FIDO2_PARAMETERS_IN_HEADER, - ARG_TPM2_DEVICE, - ARG_TPM2_DEVICE_KEY, - ARG_TPM2_SEAL_KEY_HANDLE, - ARG_TPM2_PCRS, - ARG_TPM2_PUBLIC_KEY, - ARG_TPM2_PUBLIC_KEY_PCRS, - ARG_TPM2_SIGNATURE, - ARG_TPM2_PCRLOCK, - ARG_TPM2_WITH_PIN, - ARG_WIPE_SLOT, - ARG_FIDO2_WITH_PIN, - ARG_FIDO2_WITH_UP, - ARG_FIDO2_WITH_UV, - ARG_FIDO2_CRED_ALG, - ARG_LIST_DEVICES, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "no-pager", no_argument, NULL, ARG_NO_PAGER }, - { "password", no_argument, NULL, ARG_PASSWORD }, - { "recovery-key", no_argument, NULL, ARG_RECOVERY_KEY }, - { "unlock-key-file", required_argument, NULL, ARG_UNLOCK_KEYFILE }, - { "unlock-fido2-device", required_argument, NULL, ARG_UNLOCK_FIDO2_DEVICE }, - { "unlock-tpm2-device", required_argument, NULL, ARG_UNLOCK_TPM2_DEVICE }, - { "pkcs11-token-uri", required_argument, NULL, ARG_PKCS11_TOKEN_URI }, - { "fido2-credential-algorithm", required_argument, NULL, ARG_FIDO2_CRED_ALG }, - { "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE }, - { "fido2-salt-file", required_argument, NULL, ARG_FIDO2_SALT_FILE }, - { "fido2-parameters-in-header", required_argument, NULL, ARG_FIDO2_PARAMETERS_IN_HEADER }, - { "fido2-with-client-pin", required_argument, NULL, ARG_FIDO2_WITH_PIN }, - { "fido2-with-user-presence", required_argument, NULL, ARG_FIDO2_WITH_UP }, - { "fido2-with-user-verification", required_argument, NULL, ARG_FIDO2_WITH_UV }, - { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, - { "tpm2-device-key", required_argument, NULL, ARG_TPM2_DEVICE_KEY }, - { "tpm2-seal-key-handle", required_argument, NULL, ARG_TPM2_SEAL_KEY_HANDLE }, - { "tpm2-pcrs", required_argument, NULL, ARG_TPM2_PCRS }, - { "tpm2-public-key", required_argument, NULL, ARG_TPM2_PUBLIC_KEY }, - { "tpm2-public-key-pcrs", required_argument, NULL, ARG_TPM2_PUBLIC_KEY_PCRS }, - { "tpm2-signature", required_argument, NULL, ARG_TPM2_SIGNATURE }, - { "tpm2-pcrlock", required_argument, NULL, ARG_TPM2_PCRLOCK }, - { "tpm2-with-pin", required_argument, NULL, ARG_TPM2_WITH_PIN }, - { "wipe-slot", required_argument, NULL, ARG_WIPE_SLOT }, - { "list-devices", no_argument, NULL, ARG_LIST_DEVICES }, - {} - }; - bool auto_public_key_pcr_mask = true, auto_pcrlock = true; - int c, r; assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + OptionParser state = { argc, argv }; + const char *arg; + int r; + 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_FIDO2_WITH_PIN: - r = parse_boolean_argument("--fido2-with-client-pin=", optarg, NULL); + OPTION_LONG("list-devices", NULL, + "List candidate block devices to operate on"): + return blockdev_list(BLOCKDEV_LIST_SHOW_SYMLINKS|BLOCKDEV_LIST_REQUIRE_LUKS, + /* ret_devices= */ NULL, + /* ret_n_devices= */ NULL); + + OPTION_LONG("wipe-slot", "SLOT1,SLOT2,…", + "Wipe specified slots"): + r = parse_wipe_slot(arg); if (r < 0) return r; - - SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_PIN, r); break; - case ARG_FIDO2_WITH_UP: - r = parse_boolean_argument("--fido2-with-user-presence=", optarg, NULL); - if (r < 0) - return r; + OPTION_GROUP("Unlocking"): {} - SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UP, r); - break; - - case ARG_FIDO2_WITH_UV: - r = parse_boolean_argument("--fido2-with-user-verification=", optarg, NULL); - if (r < 0) - return r; - - SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UV, r); - break; - - case ARG_PASSWORD: - if (arg_enroll_type >= 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Multiple operations specified at once, refusing."); - - arg_enroll_type = ENROLL_PASSWORD; - break; - - case ARG_RECOVERY_KEY: - if (arg_enroll_type >= 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Multiple operations specified at once, refusing."); - - arg_enroll_type = ENROLL_RECOVERY; - break; - - case ARG_UNLOCK_KEYFILE: + OPTION_LONG("unlock-key-file", "PATH", + "Use a file to unlock the volume"): if (arg_unlock_type != UNLOCK_PASSWORD) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Multiple unlock methods specified at once, refusing."); - r = parse_path_argument(optarg, /* suppress_root= */ true, &arg_unlock_keyfile); + r = parse_path_argument(arg, /* suppress_root= */ true, &arg_unlock_keyfile); if (r < 0) return r; arg_unlock_type = UNLOCK_KEYFILE; break; - case ARG_UNLOCK_FIDO2_DEVICE: { + OPTION_LONG("unlock-fido2-device", "PATH", + "Use a FIDO2 device to unlock the volume"): { _cleanup_free_ char *device = NULL; if (arg_unlock_type != UNLOCK_PASSWORD) @@ -440,8 +331,8 @@ static int parse_argv(int argc, char *argv[]) { assert(!arg_unlock_fido2_device); - if (!streq(optarg, "auto")) { - device = strdup(optarg); + if (!streq(arg, "auto")) { + device = strdup(arg); if (!device) return log_oom(); } @@ -451,7 +342,8 @@ static int parse_argv(int argc, char *argv[]) { break; } - case ARG_UNLOCK_TPM2_DEVICE: { + OPTION_LONG("unlock-tpm2-device", "PATH", + "Use a TPM2 device to unlock the volume"): { _cleanup_free_ char *device = NULL; if (arg_unlock_type != UNLOCK_PASSWORD) @@ -460,8 +352,8 @@ static int parse_argv(int argc, char *argv[]) { assert(!arg_unlock_tpm2_device); - if (!streq(optarg, "auto")) { - device = strdup(optarg); + if (!streq(arg, "auto")) { + device = strdup(arg); if (!device) return log_oom(); } @@ -471,25 +363,48 @@ static int parse_argv(int argc, char *argv[]) { break; } - case ARG_PKCS11_TOKEN_URI: { + OPTION_GROUP("Simple Enrollment"): {} + + OPTION_LONG("password", NULL, + "Enroll a user-supplied password"): + if (arg_enroll_type >= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Multiple operations specified at once, refusing."); + + arg_enroll_type = ENROLL_PASSWORD; + break; + + OPTION_LONG("recovery-key", NULL, + "Enroll a recovery key"): + if (arg_enroll_type >= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Multiple operations specified at once, refusing."); + + arg_enroll_type = ENROLL_RECOVERY; + break; + + OPTION_GROUP("PKCS#11 Enrollment"): {} + + OPTION_LONG("pkcs11-token-uri", "URI|auto|list", + "Enroll a PKCS#11 security token or list them"): { _cleanup_free_ char *uri = NULL; - if (streq(optarg, "list")) + if (streq(arg, "list")) return pkcs11_list_tokens(); if (arg_enroll_type >= 0 || arg_pkcs11_token_uri) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Multiple operations specified at once, refusing."); - if (streq(optarg, "auto")) { + if (streq(arg, "auto")) { r = pkcs11_find_token_auto(&uri); if (r < 0) return r; } else { - if (!pkcs11_uri_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid PKCS#11 URI: %s", optarg); + if (!pkcs11_uri_valid(arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid PKCS#11 URI: %s", arg); - uri = strdup(optarg); + uri = strdup(arg); if (!uri) return log_oom(); } @@ -499,24 +414,21 @@ static int parse_argv(int argc, char *argv[]) { break; } - case ARG_FIDO2_CRED_ALG: - r = parse_fido2_algorithm(optarg, &arg_fido2_cred_alg); - if (r < 0) - return log_error_errno(r, "Failed to parse COSE algorithm: %s", optarg); - break; + OPTION_GROUP("FIDO2 Enrollment"): {} - case ARG_FIDO2_DEVICE: { + OPTION_LONG("fido2-device", "PATH|auto|list", + "Enroll a FIDO2-HMAC security token or list them"): { _cleanup_free_ char *device = NULL; - if (streq(optarg, "list")) + if (streq(arg, "list")) return fido2_list_devices(); if (arg_enroll_type >= 0 || arg_fido2_device) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Multiple operations specified at once, refusing."); - if (!streq(optarg, "auto")) { - device = strdup(optarg); + if (!streq(arg, "auto")) { + device = strdup(arg); if (!device) return log_oom(); } @@ -526,32 +438,66 @@ static int parse_argv(int argc, char *argv[]) { break; } - case ARG_FIDO2_SALT_FILE: - r = parse_path_argument(optarg, /* suppress_root= */ true, &arg_fido2_salt_file); + OPTION_LONG("fido2-salt-file", "PATH", + "Use salt from a file instead of generating one"): + r = parse_path_argument(arg, /* suppress_root= */ true, &arg_fido2_salt_file); if (r < 0) return r; - break; - case ARG_FIDO2_PARAMETERS_IN_HEADER: - r = parse_boolean_argument("--fido2-parameters-in-header=", optarg, &arg_fido2_parameters_in_header); + OPTION_LONG("fido2-parameters-in-header", "BOOL", + "Whether to store FIDO2 parameters in the LUKS2 header"): + r = parse_boolean_argument("--fido2-parameters-in-header=", arg, &arg_fido2_parameters_in_header); if (r < 0) return r; - break; - case ARG_TPM2_DEVICE: { + OPTION_LONG("fido2-credential-algorithm", "STRING", + "Specify COSE algorithm for FIDO2 credential"): + r = parse_fido2_algorithm(arg, &arg_fido2_cred_alg); + if (r < 0) + return log_error_errno(r, "Failed to parse COSE algorithm: %s", arg); + break; + + OPTION_LONG("fido2-with-client-pin", "BOOL", + "Whether to require entering a PIN to unlock the volume"): + r = parse_boolean_argument("--fido2-with-client-pin=", arg, NULL); + if (r < 0) + return r; + SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_PIN, r); + break; + + OPTION_LONG("fido2-with-user-presence", "BOOL", + "Whether to require user presence to unlock the volume"): + r = parse_boolean_argument("--fido2-with-user-presence=", arg, NULL); + if (r < 0) + return r; + SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UP, r); + break; + + OPTION_LONG("fido2-with-user-verification", "BOOL", + "Whether to require user verification to unlock the volume"): + r = parse_boolean_argument("--fido2-with-user-verification=", arg, NULL); + if (r < 0) + return r; + SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UV, r); + break; + + OPTION_GROUP("TPM2 Enrollment"): {} + + OPTION_LONG("tpm2-device", "PATH|auto|list", + "Enroll a TPM2 device or list them"): { _cleanup_free_ char *device = NULL; - if (streq(optarg, "list")) + if (streq(arg, "list")) return tpm2_list_devices(/* legend= */ true, /* quiet= */ false); if (arg_enroll_type >= 0 || arg_tpm2_device) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Multiple operations specified at once, refusing."); - if (!streq(optarg, "auto")) { - device = strdup(optarg); + if (!streq(arg, "auto")) { + device = strdup(arg); if (!device) return log_oom(); } @@ -561,115 +507,96 @@ static int parse_argv(int argc, char *argv[]) { break; } - case ARG_TPM2_DEVICE_KEY: + OPTION_LONG("tpm2-device-key", "PATH", + "Enroll a TPM2 device using its public key"): if (arg_enroll_type >= 0 || arg_tpm2_device_key) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Multiple operations specified at once, refusing."); - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_device_key); + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_tpm2_device_key); if (r < 0) return r; arg_enroll_type = ENROLL_TPM2; break; - case ARG_TPM2_SEAL_KEY_HANDLE: - r = safe_atou32_full(optarg, 16, &arg_tpm2_seal_key_handle); + OPTION_LONG("tpm2-seal-key-handle", "HANDLE", + "Specify handle of key to use for sealing"): + r = safe_atou32_full(arg, 16, &arg_tpm2_seal_key_handle); if (r < 0) - return log_error_errno(r, "Could not parse TPM2 seal key handle index '%s': %m", optarg); - + return log_error_errno(r, "Could not parse TPM2 seal key handle index '%s': %m", arg); break; - case ARG_TPM2_PCRS: - r = tpm2_parse_pcr_argument_append(optarg, &arg_tpm2_hash_pcr_values, &arg_tpm2_n_hash_pcr_values); + OPTION_LONG("tpm2-pcrs", "PCR1+PCR2+PCR3+…", + "Specify TPM2 PCRs to seal against"): + r = tpm2_parse_pcr_argument_append(arg, &arg_tpm2_hash_pcr_values, &arg_tpm2_n_hash_pcr_values); if (r < 0) return r; - break; - case ARG_TPM2_PUBLIC_KEY: + OPTION_LONG("tpm2-public-key", "PATH", + "Enroll signed TPM2 PCR policy against PEM public key"): /* an empty argument disables loading a public key */ - if (isempty(optarg)) { + if (isempty(arg)) { arg_tpm2_load_public_key = false; arg_tpm2_public_key = mfree(arg_tpm2_public_key); break; } - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_public_key); + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_tpm2_public_key); if (r < 0) return r; arg_tpm2_load_public_key = true; - break; - case ARG_TPM2_PUBLIC_KEY_PCRS: + OPTION_LONG("tpm2-public-key-pcrs", "PCR1+PCR2+PCR3+…", + "Enroll signed TPM2 PCR policy for specified TPM2 PCRs"): auto_public_key_pcr_mask = false; - r = tpm2_parse_pcr_argument_to_mask(optarg, &arg_tpm2_public_key_pcr_mask); + r = tpm2_parse_pcr_argument_to_mask(arg, &arg_tpm2_public_key_pcr_mask); if (r < 0) return r; - break; - case ARG_TPM2_SIGNATURE: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_signature); + OPTION_LONG("tpm2-signature", "PATH", + "Validate public key enrollment works with JSON signature file"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_tpm2_signature); if (r < 0) return r; - break; - case ARG_TPM2_PCRLOCK: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_pcrlock); + OPTION_LONG("tpm2-pcrlock", "PATH", + "Specify pcrlock policy to lock against"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_tpm2_pcrlock); if (r < 0) return r; - auto_pcrlock = false; break; - case ARG_TPM2_WITH_PIN: - r = parse_boolean_argument("--tpm2-with-pin=", optarg, &arg_tpm2_pin); - if (r < 0) - return r; - - break; - - case ARG_WIPE_SLOT: - r = parse_wipe_slot(optarg); + OPTION_LONG("tpm2-with-pin", "BOOL", + "Whether to require entering a PIN to unlock the volume"): + r = parse_boolean_argument("--tpm2-with-pin=", arg, &arg_tpm2_pin); if (r < 0) return r; break; - - case ARG_LIST_DEVICES: - return blockdev_list(BLOCKDEV_LIST_SHOW_SYMLINKS|BLOCKDEV_LIST_REQUIRE_LUKS, - /* ret_devices= */ NULL, - /* ret_n_devices= */ NULL); - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } - if (argc > optind+1) + char **args = option_parser_get_args(&state); + + if (strv_length(args) > 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too many arguments, refusing."); - if (optind < argc) { - r = parse_path_argument(argv[optind], false, &arg_node); - if (r < 0) - return r; - } else { - if (wipe_requested()) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Wiping requested and no block device node specified, refusing."); - + if (args[0]) + r = parse_path_argument(args[0], false, &arg_node); + else if (!wipe_requested()) r = determine_default_node(); - if (r < 0) - return r; - } + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Wiping requested and no block device node specified, refusing."); + if (r < 0) + return r; if (arg_enroll_type == ENROLL_FIDO2) { - if (arg_unlock_type == UNLOCK_FIDO2 && !(arg_fido2_device && arg_unlock_fido2_device)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "When both enrolling and unlocking with FIDO2 tokens, automatic discovery is unsupported. " diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index 04eeb6223e..ff9449abd1 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include #include @@ -11,7 +10,6 @@ #include "sd-messages.h" #include "alloc-util.h" -#include "argv-util.h" #include "ask-password-api.h" #include "build.h" #include "cryptsetup-fido2.h" @@ -27,6 +25,7 @@ #include "escape.h" #include "extract-word.h" #include "fileio.h" +#include "format-table.h" #include "fs-util.h" #include "hexdecoct.h" #include "json-util.h" @@ -36,6 +35,7 @@ #include "main-func.h" #include "memory-util.h" #include "nulstr-util.h" +#include "options.h" #include "parse-util.h" #include "path-util.h" #include "pkcs11-util.h" @@ -2451,61 +2451,69 @@ static int attach_luks_or_plain_or_bitlk( static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *options = NULL, *verbs = NULL; int r; r = terminal_urlify_man("systemd-cryptsetup", "8", &link); if (r < 0) return log_oom(); - printf("%1$s attach VOLUME SOURCE-DEVICE [KEY-FILE] [CONFIG]\n" - "%1$s detach VOLUME\n\n" - "%2$sAttach or detach an encrypted block device.%3$s\n\n" - " -h --help Show this help\n" - " --version Show package version\n" - "\nSee the %4$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" + "%sAttach or detach an encrypted block device.%s\n" + "\n%sCommands:%s\n", program_invocation_short_name, ansi_highlight(), ansi_normal(), - link); + 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 parse_argv(int argc, char *argv[]) { - enum { - ARG_VERSION = 0x100, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - {} - }; - - int c; +VERB_COMMON_HELP_HIDDEN(help); +static int parse_argv(int argc, char *argv[], char ***ret_args) { assert(argc >= 0); assert(argv); + assert(ret_args); - if (argv_looks_like_help(argc, argv)) - return help(); + OptionParser state = { argc, argv }; + const char *arg; - while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + 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 '?': - return -EINVAL; - - default: - assert_not_reached(); } + *ret_args = option_parser_get_args(&state); return 1; } @@ -2588,6 +2596,8 @@ static int discover_key(const char *key_file, const char *volume, TokenType toke return r; } +VERB(verb_attach, "attach", "VOLUME SOURCE-DEVICE [KEY-FILE] [CONFIG]", 3, 5, 0, + "Attach an encrypted block device"); static int verb_attach(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(crypt_freep) struct crypt_device *cd = NULL; _unused_ _cleanup_(remove_and_erasep) const char *destroy_key_file = NULL; @@ -2828,6 +2838,8 @@ static int verb_attach(int argc, char *argv[], uintptr_t _data, void *userdata) return 0; } +VERB(verb_detach, "detach", "VOLUME", 2, 2, 0, + "Detach an encrypted block device"); static int verb_detach(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(crypt_freep) struct crypt_device *cd = NULL; const char *volume = ASSERT_PTR(argv[1]); @@ -2862,19 +2874,14 @@ static int run(int argc, char *argv[]) { umask(0022); - r = parse_argv(argc, argv); + char **args = NULL; + r = parse_argv(argc, argv, &args); if (r <= 0) return r; cryptsetup_enable_logging(NULL); - static const Verb verbs[] = { - { "attach", 3, 5, 0, verb_attach }, - { "detach", 2, 2, 0, verb_detach }, - {} - }; - - return dispatch_verb(argc, argv, verbs, NULL); + return dispatch_verb_with_args(args, NULL); } DEFINE_MAIN_FUNCTION(run); diff --git a/src/delta/delta.c b/src/delta/delta.c index df28a730a5..4a134ad235 100644 --- a/src/delta/delta.c +++ b/src/delta/delta.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include "alloc-util.h" @@ -10,12 +9,14 @@ #include "errno-util.h" #include "extract-word.h" #include "fd-util.h" +#include "format-table.h" #include "fs-util.h" #include "glyph-util.h" #include "hashmap.h" #include "log.h" #include "main-func.h" #include "nulstr-util.h" +#include "options.h" #include "pager.h" #include "parse-argument.h" #include "path-util.h" @@ -459,23 +460,26 @@ static int process_suffix_chop(const char *arg) { static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *options = NULL; int r; r = terminal_urlify_man("systemd-delta", "1", &link); if (r < 0) return log_oom(); - printf("%s [OPTIONS...] [SUFFIX...]\n\n" - "Find overridden configuration files.\n\n" - " -h --help Show this help\n" - " --version Show package version\n" - " --no-pager Do not pipe output into a pager\n" - " --diff[=1|0] Show a diff when overridden files differ\n" - " -t --type=LIST... Only display a selected set of override types\n" - "\nSee the %s for details.\n", - program_invocation_short_name, - link); + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + printf("%s [OPTIONS...] [SUFFIX...]\n\n" + "Find overridden configuration files.\n\n", + program_invocation_short_name); + + r = table_print_or_warn(options); + if (r < 0) + return r; + + printf("\nSee the %s for details.\n", link); return 0; } @@ -509,66 +513,45 @@ static int parse_flags(const char *flag_str, int flags) { } } -static int parse_argv(int argc, char *argv[]) { - - enum { - ARG_NO_PAGER = 0x100, - ARG_DIFF, - ARG_VERSION - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "no-pager", no_argument, NULL, ARG_NO_PAGER }, - { "diff", optional_argument, NULL, ARG_DIFF }, - { "type", required_argument, NULL, 't' }, - {} - }; - - int c, r; - - assert(argc >= 1); +static int parse_argv(int argc, char *argv[], char ***ret_args) { + assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "ht:", options, NULL)) >= 0) + OptionParser state = { argc, argv }; + const char *arg; + int r; + 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 't': { - int f; - f = parse_flags(optarg, arg_flags); - if (f < 0) + OPTION('t', "type", "TYPE...", "Only display a selected set of override types"): + r = parse_flags(arg, arg_flags); + if (r < 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse flags field."); - arg_flags = f; + arg_flags = r; break; - } - case ARG_DIFF: - r = parse_boolean_argument("--diff", optarg, NULL); + OPTION_LONG_FLAGS(OPTION_OPTIONAL_ARG, "diff", "yes|no", + "Show a diff when overridden files differ"): + r = parse_boolean_argument("--diff", arg, NULL); if (r < 0) return r; arg_diff = r; break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } + *ret_args = option_parser_get_args(&state); return 1; } @@ -577,7 +560,8 @@ static int run(int argc, char *argv[]) { log_setup(); - r = parse_argv(argc, argv); + char **args = NULL; + r = parse_argv(argc, argv, &args); if (r <= 0) return r; @@ -591,17 +575,16 @@ static int run(int argc, char *argv[]) { pager_open(arg_pager_flags); - if (optind < argc) { - for (int i = optind; i < argc; i++) { - path_simplify(argv[i]); + if (!strv_isempty(args)) { + STRV_FOREACH(i, args) { + path_simplify(*i); - k = process_suffix_chop(argv[i]); + k = process_suffix_chop(*i); if (k < 0) r = k; else n_found += k; } - } else { k = process_suffixes(NULL); if (k < 0) diff --git a/src/escape/escape-tool.c b/src/escape/escape-tool.c index 621182897d..aa4129cdee 100644 --- a/src/escape/escape-tool.c +++ b/src/escape/escape-tool.c @@ -1,12 +1,13 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include "alloc-util.h" #include "build.h" +#include "format-table.h" #include "log.h" #include "main-func.h" +#include "options.h" #include "path-util.h" #include "pretty-print.h" #include "string-util.h" @@ -26,107 +27,83 @@ static bool arg_instance = false; static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *options = NULL; int r; r = terminal_urlify_man("systemd-escape", "1", &link); if (r < 0) return log_oom(); - printf("%1$s [OPTIONS...] [NAME...]\n\n" - "%3$sEscape strings for usage in systemd unit names.%4$s\n\n" - " -h --help Show this help\n" - " --version Show package version\n" - " --suffix=SUFFIX Unit suffix to append to escaped strings\n" - " --template=TEMPLATE Insert strings as instance into template\n" - " --instance With --unescape, show just the instance part\n" - " -u --unescape Unescape strings\n" - " -m --mangle Mangle strings\n" - " -p --path When escaping/unescaping assume the string is a path\n" - "\nSee the %2$s for details.\n", + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + + printf("%s [OPTIONS...] [NAME...]\n\n" + "%sEscape strings for usage in systemd unit names.%s\n\n", program_invocation_short_name, - link, ansi_highlight(), 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 parse_argv(int argc, char *argv[]) { - - enum { - ARG_VERSION = 0x100, - ARG_SUFFIX, - ARG_TEMPLATE - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "suffix", required_argument, NULL, ARG_SUFFIX }, - { "template", required_argument, NULL, ARG_TEMPLATE }, - { "unescape", no_argument, NULL, 'u' }, - { "mangle", no_argument, NULL, 'm' }, - { "path", no_argument, NULL, 'p' }, - { "instance", no_argument, NULL, 'i' }, - {} - }; - - int c; - +static int parse_argv(int argc, char *argv[], char ***ret_args) { assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "hump", options, NULL)) >= 0) + 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_SUFFIX: { - UnitType t = unit_type_from_string(optarg); + OPTION_LONG("suffix", "SUFFIX", "Unit suffix to append to escaped strings"): { + UnitType t = unit_type_from_string(arg); if (t < 0) - return log_error_errno(t, "Invalid unit suffix type \"%s\".", optarg); + return log_error_errno(t, "Invalid unit suffix type \"%s\".", arg); - arg_suffix = optarg; + arg_suffix = arg; break; } - case ARG_TEMPLATE: - if (!unit_name_is_valid(optarg, UNIT_NAME_TEMPLATE)) + OPTION_LONG("template", "TEMPLATE", "Insert strings as instance into template"): + if (!unit_name_is_valid(arg, UNIT_NAME_TEMPLATE)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Template name %s is not valid.", optarg); + "Template name %s is not valid.", arg); - arg_template = optarg; + arg_template = arg; break; - case 'u': - arg_action = ACTION_UNESCAPE; - break; - - case 'm': - arg_action = ACTION_MANGLE; - break; - - case 'p': - arg_path = true; - break; - - case 'i': + OPTION_LONG("instance", NULL, "With --unescape, show just the instance part"): arg_instance = true; break; - case '?': - return -EINVAL; + OPTION('u', "unescape", NULL, "Unescape strings"): + arg_action = ACTION_UNESCAPE; + break; - default: - assert_not_reached(); + OPTION('m', "mangle", NULL, "Mangle strings"): + arg_action = ACTION_MANGLE; + break; + + OPTION('p', "path", NULL, + "When escaping/unescaping assume the string is a path"): + arg_path = true; + break; } - if (optind >= argc) + if (option_parser_get_n_args(&state) == 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not enough arguments."); @@ -154,6 +131,7 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--instance may not be combined with --template."); + *ret_args = option_parser_get_args(&state); return 1; } @@ -162,11 +140,12 @@ static int run(int argc, char *argv[]) { log_setup(); - r = parse_argv(argc, argv); + char **args = NULL; + r = parse_argv(argc, argv, &args); if (r <= 0) return r; - STRV_FOREACH(i, argv + optind) { + STRV_FOREACH(i, args) { _cleanup_free_ char *e = NULL; switch (arg_action) { @@ -267,7 +246,7 @@ static int run(int argc, char *argv[]) { break; } - if (i != argv + optind) + if (i != args) fputc(' ', stdout); fputs(e, stdout); diff --git a/src/import/import.c b/src/import/import.c index 3fe6adb8d2..c678c68f11 100644 --- a/src/import/import.c +++ b/src/import/import.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include @@ -12,12 +11,14 @@ #include "discover-image.h" #include "env-util.h" #include "fd-util.h" +#include "format-table.h" #include "import-raw.h" #include "import-tar.h" #include "import-util.h" #include "io-util.h" #include "log.h" #include "main-func.h" +#include "options.h" #include "parse-argument.h" #include "parse-util.h" #include "path-util.h" @@ -138,7 +139,8 @@ static void on_tar_finished(TarImport *import, int error, void *userdata) { sd_event_exit(event, ABS(error)); } -static int verb_import_tar(int argc, char *argv[], uintptr_t _data, void *userdata) { +VERB(verb_tar, "tar", "FILE [NAME]", 2, 3, 0, "Import a TAR image"); +static int verb_tar(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(tar_import_unrefp) TarImport *import = NULL; _cleanup_free_ char *ll = NULL, *normalized = NULL; _cleanup_(sd_event_unrefp) sd_event *event = NULL; @@ -207,7 +209,8 @@ static void on_raw_finished(RawImport *import, int error, void *userdata) { sd_event_exit(event, ABS(error)); } -static int verb_import_raw(int argc, char *argv[], uintptr_t _data, void *userdata) { +VERB(verb_raw, "raw", "FILE [NAME]", 2, 3, 0, "Import a RAW image"); +static int verb_raw(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(raw_import_unrefp) RawImport *import = NULL; _cleanup_free_ char *ll = NULL, *normalized = NULL; _cleanup_(sd_event_unrefp) sd_event *event = NULL; @@ -269,193 +272,152 @@ static int verb_import_raw(int argc, char *argv[], uintptr_t _data, void *userda } static int help(void) { - printf("%1$s [OPTIONS...] {COMMAND} ...\n" - "\n%4$sImport disk images.%5$s\n" - "\n%2$sCommands:%3$s\n" - " tar FILE [NAME] Import a TAR image\n" - " raw FILE [NAME] Import a RAW image\n" - "\n%2$sOptions:%3$s\n" - " -h --help Show this help\n" - " --version Show package version\n" - " --force Force creation of image\n" - " --image-root=PATH Image root directory\n" - " --read-only Create a read-only image\n" - " --direct Import directly to specified file\n" - " --btrfs-subvol=BOOL Controls whether to create a btrfs subvolume\n" - " instead of a directory\n" - " --btrfs-quota=BOOL Controls whether to set up quota for btrfs\n" - " subvolume\n" - " --convert-qcow2=BOOL Controls whether to convert QCOW2 images to\n" - " regular disk images\n" - " --sync=BOOL Controls whether to sync() before completing\n" - " --offset=BYTES Offset to seek to in destination\n" - " --size-max=BYTES Maximum number of bytes to write to destination\n" - " --class=CLASS Select image class (machine, sysext, confext,\n" - " portable)\n" - " --system Operate in per-system mode\n" - " --user Operate in per-user mode\n", + _cleanup_(table_unrefp) Table *options = NULL, *verbs = 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); + + printf("%s [OPTIONS...] {COMMAND} ...\n\n" + "%sImport disk images.%s\n" + "\n%sCommands:%s\n", program_invocation_short_name, - 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; + 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_argv(int argc, char *argv[]) { - enum { - ARG_VERSION = 0x100, - ARG_FORCE, - ARG_IMAGE_ROOT, - ARG_READ_ONLY, - ARG_DIRECT, - ARG_BTRFS_SUBVOL, - ARG_BTRFS_QUOTA, - ARG_CONVERT_QCOW2, - ARG_SYNC, - ARG_OFFSET, - ARG_SIZE_MAX, - ARG_CLASS, - ARG_SYSTEM, - ARG_USER, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "force", no_argument, NULL, ARG_FORCE }, - { "image-root", required_argument, NULL, ARG_IMAGE_ROOT }, - { "read-only", no_argument, NULL, ARG_READ_ONLY }, - { "direct", no_argument, NULL, ARG_DIRECT }, - { "btrfs-subvol", required_argument, NULL, ARG_BTRFS_SUBVOL }, - { "btrfs-quota", required_argument, NULL, ARG_BTRFS_QUOTA }, - { "convert-qcow2", required_argument, NULL, ARG_CONVERT_QCOW2 }, - { "sync", required_argument, NULL, ARG_SYNC }, - { "offset", required_argument, NULL, ARG_OFFSET }, - { "size-max", required_argument, NULL, ARG_SIZE_MAX }, - { "class", required_argument, NULL, ARG_CLASS }, - { "system", no_argument, NULL, ARG_SYSTEM }, - { "user", no_argument, NULL, ARG_USER }, - {} - }; - - int r, c; +static int parse_argv(int argc, char *argv[], char ***ret_args) { + int r; assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + 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_FORCE: + OPTION_LONG("force", NULL, "Force creation of image"): arg_import_flags |= IMPORT_FORCE; break; - case ARG_IMAGE_ROOT: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image_root); + OPTION_LONG("image-root", "PATH", "Image root directory"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_image_root); if (r < 0) return r; - break; - case ARG_READ_ONLY: + OPTION_LONG("read-only", NULL, "Create a read-only image"): arg_import_flags |= IMPORT_READ_ONLY; break; - case ARG_DIRECT: + OPTION_LONG("direct", NULL, "Import directly to specified file"): arg_import_flags |= IMPORT_DIRECT; break; - case ARG_BTRFS_SUBVOL: - r = parse_boolean_argument("--btrfs-subvol=", optarg, NULL); + OPTION_LONG("btrfs-subvol", "BOOL", + "Controls whether to create a btrfs subvolume instead of a directory"): + r = parse_boolean_argument("--btrfs-subvol=", arg, NULL); if (r < 0) return r; - SET_FLAG(arg_import_flags, IMPORT_BTRFS_SUBVOL, r); break; - case ARG_BTRFS_QUOTA: - r = parse_boolean_argument("--btrfs-quota=", optarg, NULL); + OPTION_LONG("btrfs-quota", "BOOL", + "Controls whether to set up quota for btrfs subvolume"): + r = parse_boolean_argument("--btrfs-quota=", arg, NULL); if (r < 0) return r; - SET_FLAG(arg_import_flags, IMPORT_BTRFS_QUOTA, r); break; - case ARG_CONVERT_QCOW2: - r = parse_boolean_argument("--convert-qcow2=", optarg, NULL); + OPTION_LONG("convert-qcow2", "BOOL", + "Controls whether to convert QCOW2 images to regular disk images"): + r = parse_boolean_argument("--convert-qcow2=", arg, NULL); if (r < 0) return r; - SET_FLAG(arg_import_flags, IMPORT_CONVERT_QCOW2, r); break; - case ARG_SYNC: - r = parse_boolean_argument("--sync=", optarg, NULL); + OPTION_LONG("sync", "BOOL", "Controls whether to sync() before completing"): + r = parse_boolean_argument("--sync=", arg, NULL); if (r < 0) return r; - SET_FLAG(arg_import_flags, IMPORT_SYNC, r); break; - case ARG_OFFSET: { + OPTION_LONG("offset", "BYTES", "Offset to seek to in destination"): { uint64_t u; - r = safe_atou64(optarg, &u); + r = safe_atou64(arg, &u); if (r < 0) - return log_error_errno(r, "Failed to parse --offset= argument: %s", optarg); + return log_error_errno(r, "Failed to parse --offset= argument: %s", arg); if (!FILE_SIZE_VALID(u)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Argument to --offset= switch too large: %s", optarg); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Argument to --offset= switch too large: %s", arg); arg_offset = u; break; } - case ARG_SIZE_MAX: { + OPTION_LONG("size-max", "BYTES", "Maximum number of bytes to write to destination"): { uint64_t u; - r = parse_size(optarg, 1024, &u); + r = parse_size(arg, 1024, &u); if (r < 0) - return log_error_errno(r, "Failed to parse --size-max= argument: %s", optarg); + return log_error_errno(r, "Failed to parse --size-max= argument: %s", arg); if (!FILE_SIZE_VALID(u)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Argument to --size-max= switch too large: %s", optarg); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Argument to --size-max= switch too large: %s", arg); arg_size_max = u; break; } - case ARG_CLASS: - arg_class = image_class_from_string(optarg); + OPTION_LONG("class", "CLASS", + "Select image class (machine, sysext, confext, portable)"): + arg_class = image_class_from_string(arg); if (arg_class < 0) - return log_error_errno(arg_class, "Failed to parse --class= argument: %s", optarg); - + return log_error_errno(arg_class, "Failed to parse --class= argument: %s", arg); break; - case ARG_SYSTEM: + OPTION_LONG("system", NULL, "Operate in per-system mode"): arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; break; - case ARG_USER: + OPTION_LONG("user", NULL, "Operate in per-user mode"): arg_runtime_scope = RUNTIME_SCOPE_USER; break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } /* Make sure offset+size is still in the valid range if both set */ @@ -476,20 +438,10 @@ static int parse_argv(int argc, char *argv[]) { if (arg_runtime_scope == RUNTIME_SCOPE_USER) arg_import_flags |= IMPORT_FOREIGN_UID; + *ret_args = option_parser_get_args(&state); return 1; } -static int import_main(int argc, char *argv[]) { - static const Verb verbs[] = { - { "help", VERB_ANY, VERB_ANY, 0, verb_help }, - { "tar", 2, 3, 0, verb_import_tar }, - { "raw", 2, 3, 0, verb_import_raw }, - {} - }; - - return dispatch_verb(argc, argv, verbs, NULL); -} - static void parse_env(void) { int r; @@ -523,13 +475,14 @@ static int run(int argc, char *argv[]) { parse_env(); - r = parse_argv(argc, argv); + char **args = NULL; + r = parse_argv(argc, argv, &args); if (r <= 0) return r; (void) ignore_signals(SIGPIPE); - return import_main(argc, argv); + return dispatch_verb_with_args(args, NULL); } DEFINE_MAIN_FUNCTION(run); diff --git a/src/import/importctl.c b/src/import/importctl.c index 2993073cc5..dceb03a6d6 100644 --- a/src/import/importctl.c +++ b/src/import/importctl.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include @@ -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; - break; - - case ARG_FORCE: - arg_import_flags |= IMPORT_FORCE; - arg_import_flags_mask |= IMPORT_FORCE; - 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); - - arg_format = optarg; - break; - - case ARG_JSON: - r = parse_json_argument(optarg, &arg_json_format_flags); + OPTION_COMMON_JSON: + r = parse_json_argument(arg, &arg_json_format_flags); if (r <= 0) return r; - arg_legend = false; break; - case 'j': + OPTION_COMMON_LOWERCASE_J: arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; arg_legend = false; break; - case ARG_CLASS: - arg_image_class = image_class_from_string(optarg); - if (arg_image_class < 0) - return log_error_errno(arg_image_class, "Failed to parse --class= parameter: %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); + + 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 'm': + 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; + + OPTION_LONG("force", NULL, "Install image even if already exists"): + arg_import_flags |= IMPORT_FORCE; + arg_import_flags_mask |= IMPORT_FORCE; + break; + + 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", arg); + break; + + 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': - arg_import_flags_mask &= ~IMPORT_PULL_KEEP_DOWNLOAD; + 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); diff --git a/src/import/pull.c b/src/import/pull.c index 270f70396c..10aa4c52bf 100644 --- a/src/import/pull.c +++ b/src/import/pull.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include @@ -11,6 +10,7 @@ #include "build.h" #include "discover-image.h" #include "env-util.h" +#include "format-table.h" #include "hexdecoct.h" #include "import-common.h" #include "import-util.h" @@ -19,6 +19,7 @@ #include "log.h" #include "main-func.h" #include "oci-util.h" +#include "options.h" #include "parse-argument.h" #include "parse-util.h" #include "path-util.h" @@ -117,7 +118,8 @@ static void on_tar_finished(TarPull *pull, int error, void *userdata) { sd_event_exit(event, ABS(error)); } -static int verb_pull_tar(int argc, char *argv[], uintptr_t _data, void *userdata) { +VERB(verb_tar, "tar", "URL [NAME]", 2, 3, 0, "Download a TAR image"); +static int verb_tar(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_free_ char *ll = NULL, *normalized = NULL; _cleanup_(sd_event_unrefp) sd_event *event = NULL; _cleanup_(tar_pull_unrefp) TarPull *pull = NULL; @@ -187,7 +189,8 @@ static void on_raw_finished(RawPull *pull, int error, void *userdata) { sd_event_exit(event, ABS(error)); } -static int verb_pull_raw(int argc, char *argv[], uintptr_t _data, void *userdata) { +VERB(verb_raw, "raw", "URL [NAME]", 2, 3, 0, "Download a RAW image"); +static int verb_raw(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_free_ char *ll = NULL, *normalized = NULL; _cleanup_(sd_event_unrefp) sd_event *event = NULL; _cleanup_(raw_pull_unrefp) RawPull *pull = NULL; @@ -256,7 +259,8 @@ static void on_oci_finished(OciPull *pull, int error, void *userdata) { sd_event_exit(event, ABS(error)); } -static int verb_pull_oci(int argc, char *argv[], uintptr_t _data, void *userdata) { +VERB(verb_oci, "oci", "REF [NAME]", 2, 3, 0, "Download an OCI image"); +static int verb_oci(int argc, char *argv[], uintptr_t _data, void *userdata) { int r; const char *ref = argv[1]; @@ -312,134 +316,79 @@ static int verb_pull_oci(int argc, char *argv[], uintptr_t _data, void *userdata } static int help(void) { - printf("%1$s [OPTIONS...] {COMMAND} ...\n" - "\n%4$sDownload disk images.%5$s\n" - "\n%2$sCommands:%3$s\n" - " tar URL [NAME] Download a TAR image\n" - " raw URL [NAME] Download a RAW image\n" - " oci REF [NAME] Download an OCI image\n" - "\n%2$sOptions:%3$s\n" - " -h --help Show this help\n" - " --version Show package version\n" - " --force Force creation of image\n" - " --verify=MODE Verify downloaded image, one of: 'no',\n" - " 'checksum', 'signature' or literal SHA256 hash\n" - " --settings=BOOL Download settings file with image\n" - " --roothash=BOOL Download root hash file with image\n" - " --roothash-signature=BOOL\n" - " Download root hash signature file with image\n" - " --verity=BOOL Download verity file with image\n" - " --image-root=PATH Image root directory\n" - " --read-only Create a read-only image\n" - " --direct Download directly to specified file\n" - " --btrfs-subvol=BOOL Controls whether to create a btrfs subvolume\n" - " instead of a directory\n" - " --btrfs-quota=BOOL Controls whether to set up quota for btrfs\n" - " subvolume\n" - " --convert-qcow2=BOOL Controls whether to convert QCOW2 images to\n" - " regular disk images\n" - " --sync=BOOL Controls whether to sync() before completing\n" - " --offset=BYTES Offset to seek to in destination\n" - " --size-max=BYTES Maximum number of bytes to write to destination\n" - " --class=CLASS Select image class (machine, sysext, confext,\n" - " portable)\n" - " --keep-download=BOOL Keep a copy pristine copy of the downloaded file\n" - " around\n" - " --system Operate in per-system mode\n" - " --user Operate in per-user mode\n", + _cleanup_(table_unrefp) Table *options = NULL, *verbs = 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); + + printf("%s [OPTIONS...] {COMMAND} ...\n\n" + "%sDownload disk images.%s\n" + "\n%sCommands:%s\n", program_invocation_short_name, - 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; + 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_argv(int argc, char *argv[]) { - enum { - ARG_VERSION = 0x100, - ARG_FORCE, - ARG_IMAGE_ROOT, - ARG_VERIFY, - ARG_SETTINGS, - ARG_ROOTHASH, - ARG_ROOTHASH_SIGNATURE, - ARG_VERITY, - ARG_READ_ONLY, - ARG_DIRECT, - ARG_BTRFS_SUBVOL, - ARG_BTRFS_QUOTA, - ARG_CONVERT_QCOW2, - ARG_SYNC, - ARG_OFFSET, - ARG_SIZE_MAX, - 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 }, - { "force", no_argument, NULL, ARG_FORCE }, - { "image-root", required_argument, NULL, ARG_IMAGE_ROOT }, - { "verify", required_argument, NULL, ARG_VERIFY }, - { "settings", required_argument, NULL, ARG_SETTINGS }, - { "roothash", required_argument, NULL, ARG_ROOTHASH }, - { "roothash-signature", required_argument, NULL, ARG_ROOTHASH_SIGNATURE }, - { "verity", required_argument, NULL, ARG_VERITY }, - { "read-only", no_argument, NULL, ARG_READ_ONLY }, - { "direct", no_argument, NULL, ARG_DIRECT }, - { "btrfs-subvol", required_argument, NULL, ARG_BTRFS_SUBVOL }, - { "btrfs-quota", required_argument, NULL, ARG_BTRFS_QUOTA }, - { "convert-qcow2", required_argument, NULL, ARG_CONVERT_QCOW2 }, - { "sync", required_argument, NULL, ARG_SYNC }, - { "offset", required_argument, NULL, ARG_OFFSET }, - { "size-max", required_argument, NULL, ARG_SIZE_MAX }, - { "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; bool auto_settings = true, auto_keep_download = true; assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + 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_FORCE: + OPTION_LONG("force", NULL, "Force creation of image"): arg_import_flags |= IMPORT_FORCE; break; - case ARG_IMAGE_ROOT: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image_root); + OPTION_LONG("image-root", "PATH", "Image root directory"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_image_root); if (r < 0) return r; - break; - case ARG_VERIFY: { + OPTION_LONG("verify", "MODE", + "Verify downloaded image, one of: 'no', 'checksum', 'signature' or literal SHA256 hash"): { ImportVerify v; - v = import_verify_from_string(optarg); + v = import_verify_from_string(arg); if (v < 0) { _cleanup_free_ void *h = NULL; size_t n; @@ -447,10 +396,10 @@ static int parse_argv(int argc, char *argv[]) { /* If this is not a valid verification mode, maybe it's a literally specified * SHA256 hash? We can handle that too... */ - r = unhexmem(optarg, &h, &n); + r = unhexmem(arg, &h, &n); if (r < 0 || n == 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Invalid verification setting: %s", optarg); + "Invalid verification setting: %s", arg); if (n != 32) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "64 hex character SHA256 hash required when specifying explicit checksum, %zu specified", n * 2); @@ -466,8 +415,8 @@ static int parse_argv(int argc, char *argv[]) { break; } - case ARG_SETTINGS: - r = parse_boolean_argument("--settings=", optarg, NULL); + OPTION_LONG("settings", "BOOL", "Download settings file with image"): + r = parse_boolean_argument("--settings=", arg, NULL); if (r < 0) return r; @@ -475,8 +424,8 @@ static int parse_argv(int argc, char *argv[]) { auto_settings = false; break; - case ARG_ROOTHASH: - r = parse_boolean_argument("--roothash=", optarg, NULL); + OPTION_LONG("roothash", "BOOL", "Download root hash file with image"): + r = parse_boolean_argument("--roothash=", arg, NULL); if (r < 0) return r; @@ -487,118 +436,113 @@ static int parse_argv(int argc, char *argv[]) { SET_FLAG(arg_import_flags, IMPORT_PULL_ROOTHASH_SIGNATURE, false); break; - case ARG_ROOTHASH_SIGNATURE: - r = parse_boolean_argument("--roothash-signature=", optarg, NULL); + OPTION_LONG("roothash-signature", "BOOL", + "Download root hash signature file with image"): + r = parse_boolean_argument("--roothash-signature=", arg, NULL); if (r < 0) return r; SET_FLAG(arg_import_flags, IMPORT_PULL_ROOTHASH_SIGNATURE, r); break; - case ARG_VERITY: - r = parse_boolean_argument("--verity=", optarg, NULL); + OPTION_LONG("verity", "BOOL", "Download verity file with image"): + r = parse_boolean_argument("--verity=", arg, NULL); if (r < 0) return r; SET_FLAG(arg_import_flags, IMPORT_PULL_VERITY, r); break; - case ARG_READ_ONLY: + OPTION_LONG("read-only", NULL, "Create a read-only image"): arg_import_flags |= IMPORT_READ_ONLY; break; - case ARG_DIRECT: + OPTION_LONG("direct", NULL, "Download directly to specified file"): arg_import_flags |= IMPORT_DIRECT; arg_import_flags &= ~(IMPORT_PULL_SETTINGS|IMPORT_PULL_ROOTHASH|IMPORT_PULL_ROOTHASH_SIGNATURE|IMPORT_PULL_VERITY); break; - case ARG_BTRFS_SUBVOL: - r = parse_boolean_argument("--btrfs-subvol=", optarg, NULL); + OPTION_LONG("btrfs-subvol", "BOOL", + "Controls whether to create a btrfs subvolume instead of a directory"): + r = parse_boolean_argument("--btrfs-subvol=", arg, NULL); if (r < 0) return r; - SET_FLAG(arg_import_flags, IMPORT_BTRFS_SUBVOL, r); break; - case ARG_BTRFS_QUOTA: - r = parse_boolean_argument("--btrfs-quota=", optarg, NULL); + OPTION_LONG("btrfs-quota", "BOOL", + "Controls whether to set up quota for btrfs subvolume"): + r = parse_boolean_argument("--btrfs-quota=", arg, NULL); if (r < 0) return r; - SET_FLAG(arg_import_flags, IMPORT_BTRFS_QUOTA, r); break; - case ARG_CONVERT_QCOW2: - r = parse_boolean_argument("--convert-qcow2=", optarg, NULL); + OPTION_LONG("convert-qcow2", "BOOL", + "Controls whether to convert QCOW2 images to regular disk images"): + r = parse_boolean_argument("--convert-qcow2=", arg, NULL); if (r < 0) return r; - SET_FLAG(arg_import_flags, IMPORT_CONVERT_QCOW2, r); break; - case ARG_SYNC: - r = parse_boolean_argument("--sync=", optarg, NULL); + OPTION_LONG("sync", "BOOL", "Controls whether to sync() before completing"): + r = parse_boolean_argument("--sync=", arg, NULL); if (r < 0) return r; - SET_FLAG(arg_import_flags, IMPORT_SYNC, r); break; - case ARG_OFFSET: { + OPTION_LONG("offset", "BYTES", "Offset to seek to in destination"): { uint64_t u; - r = safe_atou64(optarg, &u); + r = safe_atou64(arg, &u); if (r < 0) - return log_error_errno(r, "Failed to parse --offset= argument: %s", optarg); + return log_error_errno(r, "Failed to parse --offset= argument: %s", arg); if (!FILE_SIZE_VALID(u)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Argument to --offset= switch too large: %s", optarg); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Argument to --offset= switch too large: %s", arg); arg_offset = u; break; } - case ARG_SIZE_MAX: { + OPTION_LONG("size-max", "BYTES", "Maximum number of bytes to write to destination"): { uint64_t u; - r = parse_size(optarg, 1024, &u); + r = parse_size(arg, 1024, &u); if (r < 0) - return log_error_errno(r, "Failed to parse --size-max= argument: %s", optarg); + return log_error_errno(r, "Failed to parse --size-max= argument: %s", arg); if (!FILE_SIZE_VALID(u)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Argument to --size-max= switch too large: %s", optarg); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Argument to --size-max= switch too large: %s", arg); arg_size_max = u; break; } - case ARG_CLASS: - arg_class = image_class_from_string(optarg); + OPTION_LONG("class", "CLASS", + "Select image class (machine, sysext, confext, portable)"): + arg_class = image_class_from_string(arg); if (arg_class < 0) - return log_error_errno(arg_class, "Failed to parse --class= argument: %s", optarg); - + return log_error_errno(arg_class, "Failed to parse --class= argument: %s", arg); break; - case ARG_KEEP_DOWNLOAD: - r = parse_boolean(optarg); + OPTION_LONG("keep-download", "BOOL", + "Keep a pristine copy of the downloaded file around"): + r = parse_boolean(arg); if (r < 0) - return log_error_errno(r, "Failed to parse --keep-download= argument: %s", optarg); + return log_error_errno(r, "Failed to parse --keep-download= argument: %s", arg); SET_FLAG(arg_import_flags, IMPORT_PULL_KEEP_DOWNLOAD, r); auto_keep_download = false; break; - case ARG_SYSTEM: + OPTION_LONG("system", NULL, "Operate in per-system mode"): arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; break; - case ARG_USER: + OPTION_LONG("user", NULL, "Operate in per-user mode"): arg_runtime_scope = RUNTIME_SCOPE_USER; break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } /* Make sure offset+size is still in the valid range if both set */ @@ -631,6 +575,7 @@ static int parse_argv(int argc, char *argv[]) { if (arg_runtime_scope == RUNTIME_SCOPE_USER) arg_import_flags |= IMPORT_FOREIGN_UID; + *ret_args = option_parser_get_args(&state); return 1; } @@ -659,18 +604,6 @@ static void parse_env(void) { log_warning_errno(r, "Failed to parse $SYSTEMD_IMPORT_SYNC: %m"); } -static int pull_main(int argc, char *argv[]) { - static const Verb verbs[] = { - { "help", VERB_ANY, VERB_ANY, 0, verb_help }, - { "tar", 2, 3, 0, verb_pull_tar }, - { "raw", 2, 3, 0, verb_pull_raw }, - { "oci", 2, 3, 0, verb_pull_oci }, - {} - }; - - return dispatch_verb(argc, argv, verbs, NULL); -} - static int run(int argc, char *argv[]) { int r; @@ -679,13 +612,14 @@ static int run(int argc, char *argv[]) { parse_env(); - r = parse_argv(argc, argv); + char **args = NULL; + r = parse_argv(argc, argv, &args); if (r <= 0) return r; (void) ignore_signals(SIGPIPE); - return pull_main(argc, argv); + return dispatch_verb_with_args(args, NULL); } DEFINE_MAIN_FUNCTION(run); diff --git a/src/journal/bsod.c b/src/journal/bsod.c index 9a370af390..b314c08660 100644 --- a/src/journal/bsod.c +++ b/src/journal/bsod.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include #include @@ -11,10 +10,12 @@ #include "build.h" #include "fd-util.h" #include "fileio.h" +#include "format-table.h" #include "io-util.h" #include "log.h" #include "logs-show.h" #include "main-func.h" +#include "options.h" #include "parse-argument.h" #include "pretty-print.h" #include "qrcode-util.h" @@ -29,29 +30,32 @@ STATIC_DESTRUCTOR_REGISTER(arg_tty, freep); static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *options = NULL; int r; r = terminal_urlify_man("systemd-bsod", "8", &link); if (r < 0) return log_oom(); - printf("%1$s [OPTIONS...]\n\n" - "%5$sFilter the journal to fetch the first message from the current boot with an%6$s\n" - "%5$semergency log level and display it as a string and a QR code.%6$s\n" - "\n%3$sOptions:%4$s\n" - " -h --help Show this help\n" - " --version Show package version\n" - " -c --continuous Make systemd-bsod wait continuously\n" - " for changes in the journal\n" - " --tty=TTY Specify path to TTY to use\n" - "\nSee the %2$s for details.\n", + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + + printf("%s [OPTIONS...]\n\n" + "%sFilter the journal to fetch the first message from the current boot with an\n" + "emergency log level and display it as a string and a QR code.%s\n" + "\n%sOptions:%s\n", program_invocation_short_name, - link, - ansi_underline(), - ansi_normal(), ansi_highlight(), + ansi_normal(), + 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; } @@ -239,55 +243,35 @@ cleanup: return r; } -static int parse_argv(int argc, char * argv[]) { - - enum { - ARG_VERSION = 0x100, - ARG_TTY, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "continuous", no_argument, NULL, 'c' }, - { "tty", required_argument, NULL, ARG_TTY }, - {} - }; - - int c, r; - +static int parse_argv(int argc, char *argv[]) { assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "hc", options, NULL)) >= 0) + OptionParser state = { argc, argv }; + const char *arg; + int r; + 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 'c': + OPTION('c', "continuous", NULL, "Continuously wait for changes in the journal"): arg_continuous = true; break; - case ARG_TTY: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tty); + OPTION_LONG("tty", "TTY", "Specify path to TTY to use"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_tty); if (r < 0) return r; - break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } - if (optind < argc) + if (option_parser_get_n_args(&state) > 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "%s takes no argument.", program_invocation_short_name); diff --git a/src/journal/cat.c b/src/journal/cat.c index 76d36fce7e..e2419d36ab 100644 --- a/src/journal/cat.c +++ b/src/journal/cat.c @@ -1,7 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include -#include #include #include #include @@ -12,12 +11,15 @@ #include "build.h" #include "env-util.h" #include "fd-util.h" +#include "format-table.h" #include "format-util.h" #include "log.h" #include "main-func.h" +#include "options.h" #include "parse-argument.h" #include "pretty-print.h" #include "string-util.h" +#include "strv.h" #include "syslog-util.h" static const char *arg_identifier = NULL; @@ -28,104 +30,81 @@ static bool arg_level_prefix = true; static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *options = NULL; int r; r = terminal_urlify_man("systemd-cat", "1", &link); if (r < 0) return log_oom(); - printf("%s [OPTIONS...] COMMAND ...\n" - "\n%sExecute process with stdout/stderr connected to the journal.%s\n\n" - " -h --help Show this help\n" - " --version Show package version\n" - " -t --identifier=STRING Set syslog identifier\n" - " -p --priority=PRIORITY Set priority value (0..7)\n" - " --stderr-priority=PRIORITY Set priority value (0..7) used for stderr\n" - " --level-prefix=BOOL Control whether level prefix shall be parsed\n" - " --namespace=NAMESPACE Connect to specified journal namespace\n" - "\nSee the %s for details.\n", + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + + printf("%s [OPTIONS...] COMMAND ...\n\n" + "%sExecute process with stdout/stderr connected to the journal.%s\n\n", program_invocation_short_name, ansi_highlight(), - ansi_normal(), - link); + 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 parse_argv(int argc, char *argv[]) { - - enum { - ARG_VERSION = 0x100, - ARG_STDERR_PRIORITY, - ARG_LEVEL_PREFIX, - ARG_NAMESPACE, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "identifier", required_argument, NULL, 't' }, - { "priority", required_argument, NULL, 'p' }, - { "stderr-priority", required_argument, NULL, ARG_STDERR_PRIORITY }, - { "level-prefix", required_argument, NULL, ARG_LEVEL_PREFIX }, - { "namespace", required_argument, NULL, ARG_NAMESPACE }, - {} - }; - - int c, r; - +static int parse_argv(int argc, char *argv[], char ***ret_args) { assert(argc >= 0); assert(argv); - /* 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; - while ((c = getopt_long(argc, argv, "+ht:p:", options, NULL)) >= 0) + OptionParser state = { argc, argv, /* stop_at_first_nonoption= */ true }; + const char *arg; + int r; + FOREACH_OPTION(&state, c, &arg, /* on_error= */ return c) switch (c) { - case 'h': - help(); - return 0; + OPTION_COMMON_HELP: + return help(); - case ARG_VERSION: + OPTION_COMMON_VERSION: return version(); - case 't': - arg_identifier = empty_to_null(optarg); + OPTION('t', "identifier", "STRING", "Set syslog identifier"): + arg_identifier = empty_to_null(arg); break; - case 'p': - arg_priority = log_level_from_string(optarg); + OPTION('p', "priority", "PRIORITY", "Set priority value (0..7)"): + arg_priority = log_level_from_string(arg); if (arg_priority < 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse priority value."); break; - case ARG_STDERR_PRIORITY: - arg_stderr_priority = log_level_from_string(optarg); + OPTION_LONG("stderr-priority", "PRIORITY", + "Set priority value (0..7) used for stderr"): + arg_stderr_priority = log_level_from_string(arg); if (arg_stderr_priority < 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse stderr priority value."); break; - case ARG_LEVEL_PREFIX: - r = parse_boolean_argument("--level-prefix=", optarg, &arg_level_prefix); + OPTION_LONG("level-prefix", "BOOL", + "Control whether level prefix shall be parsed"): + r = parse_boolean_argument("--level-prefix=", arg, &arg_level_prefix); if (r < 0) return r; break; - case ARG_NAMESPACE: - arg_namespace = empty_to_null(optarg); + OPTION_LONG("namespace", "NAMESPACE", + "Connect to specified journal namespace"): + arg_namespace = empty_to_null(arg); break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } + *ret_args = option_parser_get_args(&state); return 1; } @@ -135,7 +114,8 @@ static int run(int argc, char *argv[]) { log_setup(); - r = parse_argv(argc, argv); + char **args = NULL; + r = parse_argv(argc, argv, &args); if (r <= 0) return r; @@ -157,7 +137,7 @@ static int run(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to rearrange stdout/stderr: %m"); - if (argc <= optind) + if (strv_isempty(args)) (void) execlp("cat", "cat", NULL); else { struct stat st; @@ -171,7 +151,7 @@ static int run(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to set environment variable JOURNAL_STREAM: %m"); - (void) execvp(argv[optind], argv + optind); + (void) execvp(args[0], args); } r = -errno; diff --git a/src/shared/options.c b/src/shared/options.c index e5206e641d..dc2fb34cf6 100644 --- a/src/shared/options.c +++ b/src/shared/options.c @@ -21,8 +21,9 @@ static bool option_arg_required(const Option *opt) { static bool option_is_metadata(const Option *opt) { /* A metadata entry that is not a real option, like the group marker */ - return FLAGS_SET(ASSERT_PTR(opt)->flags, OPTION_GROUP_MARKER) || - FLAGS_SET(ASSERT_PTR(opt)->flags, OPTION_HELP_ENTRY); + return ASSERT_PTR(opt)->flags & (OPTION_GROUP_MARKER | + OPTION_HELP_ENTRY | + OPTION_HELP_ENTRY_VERBATIM); } static void shift_arg(char* argv[], int target, int source) { @@ -313,30 +314,38 @@ int _option_parser_get_help_table( /* No help string — we do not show the option */ continue; - char sc[3] = " "; - if (opt->short_code != 0) - xsprintf(sc, "-%c", opt->short_code); + _cleanup_free_ char *s = NULL; - /* We indent the option string by two spaces. We could set the minimum cell width and - * right-align for a similar result, but that'd be more work. This is only used for - * display. - * - * "=" is shown only when a long option is defined: -l --long=ARG, --long=ARG, -s ARG. - */ - bool need_eq = option_takes_arg(opt) && opt->long_code; - bool need_quote = opt->metavar && strchr(opt->metavar, ' '); - _cleanup_free_ char *s = strjoin( - " ", - sc, - " ", - opt->long_code ? "--" : "", - strempty(opt->long_code), - option_arg_optional(opt) ? "[" : "", - need_eq ? "=" : "", - need_quote ? "'" : "", - strempty(opt->metavar), - need_quote ? "'" : "", - option_arg_optional(opt) ? "]" : ""); + if (FLAGS_SET(opt->flags, OPTION_HELP_ENTRY_VERBATIM)) { + assert(opt->long_code); + + s = strjoin(" ", + opt->long_code); + } else { + char sc[3] = " "; + if (opt->short_code != 0) + xsprintf(sc, "-%c", opt->short_code); + + /* We indent the option string by two spaces. We could set the minimum cell width and + * right-align for a similar result, but that'd be more work. This is only used for + * display. + * + * "=" is shown only when a long option is defined: -l --long=ARG, --long=ARG, -s ARG. + */ + bool need_eq = option_takes_arg(opt) && opt->long_code; + bool need_quote = opt->metavar && strchr(opt->metavar, ' '); + s = strjoin(" ", + sc, + " ", + opt->long_code ? "--" : "", + strempty(opt->long_code), + option_arg_optional(opt) ? "[" : "", + need_eq ? "=" : "", + need_quote ? "'" : "", + strempty(opt->metavar), + need_quote ? "'" : "", + option_arg_optional(opt) ? "]" : ""); + } if (!s) return log_oom(); diff --git a/src/shared/options.h b/src/shared/options.h index e834cbeded..e8256059d1 100644 --- a/src/shared/options.h +++ b/src/shared/options.h @@ -5,10 +5,11 @@ #include "shared-forward.h" typedef enum OptionFlags { - OPTION_OPTIONAL_ARG = 1U << 0, /* Same as optional_argument in getopt */ - OPTION_STOPS_PARSING = 1U << 1, /* This option acts like "--" */ - OPTION_GROUP_MARKER = 1U << 2, /* Fake option entry to separate groups */ - OPTION_HELP_ENTRY = 1U << 3, /* Fake option entry to insert an additional help line */ + OPTION_OPTIONAL_ARG = 1U << 0, /* Same as optional_argument in getopt */ + OPTION_STOPS_PARSING = 1U << 1, /* This option acts like "--" */ + OPTION_GROUP_MARKER = 1U << 2, /* Fake option entry to separate groups */ + OPTION_HELP_ENTRY = 1U << 3, /* Fake option entry to insert an additional help line */ + OPTION_HELP_ENTRY_VERBATIM = 1U << 4, /* Same, but use the long_code in the first column as written */ } OptionFlags; typedef struct Option { @@ -49,6 +50,7 @@ typedef struct Option { #define OPTION_LONG_FLAGS(fl, lc, mv, h) OPTION_FULL(fl, /* sc= */ 0, lc, mv, h) #define OPTION_SHORT(sc, mv, h) OPTION(sc, /* lc= */ NULL, mv, h) #define OPTION_SHORT_FLAGS(fl, sc, mv, h) OPTION_FULL(fl, sc, /* lc= */ NULL, mv, h) +#define OPTION_HELP_VERBATIM(lc, h) OPTION_FULL(OPTION_HELP_ENTRY_VERBATIM, /* sc= */ 0, lc, /* mv= */ NULL, h) #define OPTION_COMMON_HELP \ OPTION('h', "help", NULL, "Show this help") diff --git a/src/shared/verbs.h b/src/shared/verbs.h index 51a9e3a525..4fb0486f75 100644 --- a/src/shared/verbs.h +++ b/src/shared/verbs.h @@ -39,19 +39,27 @@ typedef struct { .help = h, \ } -#define VERB_FULL(d, v, a, amin, amax, f, dat, h) \ +/* Forward-define function d. scope specifies the scope, e.g. static. */ +#define VERB_SCOPE_FULL(scope, d, v, a, amin, amax, f, dat, h) \ DISABLE_WARNING_REDUNDANT_DECLS \ - static int d(int, char**, uintptr_t, void*); \ + scope int d(int, char**, uintptr_t, void*); \ REENABLE_WARNING \ _VERB_DATA(d, v, a, amin, amax, f, dat, h) +/* The same as VERB_SCOPE_FULL with scope hardwired to 'static'. */ +#define VERB_FULL(d, v, a, amin, amax, f, dat, h) \ + VERB_SCOPE_FULL(static, d, v, a, amin, amax, f, dat, h) -/* The same as VERB_FULL, but without the data argument */ +/* The same as VERB_SCOPE_FULL/VERB_FULL, but without the data argument. */ +#define VERB_SCOPE(scope, d, v, a, amin, amax, f, h) \ + VERB_SCOPE_FULL(scope, d, v, a, amin, amax, f, /* dat= */ 0, h) #define VERB(d, v, a, amin, amax, f, h) \ - VERB_FULL(d, v, a, amin, amax, f, /* dat= */ 0, h) + VERB_SCOPE(static, d, v, a, amin, amax, f, h) -/* Simplified VERB for parameters that take no argument */ +/* Simplified VERB_SCOPE/VERB for verbs that take no argument. */ +#define VERB_SCOPE_NOARG(scope, d, v, h) \ + VERB_SCOPE(scope, d, v, /* a= */ NULL, /* amin= */ VERB_ANY, /* amax= */ 1, /* f= */ 0, h) #define VERB_NOARG(d, v, h) \ - VERB(d, v, /* a= */ NULL, /* amin= */ VERB_ANY, /* amax= */ 1, /* f= */ 0, h) + VERB_SCOPE_NOARG(static, d, v, h) /* Magic entry in the table (which will not be returned) that designates the start of the group . * The macro works as a separator between groups and must be between other VERB* stanzas. */