From 9a3a861ff8c56f85d86b2276917d5bc7a1a331fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 23 Apr 2026 11:09:11 +0200 Subject: [PATCH 01/15] measure: fix oom check Pointed out in review. --- src/measure/measure-tool.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/measure/measure-tool.c b/src/measure/measure-tool.c index 6a2e2994f9..4abb77aeb8 100644 --- a/src/measure/measure-tool.c +++ b/src/measure/measure-tool.c @@ -1096,10 +1096,8 @@ static int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) return log_error_errno(r, "Failed to decode PCR value '%s': %m", s); if (!sd_json_format_enabled(arg_json_format_flags)) { - _cleanup_free_ char *f = NULL; - - f = hexmem(h, l); - if (!h) + _cleanup_free_ char *f = hexmem(h, l); + if (!f) return log_oom(); if (bank == arg_banks) { From 8cb4ff428e5f84551e578017bbb3a87893b1dd67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 23 Apr 2026 10:10:31 +0200 Subject: [PATCH 02/15] shared/options: add a 'data' parameter to options This mirrors a similar field in Verb. In some cases it convenient to pass a fixed value to the parser. --- src/shared/options.h | 11 ++++++++--- src/test/test-options.c | 12 ++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/shared/options.h b/src/shared/options.h index cd9f64fd49..974c21d16a 100644 --- a/src/shared/options.h +++ b/src/shared/options.h @@ -19,10 +19,11 @@ typedef struct Option { char short_code; const char *long_code; const char *metavar; + uintptr_t data; const char *help; } Option; -#define _OPTION(counter, fl, sc, lc, mv, h) \ +#define _OPTION(counter, fl, sc, lc, mv, d, h) \ _section_("SYSTEMD_OPTIONS") \ _alignptr_ \ _used_ \ @@ -35,6 +36,7 @@ typedef struct Option { .short_code = sc, \ .long_code = lc, \ .metavar = mv, \ + .data = d, \ .help = h, \ }; \ case (0x100 + counter) @@ -43,14 +45,17 @@ typedef struct Option { * The define is structured as 'case' so that it can be followed by ':' and indented appropriately. */ #define OPTION_GROUP(gr) \ - _OPTION(__COUNTER__, OPTION_GROUP_MARKER, /* sc= */ 0, /* lc= */ gr, /* mv= */ NULL, /* h= */ NULL) + _OPTION(__COUNTER__, OPTION_GROUP_MARKER, /* sc= */ 0, /* lc= */ gr, /* mv= */ NULL, /* d= */ 0u, /* h= */ NULL) -#define OPTION_FULL(fl, sc, lc, mv, h) _OPTION(__COUNTER__, fl, sc, lc, mv, h) +#define OPTION_FULL_DATA(fl, sc, lc, mv, d, h) _OPTION(__COUNTER__, fl, sc, lc, mv, d, h) +#define OPTION_FULL(fl, sc, lc, mv, h) OPTION_FULL_DATA(fl, sc, lc, mv, /* d= */ 0u, h) #define OPTION(sc, lc, mv, h) OPTION_FULL(/* fl= */ 0, sc, lc, mv, h) #define OPTION_LONG(lc, mv, h) OPTION(/* sc= */ 0, lc, mv, h) #define OPTION_LONG_FLAGS(fl, lc, mv, h) OPTION_FULL(fl, /* sc= */ 0, lc, mv, h) +#define OPTION_LONG_DATA(lc, mv, d, h) OPTION_FULL_DATA(/* fl= */ 0, /* sc= */ 0, lc, mv, d, 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_SHORT_DATA(sc, mv, d, h) OPTION_FULL_DATA(/* fl= */ 0, sc, /* lc= */ NULL, mv, d, h) #define OPTION_POSITIONAL OPTION_FULL(OPTION_POSITIONAL_ENTRY, /* sc= */ 0, "(positional)", /* mv= */ NULL, /* h= */ NULL) #define OPTION_HELP_VERBATIM(lc, h) OPTION_FULL(OPTION_HELP_ENTRY_VERBATIM, /* sc= */ 0, lc, /* mv= */ NULL, h) diff --git a/src/test/test-options.c b/src/test/test-options.c index 05fd4bcbe0..687bafd4a2 100644 --- a/src/test/test-options.c +++ b/src/test/test-options.c @@ -772,6 +772,12 @@ static void test_macros_parse_one( if (entries[i].short_code != 0) ASSERT_EQ(opt->short_code, entries[i].short_code); ASSERT_TRUE(streq_ptr(arg, entries[i].argument)); + + if (streq_ptr(entries[i].long_code, "optional2")) + ASSERT_EQ(opt->data, 666u); + else + ASSERT_EQ(opt->data, 0u); + i++; switch (c) { @@ -796,6 +802,10 @@ static void test_macros_parse_one( OPTION_FULL(OPTION_OPTIONAL_ARG, 'o', "optional", "ARG", "Optional arg option"): break; + /* OPTION_FULL_DATA: optional arg */ + OPTION_FULL_DATA(OPTION_OPTIONAL_ARG, 'O', "optional2", "ARG", 666, "Optional arg option"): + break; + /* OPTION_FULL: stops parsing */ OPTION_FULL(OPTION_STOPS_PARSING, 0, "exec", NULL, "Stop parsing after this"): break; @@ -1015,6 +1025,7 @@ TEST(option_macros) { "-v", "--required=rval", "--optional=oval", + "--optional2=oval", "--debug", "pos2", "-o", @@ -1025,6 +1036,7 @@ TEST(option_macros) { { .short_code = 'v' }, { "required", "rval" }, { "optional", "oval" }, + { "optional2", "oval" }, { "debug" }, { "optional", NULL }, { "help" }, From f6583875057bd2f1e53a4fc00f6e8f817b0931bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 22 Apr 2026 14:37:28 +0200 Subject: [PATCH 03/15] test-chase-manual: convert to the new option parser --help now has help strings. --no_autofs is renamed to --no-autofs. Co-developed-by: Claude Opus 4.7 --- src/test/test-chase-manual.c | 108 ++++++++++++++++------------------- 1 file changed, 50 insertions(+), 58 deletions(-) diff --git a/src/test/test-chase-manual.c b/src/test/test-chase-manual.c index 376cd12c42..0e760aeb89 100644 --- a/src/test/test-chase-manual.c +++ b/src/test/test-chase-manual.c @@ -1,84 +1,75 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include "chase.h" #include "fd-util.h" +#include "format-table.h" #include "log.h" #include "main-func.h" +#include "options.h" +#include "strv.h" #include "tests.h" -static char *arg_root = NULL; +static const char *arg_root = NULL; static int arg_flags = 0; static bool arg_open = false; -static int parse_argv(int argc, char *argv[]) { - enum { - ARG_ROOT = 0x1000, - ARG_OPEN, - }; +static int help(void) { + _cleanup_(table_unrefp) Table *options = NULL; + int r; - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "root", required_argument, NULL, ARG_ROOT }, - { "open", no_argument, NULL, ARG_OPEN }, + r = option_parser_get_help_table(&options); + if (r < 0) + return r; - { "prefix-root", no_argument, NULL, CHASE_PREFIX_ROOT }, - { "nonexistent", no_argument, NULL, CHASE_NONEXISTENT }, - { "no_autofs", no_argument, NULL, CHASE_NO_AUTOFS }, - { "trigger-autofs", no_argument, NULL, CHASE_TRIGGER_AUTOFS }, - { "safe", no_argument, NULL, CHASE_SAFE }, - { "trail-slash", no_argument, NULL, CHASE_TRAIL_SLASH }, - { "step", no_argument, NULL, CHASE_STEP }, - { "nofollow", no_argument, NULL, CHASE_NOFOLLOW }, - { "warn", no_argument, NULL, CHASE_WARN }, - {} - }; + printf("%s [OPTIONS...] path...\n" + "\nExercise chase() function on specified paths.\n\n", + program_invocation_short_name); - int c; + r = table_print_or_warn(options); + if (r < 0) + return r; - assert_se(argc >= 0); - assert_se(argv); + return 0; +} - while ((c = getopt_long(argc, argv, "", options, NULL)) >= 0) +static int parse_argv(int argc, char *argv[], char ***ret_args) { + assert(argc >= 0); + assert(argv); + assert(ret_args); + + OptionParser state = { argc, argv }; + const Option *opt; + const char *arg; + + FOREACH_OPTION_FULL(&state, c, &opt, &arg, /* on_error= */ return c) switch (c) { - case 'h': - printf("Syntax:\n" - " %s [OPTION...] path...\n" - "Options:\n" - , argv[0]); - FOREACH_ARRAY(option, options, ELEMENTSOF(options) - 1) - printf(" --%s\n", option->name); - return 0; + OPTION_COMMON_HELP: + return help(); - case ARG_ROOT: - arg_root = optarg; + OPTION_LONG("root", "PATH", "Operate below specified root directory"): + arg_root = arg; break; - case ARG_OPEN: + OPTION_LONG("open", NULL, "Open the resolved path"): arg_open = true; break; - case CHASE_PREFIX_ROOT: - case CHASE_NONEXISTENT: - case CHASE_NO_AUTOFS: - case CHASE_TRIGGER_AUTOFS: - case CHASE_SAFE: - case CHASE_TRAIL_SLASH: - case CHASE_STEP: - case CHASE_NOFOLLOW: - case CHASE_WARN: - arg_flags |= c; + OPTION_LONG_DATA("prefix-root", NULL, CHASE_PREFIX_ROOT, "Prefix path with --root"): {} + OPTION_LONG_DATA("nonexistent", NULL, CHASE_NONEXISTENT, "Allow path to not exist"): {} + OPTION_LONG_DATA("no-autofs", NULL, CHASE_NO_AUTOFS, "Return -EREMOTE if autofs mount point found"): {} + OPTION_LONG_DATA("trigger-autofs", NULL, CHASE_TRIGGER_AUTOFS, "Trigger autofs mounts"): {} + OPTION_LONG_DATA("safe", NULL, CHASE_SAFE, "Refuse privilege boundary crossings"): {} + OPTION_LONG_DATA("trail-slash", NULL, CHASE_TRAIL_SLASH, "Preserve trailing slash"): {} + OPTION_LONG_DATA("step", NULL, CHASE_STEP, "Execute a single normalization step"): {} + OPTION_LONG_DATA("nofollow", NULL, CHASE_NOFOLLOW, "Do not follow the path's right-most component"): {} + OPTION_LONG_DATA("warn", NULL, CHASE_WARN, "Emit a warning on error"): + arg_flags |= opt->data; break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } - if (optind == argc) + *ret_args = option_parser_get_args(&state); + if (strv_isempty(*ret_args)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "At least one argument is required."); return 1; @@ -89,18 +80,19 @@ static int run(int argc, char **argv) { test_setup_logging(LOG_DEBUG); - r = parse_argv(argc, argv); + char **args = NULL; + r = parse_argv(argc, argv, &args); if (r <= 0) return r; - for (int i = optind; i < argc; i++) { + STRV_FOREACH(a, args) { _cleanup_free_ char *p = NULL; _cleanup_close_ int fd = -EBADF; - printf("%s ", argv[i]); + printf("%s ", *a); fflush(stdout); - r = chase(argv[i], arg_root, arg_flags, &p, arg_open ? &fd : NULL); + r = chase(*a, arg_root, arg_flags, &p, arg_open ? &fd : NULL); if (r < 0) log_error_errno(r, "failed: %m"); else { From 663a831089af0373d01e6718f2ef84244f4093f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 17 Apr 2026 10:26:02 +0200 Subject: [PATCH 04/15] stdio-bridge: convert to the new option parser Co-developed-by: Claude Opus 4.7 --- src/stdio-bridge/stdio-bridge.c | 79 +++++++++++++-------------------- 1 file changed, 31 insertions(+), 48 deletions(-) diff --git a/src/stdio-bridge/stdio-bridge.c b/src/stdio-bridge/stdio-bridge.c index 52c87559a4..42f365ad3e 100644 --- a/src/stdio-bridge/stdio-bridge.c +++ b/src/stdio-bridge/stdio-bridge.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include @@ -11,9 +10,11 @@ #include "bus-internal.h" #include "bus-util.h" #include "errno-util.h" +#include "format-table.h" #include "io-util.h" #include "log.h" #include "main-func.h" +#include "options.h" #include "parse-argument.h" #include "time-util.h" @@ -23,84 +24,66 @@ static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; static bool arg_quiet = false; static int help(void) { - printf("%s [OPTIONS...]\n\n" - "Forward messages between a pipe or socket and a D-Bus bus.\n\n" - " -h --help Show this help\n" - " --version Show package version\n" - " -p --bus-path=PATH Path to the bus address (default: %s)\n" - " --system Connect to system bus\n" - " --user Connect to user bus\n" - " -M --machine=CONTAINER Name of local container to connect to\n" - " -q --quiet Fail silently instead of logging errors\n", - program_invocation_short_name, DEFAULT_SYSTEM_BUS_ADDRESS); + _cleanup_(table_unrefp) Table *options = NULL; + int r; + + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + + printf("%s [OPTIONS...]\n" + "\nForward messages between a pipe or socket and a D-Bus bus.\n\n", + program_invocation_short_name); + + r = table_print_or_warn(options); + if (r < 0) + return r; return 0; } static int parse_argv(int argc, char *argv[]) { - - enum { - ARG_VERSION = 0x100, - ARG_USER, - ARG_SYSTEM, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "bus-path", required_argument, NULL, 'p' }, - { "user", no_argument, NULL, ARG_USER }, - { "system", no_argument, NULL, ARG_SYSTEM }, - { "machine", required_argument, NULL, 'M' }, - { "quiet", no_argument, NULL, 'q' }, - {}, - }; - - int c, r; - assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "hp:M:", 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_USER: + OPTION_LONG("user", NULL, "Connect to user bus"): arg_runtime_scope = RUNTIME_SCOPE_USER; break; - case ARG_SYSTEM: + OPTION_LONG("system", NULL, "Connect to system bus"): arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; break; - case 'p': - arg_bus_path = optarg; + OPTION('p', "bus-path", "PATH", + "Path to the bus address (default: " DEFAULT_SYSTEM_BUS_ADDRESS ")"): + arg_bus_path = arg; break; - case 'M': - r = parse_machine_argument(optarg, &arg_bus_path, &arg_transport); + OPTION_COMMON_MACHINE: + r = parse_machine_argument(arg, &arg_bus_path, &arg_transport); if (r < 0) return r; break; - case 'q': + OPTION('q', "quiet", NULL, "Fail silently instead of logging errors"): arg_quiet = true; break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } - if (argc > optind) + if (option_parser_get_n_args(&state) > 0) return log_full_errno(arg_quiet ? LOG_DEBUG : LOG_ERR, SYNTHETIC_ERRNO(EINVAL), "%s takes no arguments.", program_invocation_short_name); From 108f6045cc94b6ee6821e6762ecc6640e5aba13d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 23 Apr 2026 10:17:52 +0200 Subject: [PATCH 05/15] shutdown: convert to the new option parser Co-developed-by: Claude Opus 4.7 --- src/shutdown/shutdown.c | 96 +++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 56 deletions(-) diff --git a/src/shutdown/shutdown.c b/src/shutdown/shutdown.c index 1fb0f422e5..5e6ff9f68f 100644 --- a/src/shutdown/shutdown.c +++ b/src/shutdown/shutdown.c @@ -3,7 +3,6 @@ Copyright © 2010 ProFUSION embedded systems ***/ -#include #include #include #include @@ -29,10 +28,10 @@ #include "fd-util.h" #include "fileio.h" #include "format-util.h" -#include "getopt-defs.h" #include "initrd-util.h" #include "killall.h" #include "log.h" +#include "options.h" #include "parse-util.h" #include "pidref.h" #include "printk-util.h" @@ -58,103 +57,88 @@ static uint8_t arg_exit_code = 0; static usec_t arg_timeout = DEFAULT_TIMEOUT_USEC; static int parse_argv(int argc, char *argv[]) { - enum { - COMMON_GETOPT_ARGS, - SHUTDOWN_GETOPT_ARGS, - }; - - static const struct option options[] = { - COMMON_GETOPT_OPTIONS, - SHUTDOWN_GETOPT_OPTIONS, - {} - }; - - int c, r; - assert(argc >= 1); 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; + /* The interface is: the verb must stay in argv[1]. Any extra positional arguments + * are warned about and ignored. See 4b5d8d0f22ae61ceb45a25391354ba53b43ee992. */ - /* "-" prevents getopt from permuting argv[] and moving the verb away - * from argv[1]. Our interface to initrd promises it'll be there. */ - while ((c = getopt_long(argc, argv, "-", options, NULL)) >= 0) + OptionParser state = { argc, argv, OPTION_PARSER_RETURN_POSITIONAL_ARGS }; + const char *arg; + int r; + + FOREACH_OPTION(&state, c, &arg, /* on_error= */ return c) switch (c) { - case ARG_LOG_LEVEL: - r = log_set_max_level_from_string(optarg); + OPTION_COMMON_LOG_LEVEL: + r = log_set_max_level_from_string(arg); if (r < 0) - log_warning_errno(r, "Failed to parse log level %s, ignoring: %m", optarg); + log_warning_errno(r, "Failed to parse log level %s, ignoring: %m", arg); break; - case ARG_LOG_TARGET: - r = log_set_target_from_string(optarg); + OPTION_COMMON_LOG_TARGET: + r = log_set_target_from_string(arg); if (r < 0) - log_warning_errno(r, "Failed to parse log target %s, ignoring: %m", optarg); + log_warning_errno(r, "Failed to parse log target %s, ignoring: %m", arg); break; - case ARG_LOG_COLOR: - - if (optarg) { - r = log_show_color_from_string(optarg); + OPTION_LONG_FLAGS(OPTION_OPTIONAL_ARG, "log-color", "BOOL", + "Highlight important messages"): + if (arg) { + r = log_show_color_from_string(arg); if (r < 0) - log_warning_errno(r, "Failed to parse log color setting %s, ignoring: %m", optarg); + log_warning_errno(r, "Failed to parse log color setting %s, ignoring: %m", arg); } else log_show_color(true); break; - case ARG_LOG_LOCATION: - if (optarg) { - r = log_show_location_from_string(optarg); + OPTION_LONG_FLAGS(OPTION_OPTIONAL_ARG, "log-location", "BOOL", + "Include code location in messages"): + if (arg) { + r = log_show_location_from_string(arg); if (r < 0) - log_warning_errno(r, "Failed to parse log location setting %s, ignoring: %m", optarg); + log_warning_errno(r, "Failed to parse log location setting %s, ignoring: %m", arg); } else log_show_location(true); break; - case ARG_LOG_TIME: - - if (optarg) { - r = log_show_time_from_string(optarg); + OPTION_LONG_FLAGS(OPTION_OPTIONAL_ARG, "log-time", "BOOL", + "Prefix messages with current time"): + if (arg) { + r = log_show_time_from_string(arg); if (r < 0) - log_warning_errno(r, "Failed to parse log time setting %s, ignoring: %m", optarg); + log_warning_errno(r, "Failed to parse log time setting %s, ignoring: %m", arg); } else log_show_time(true); break; - case ARG_EXIT_CODE: - r = safe_atou8(optarg, &arg_exit_code); + OPTION_LONG("exit-code", "N", + "Exit code for reboot/kexec"): + r = safe_atou8(arg, &arg_exit_code); if (r < 0) - log_warning_errno(r, "Failed to parse exit code %s, ignoring: %m", optarg); + log_warning_errno(r, "Failed to parse exit code %s, ignoring: %m", arg); break; - case ARG_TIMEOUT: - r = parse_sec(optarg, &arg_timeout); + OPTION_LONG("timeout", "TIME", + "Overall shutdown timeout"): + r = parse_sec(arg, &arg_timeout); if (r < 0) - log_warning_errno(r, "Failed to parse shutdown timeout %s, ignoring: %m", optarg); + log_warning_errno(r, "Failed to parse shutdown timeout %s, ignoring: %m", arg); break; - case '\001': + OPTION_POSITIONAL: if (!arg_verb) - arg_verb = optarg; + arg_verb = arg; else - log_warning("Got extraneous arguments, ignoring."); + log_warning("Got extraneous argument, ignoring."); break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } if (!arg_verb) From 39c5bee7c12f3de32953b24d56e681ba2d2db0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 23 Apr 2026 10:34:42 +0200 Subject: [PATCH 06/15] network-generator: convert to the new option parser --help is the same except for whitespace. Co-developed-by: Claude Opus 4.7 --- .../generator/network-generator-main.c | 64 +++++++++---------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/src/network/generator/network-generator-main.c b/src/network/generator/network-generator-main.c index d64f65bc44..f9acf40500 100644 --- a/src/network/generator/network-generator-main.c +++ b/src/network/generator/network-generator-main.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include "alloc-util.h" @@ -8,15 +7,18 @@ #include "creds-util.h" #include "errno-util.h" #include "fd-util.h" +#include "format-table.h" #include "fs-util.h" #include "generator.h" #include "log.h" #include "main-func.h" #include "mkdir.h" #include "network-generator.h" +#include "options.h" #include "path-util.h" #include "proc-cmdline.h" #include "string-util.h" +#include "strv.h" #define NETWORK_UNIT_DIRECTORY "/run/systemd/network/" @@ -148,52 +150,47 @@ static int context_save(Context *context) { } static int help(void) { - printf("%s [OPTIONS...] [-- KERNEL_CMDLINE]\n" - " -h --help Show this help\n" - " --version Show package version\n" - " --root=PATH Operate on an alternate filesystem root\n", + _cleanup_(table_unrefp) Table *options = NULL; + int r; + + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + + printf("%s [OPTIONS...] [-- KERNEL_CMDLINE]\n\n", program_invocation_short_name); + r = table_print_or_warn(options); + if (r < 0) + return r; + return 0; } -static int parse_argv(int argc, char *argv[]) { - enum { - ARG_VERSION = 0x100, - ARG_ROOT, - }; - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "root", required_argument, NULL, ARG_ROOT }, - {}, - }; - int c; - +static int parse_argv(int argc, char *argv[], char ***ret_args) { assert(argc >= 0); assert(argv); + assert(ret_args); - 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_ROOT: - arg_root = optarg; + OPTION_LONG("root", "PATH", + "Operate on an alternate filesystem root"): + arg_root = arg; break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } + *ret_args = option_parser_get_args(&state); return 1; } @@ -205,20 +202,21 @@ 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; - if (optind >= argc) { + if (strv_isempty(args)) { r = proc_cmdline_parse(parse_cmdline_item, &context, 0); if (r < 0) return log_warning_errno(r, "Failed to parse kernel command line: %m"); } else { - for (int i = optind; i < argc; i++) { + STRV_FOREACH(a, args) { _cleanup_free_ char *word = NULL; char *value; - word = strdup(argv[i]); + word = strdup(*a); if (!word) return log_oom(); From b7f2c23da97f9a6e622886dd5a67ea78d8963aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 22 Apr 2026 15:09:45 +0200 Subject: [PATCH 07/15] test-libudev: convert to the new option parser The program now has a proper --help output. (Not on purpose. It's just easier to do same thing as everywhere else.) Co-developed-by: Claude Opus 4.7 --- src/libudev/test-libudev.c | 74 ++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/src/libudev/test-libudev.c b/src/libudev/test-libudev.c index f15cbc3a91..48d5ac4d9d 100644 --- a/src/libudev/test-libudev.c +++ b/src/libudev/test-libudev.c @@ -1,15 +1,16 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include #include "devnum-util.h" #include "fd-util.h" +#include "format-table.h" #include "libudev-list-internal.h" #include "log.h" #include "main-func.h" #include "libudev-util.h" +#include "options.h" #include "string-util.h" #include "tests.h" #include "version.h" @@ -404,52 +405,57 @@ static void test_list(void) { assert_se(!udev_list_entry_get_by_name(e, "ccc")); } -static int parse_args(int argc, char *argv[], const char **syspath, const char **subsystem) { - static const struct option options[] = { - { "syspath", required_argument, NULL, 'p' }, - { "subsystem", required_argument, NULL, 's' }, - { "debug", no_argument, NULL, 'd' }, - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, 'V' }, - { "monitor", no_argument, NULL, 'm' }, - {} - }; - int c; +static int help(void) { + _cleanup_(table_unrefp) Table *options = NULL; + int r; + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + + printf("%s [OPTIONS...]\n\n", program_invocation_short_name); + + r = table_print_or_warn(options); + if (r < 0) + return r; + + return 0; +} + +static int parse_args(int argc, char *argv[], const char **syspath, const char **subsystem) { + assert(argc >= 0); + assert(argv); assert(syspath); assert(subsystem); - while ((c = getopt_long(argc, argv, "p:s:dhVm", options, NULL)) >= 0) + OptionParser state = { argc, argv }; + const char *arg; + + FOREACH_OPTION(&state, c, &arg, /* on_error= */ return c) switch (c) { - case 'p': - *syspath = optarg; - break; - case 's': - *subsystem = optarg; - break; + OPTION_COMMON_HELP: + return help(); - case 'd': - log_set_max_level(LOG_DEBUG); - break; - - case 'h': - printf("--debug --syspath= --subsystem= --help\n"); - return 0; - - case 'V': + OPTION('V', "version", NULL, "Show package version"): printf("%s\n", GIT_VERSION); return 0; - case 'm': - arg_monitor = true; + OPTION('p', "syspath", "PATH", "Syspath to test"): + *syspath = arg; break; - case '?': - return -EINVAL; + OPTION('s', "subsystem", "SUBSYSTEM", "Subsystem to enumerate"): + *subsystem = arg; + break; - default: - assert_not_reached(); + OPTION('d', "debug", NULL, "Enable debug logging"): + log_set_max_level(LOG_DEBUG); + break; + + OPTION('m', "monitor", NULL, "Run monitor test"): + arg_monitor = true; + break; } return 1; From 04ad6079bbb37f791f4a80f2de0c3b29df5b0751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 22 Apr 2026 23:59:30 +0200 Subject: [PATCH 08/15] sbsign: convert to the new option and verb parsers The options --private-key, --private-key-source, --certificate, --certificate-source are almost identical in sbsign, but are described slightly differently. Add OPTION_COMMON_ macros that are parametrized to keep the purpose of the --private-key and --certificate options in the description. Co-developed-by: Claude Opus 4.7 --- src/bootctl/bootctl.c | 16 ++--- src/sbsign/sbsign.c | 151 +++++++++++++++++------------------------- src/shared/options.h | 14 ++++ 3 files changed, 79 insertions(+), 102 deletions(-) diff --git a/src/bootctl/bootctl.c b/src/bootctl/bootctl.c index 00ae512668..9ec2596285 100644 --- a/src/bootctl/bootctl.c +++ b/src/bootctl/bootctl.c @@ -605,16 +605,13 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { return r; break; - OPTION_LONG("private-key", "PATH|URI", - "Private key for Secure Boot auto-enrollment"): + OPTION_COMMON_PRIVATE_KEY("Private key for Secure Boot auto-enrollment"): r = free_and_strdup_warn(&arg_private_key, arg); if (r < 0) return r; break; - OPTION_LONG("private-key-source", "SOURCE", - "Specify how to use the private key " - "(file, provider:PROVIDER, engine:ENGINE)"): + OPTION_COMMON_PRIVATE_KEY_SOURCE: r = parse_openssl_key_source_argument(arg, &arg_private_key_source, &arg_private_key_source_type); @@ -622,18 +619,13 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { return r; break; - 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"): + OPTION_COMMON_CERTIFICATE("PEM certificate to use when setting up Secure Boot auto-enrollment"): 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)"): + OPTION_COMMON_CERTIFICATE_SOURCE: r = parse_openssl_certificate_source_argument(arg, &arg_certificate_source, &arg_certificate_source_type); diff --git a/src/sbsign/sbsign.c b/src/sbsign/sbsign.c index f54dacf65a..a8c554ffcf 100644 --- a/src/sbsign/sbsign.c +++ b/src/sbsign/sbsign.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include "alloc-util.h" @@ -11,12 +10,14 @@ #include "efi-fundamental.h" #include "fd-util.h" #include "fileio.h" +#include "format-table.h" #include "fs-util.h" #include "install-file.h" #include "io-util.h" #include "log.h" #include "main-func.h" #include "openssl-util.h" +#include "options.h" #include "parse-argument.h" #include "pe-binary.h" #include "pretty-print.h" @@ -47,119 +48,96 @@ STATIC_DESTRUCTOR_REGISTER(arg_signed_data_signature, freep); static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *options = NULL, *verbs = NULL; int r; r = terminal_urlify_man("systemd-sbsign", "1", &link); if (r < 0) return log_oom(); - printf("%1$s [OPTIONS...] COMMAND ...\n" - "\n%5$sSign binaries for EFI Secure Boot%6$s\n" - "\n%3$sCommands:%4$s\n" - " sign EXEFILE Sign the given binary for EFI Secure Boot\n" - "\n%3$sOptions:%4$s\n" - " -h --help Show this help\n" - " --version Print version\n" - " --output Where to write the signed PE binary\n" - " --certificate=PATH|URI\n" - " PEM certificate to use for signing, or a provider\n" - " specific designation 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" - " --private-key=KEY Private key (PEM) to sign with\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 for signing\n" - "\nSee the %2$s for details.\n", - program_invocation_short_name, - link, - ansi_underline(), - ansi_normal(), - ansi_highlight(), - ansi_normal()); + 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%sSign binaries for EFI Secure Boot%s\n" + "\n%sCommands:%s\n", + program_invocation_short_name, + 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(); -} - -static int parse_argv(int argc, char *argv[]) { - enum { - ARG_VERSION = 0x100, - ARG_OUTPUT, - ARG_CERTIFICATE, - ARG_CERTIFICATE_SOURCE, - ARG_PRIVATE_KEY, - ARG_PRIVATE_KEY_SOURCE, - ARG_PREPARE_OFFLINE_SIGNING, - ARG_SIGNED_DATA, - ARG_SIGNED_DATA_SIGNATURE, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "output", required_argument, NULL, ARG_OUTPUT }, - { "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 }, - { "prepare-offline-signing", no_argument, NULL, ARG_PREPARE_OFFLINE_SIGNING }, - { "signed-data", required_argument, NULL, ARG_SIGNED_DATA }, - { "signed-data-signature", required_argument, NULL, ARG_SIGNED_DATA_SIGNATURE }, - {} - }; - - int c, r; +VERB_COMMON_HELP_HIDDEN(help); +static int parse_argv(int argc, char *argv[], char ***ret_args) { assert(argc >= 0); assert(argv); + assert(ret_args); - 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_OUTPUT: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_output); + OPTION_LONG("output", "PATH", + "Where to write the signed PE binary"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_output); if (r < 0) return r; break; - case ARG_CERTIFICATE: - r = free_and_strdup_warn(&arg_certificate, optarg); + OPTION_COMMON_CERTIFICATE("PEM certificate to use for signing"): + r = free_and_strdup_warn(&arg_certificate, arg); if (r < 0) return r; break; - case ARG_CERTIFICATE_SOURCE: + OPTION_COMMON_CERTIFICATE_SOURCE: r = parse_openssl_certificate_source_argument( - optarg, + arg, &arg_certificate_source, &arg_certificate_source_type); if (r < 0) return r; break; - case ARG_PRIVATE_KEY: - r = free_and_strdup_warn(&arg_private_key, optarg); + OPTION_COMMON_PRIVATE_KEY("Private key (PEM) to sign with"): + r = free_and_strdup_warn(&arg_private_key, arg); if (r < 0) return r; break; - case ARG_PRIVATE_KEY_SOURCE: + OPTION_COMMON_PRIVATE_KEY_SOURCE: r = parse_openssl_key_source_argument( - optarg, + arg, &arg_private_key_source, &arg_private_key_source_type); if (r < 0) @@ -167,29 +145,23 @@ static int parse_argv(int argc, char *argv[]) { break; - case ARG_PREPARE_OFFLINE_SIGNING: + OPTION_LONG("prepare-offline-signing", NULL, /* help= */ NULL): arg_prepare_offline_signing = true; break; - case ARG_SIGNED_DATA: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_signed_data); + OPTION_LONG("signed-data", "PATH", /* help= */ NULL): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_signed_data); if (r < 0) return r; break; - case ARG_SIGNED_DATA_SIGNATURE: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_signed_data_signature); + OPTION_LONG("signed-data-signature", "PATH", /* help= */ NULL): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_signed_data_signature); if (r < 0) return r; break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } if (arg_private_key_source && !arg_certificate) @@ -201,6 +173,7 @@ static int parse_argv(int argc, char *argv[]) { if (arg_prepare_offline_signing && (arg_private_key || arg_signed_data || arg_signed_data_signature)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--prepare-offline-signing cannot be used with --private-key=, --signed-data= or --signed-data-signature="); + *ret_args = option_parser_get_args(&state); return 1; } @@ -442,6 +415,8 @@ static int pkcs7_add_digest_attribute(PKCS7 *p7, BIO *data, PKCS7_SIGNER_INFO *s return 0; } +VERB(verb_sign, "sign", "EXEFILE", 2, 2, 0, + "Sign the given binary for EFI Secure Boot"); static int verb_sign(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(openssl_ask_password_ui_freep) OpenSSLAskPasswordUI *ui = NULL; _cleanup_(EVP_PKEY_freep) EVP_PKEY *private_key = NULL; @@ -742,20 +717,16 @@ static int verb_sign(int argc, char *argv[], uintptr_t _data, void *userdata) { } static int run(int argc, char *argv[]) { - static const Verb verbs[] = { - { "help", VERB_ANY, VERB_ANY, 0, verb_help }, - { "sign", 2, 2, 0, verb_sign }, - {} - }; int r; log_setup(); - r = parse_argv(argc, argv); + char **args = NULL; + r = parse_argv(argc, argv, &args); if (r <= 0) return r; - return dispatch_verb(argc, argv, verbs, NULL); + return dispatch_verb_with_args(args, NULL); } DEFINE_MAIN_FUNCTION(run); diff --git a/src/shared/options.h b/src/shared/options.h index 974c21d16a..d86766f90e 100644 --- a/src/shared/options.h +++ b/src/shared/options.h @@ -94,6 +94,20 @@ typedef struct Option { #define OPTION_COMMON_LOWERCASE_J \ OPTION_SHORT('j', NULL, \ "Equivalent to --json=pretty (on TTY) or --json=short (otherwise)") +#define OPTION_COMMON_PRIVATE_KEY(purpose) \ + OPTION_LONG("private-key", "PATH|URI", purpose) +#define OPTION_COMMON_PRIVATE_KEY_SOURCE \ + OPTION_LONG("private-key-source", "SOURCE", \ + "Specify how to use the private key " \ + "(file, provider:PROVIDER, engine:ENGINE)") +#define OPTION_COMMON_CERTIFICATE(purpose) \ + OPTION_LONG("certificate", "PATH|URI", purpose \ + ", or a provider-specific designation if --certificate-source= is used") +#define OPTION_COMMON_CERTIFICATE_SOURCE \ + 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)") /* This is magically mapped to the beginning and end of the section */ extern const Option __start_SYSTEMD_OPTIONS[]; From 82a95edc9257a9088445c48721a5e2e846e867dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 23 Apr 2026 00:32:59 +0200 Subject: [PATCH 09/15] repart: convert to the new option parser The metavars for a few options were changed to be shorter, so that the automatic alignment works better. Overall, I think the new version is as least as legible as the old one. The synopsis for -S/-C/-P is fixed, they do not take an argument. Co-developed-by: Claude Opus 4.7 --- src/repart/repart.c | 751 +++++++++++++++++++------------------------- 1 file changed, 319 insertions(+), 432 deletions(-) diff --git a/src/repart/repart.c b/src/repart/repart.c index e307bfe13f..becdacd554 100644 --- a/src/repart/repart.c +++ b/src/repart/repart.c @@ -1,7 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include -#include #include #include #include @@ -61,6 +60,7 @@ #include "mountpoint-util.h" #include "nulstr-util.h" #include "openssl-util.h" +#include "options.h" #include "parse-argument.h" #include "parse-helpers.h" #include "parse-util.h" @@ -9606,355 +9606,164 @@ static int help(void) { if (r < 0) return log_oom(); - printf("%1$s [OPTIONS...] [DEVICE]\n" - "\n%5$sGrow and add partitions to a partition table, and generate disk images (DDIs).%6$s\n\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" - "\n%3$sOperation:%4$s\n" - " --dry-run=BOOL Whether to run dry-run operation\n" - " --empty=MODE One of refuse, allow, require, force, create; controls\n" - " how to handle empty disks lacking partition tables\n" - " --offline=BOOL Whether to build the image offline\n" - " --discard=BOOL Whether to discard backing blocks for new partitions\n" - " --sector-size=SIZE Set the logical sector size for the image\n" - " --grain-size=BYTES Set the grain size for partition alignment\n" - " --architecture=ARCH Set the generic architecture for the image\n" - " --size=BYTES Grow loopback file to specified size\n" - " --seed=UUID 128-bit seed UUID to derive all UUIDs from\n" - " --split=BOOL Whether to generate split artifacts\n" - "\n%3$sOutput:%4$s\n" - " --pretty=BOOL Whether to show pretty summary before doing changes\n" - " --json=pretty|short|off\n" - " Generate JSON output\n" - "\n%3$sFactory Reset:%4$s\n" - " --factory-reset=BOOL Whether to remove data partitions before recreating\n" - " them\n" - " --can-factory-reset Test whether factory reset is defined\n" - "\n%3$sConfiguration & Image Control:%4$s\n" - " --root=PATH Operate relative to root path\n" - " --image=PATH Operate relative to image file\n" - " --image-policy=POLICY\n" - " Specify disk image dissection policy\n" - " --definitions=DIR Find partition definitions in specified directory\n" - " --list-devices List candidate block devices to operate on\n" - "\n%3$sVerity:%4$s\n" - " --private-key=PATH|URI\n" - " Private key to use when generating verity roothash\n" - " signatures, 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 generating\n" - " verity roothash signatures\n" - " --certificate=PATH|URI\n" - " PEM certificate to use when generating verity roothash\n" - " signatures, or a provider specific designation if\n" - " --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" - " --join-signature=HASH:SIG\n" - " Specify root hash and pkcs7 signature of root hash for\n" - " verity as a tuple of hex encoded hash and a DER\n" - " encoded PKCS7, either as a path to a file or as an\n" - " ASCII base64 encoded string prefixed by 'base64:'\n" - "\n%3$sEncryption:%4$s\n" - " --key-file=PATH Key to use when encrypting partitions\n" - " --tpm2-device=PATH Path to TPM2 device node to use\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" - " TPM2 PCR indexes to use for TPM2 enrollment\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-pcrlock=PATH\n" - " Specify pcrlock policy to lock against\n" - "\n%3$sPartition Control:%4$s\n" - " --include-partitions=PARTITION1,PARTITION2,PARTITION3,…\n" - " Ignore partitions not of the specified types\n" - " --exclude-partitions=PARTITION1,PARTITION2,PARTITION3,…\n" - " Ignore partitions of the specified types\n" - " --defer-partitions=PARTITION1,PARTITION2,PARTITION3,…\n" - " Take partitions of the specified types into account\n" - " but don't populate them yet\n" - " --defer-partitions-empty=yes\n" - " Defer all partitions marked for formatting as empty\n" - " --defer-partitions-factory-reset=yes\n" - " Defer all partitions marked for factory reset\n" - "\n%3$sCopying:%4$s\n" - " -s --copy-source=PATH Specify the primary source tree to copy files from\n" - " --copy-from=IMAGE Copy partitions from the given image(s)\n" - "\n%3$sDDI Profile:%4$s\n" - " -S --make-ddi=sysext Make a system extension DDI\n" - " -C --make-ddi=confext Make a configuration extension DDI\n" - " -P --make-ddi=portable Make a portable service DDI\n" - "\n%3$sAuxiliary Resource Generation:%4$s\n" - " --append-fstab=MODE One of no, auto, replace; controls how to join the\n" - " content of a pre-existing fstab with the generated one\n" - " --generate-fstab=PATH\n" - " Write fstab configuration to the given path\n" - " --generate-crypttab=PATH\n" - " Write crypttab configuration to the given path\n" - "\n%3$sEl Torito boot catalog:%4$s\n" - " --el-torito=BOOL Whether to add a boot catalog to boot the ESP\n" - " --el-torito-system=STRING\n" - " Set the system identifier in the ISO9660 descriptor\n" - " --el-torito-volume=STRING\n" - " Set the volume identifier in the ISO9660 descriptor\n" - " --el-torito-publisher=STRING\n" - " Set the publisher identifier in the ISO9660 descriptor\n" - "\nSee the %2$s for details.\n", + static const char *const option_groups[] = { + "Options", + "Operation", + "Output", + "Factory Reset", + "Configuration & Image Control", + "Verity", + "Encryption", + "Partition Control", + "Copying", + "DDI Profile", + "Auxiliary Resource Generation", + "El Torito boot catalog", + }; + + _cleanup_(table_unref_many) Table *option_tables[ELEMENTSOF(option_groups) + 1] = {}; + + 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, + option_tables[0], option_tables[1], option_tables[2], + option_tables[3], option_tables[4], option_tables[5], + option_tables[6], option_tables[7], option_tables[8], + option_tables[9], option_tables[10], option_tables[11]); + + printf("%s [OPTIONS...] [DEVICE]\n" + "\n%sGrow and add partitions to a partition table, and generate disk images (DDIs).%s\n", program_invocation_short_name, - link, - ansi_underline(), - ansi_normal(), ansi_highlight(), ansi_normal()); + 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 parse_argv(int argc, char *argv[]) { - enum { - ARG_VERSION = 0x100, - ARG_NO_PAGER, - ARG_NO_LEGEND, - ARG_DRY_RUN, - ARG_EMPTY, - ARG_DISCARD, - ARG_FACTORY_RESET, - ARG_CAN_FACTORY_RESET, - ARG_ROOT, - ARG_IMAGE, - ARG_IMAGE_POLICY, - ARG_SEED, - ARG_PRETTY, - ARG_DEFINITIONS, - ARG_SIZE, - ARG_JSON, - ARG_KEY_FILE, - ARG_PRIVATE_KEY, - ARG_PRIVATE_KEY_SOURCE, - ARG_CERTIFICATE, - ARG_CERTIFICATE_SOURCE, - 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_PCRLOCK, - ARG_SPLIT, - ARG_INCLUDE_PARTITIONS, - ARG_EXCLUDE_PARTITIONS, - ARG_DEFER_PARTITIONS, - ARG_DEFER_PARTITIONS_EMPTY, - ARG_DEFER_PARTITIONS_FACTORY_RESET, - ARG_SECTOR_SIZE, - ARG_GRAIN_SIZE, - ARG_SKIP_PARTITIONS, - ARG_ARCHITECTURE, - ARG_OFFLINE, - ARG_COPY_FROM, - ARG_MAKE_DDI, - ARG_APPEND_FSTAB, - ARG_GENERATE_FSTAB, - ARG_GENERATE_CRYPTTAB, - ARG_LIST_DEVICES, - ARG_JOIN_SIGNATURE, - ARG_ELTORITO, - ARG_ELTORITO_SYSTEM, - ARG_ELTORITO_VOLUME, - ARG_ELTORITO_PUBLISHER, - }; - - 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 }, - { "dry-run", required_argument, NULL, ARG_DRY_RUN }, - { "empty", required_argument, NULL, ARG_EMPTY }, - { "discard", required_argument, NULL, ARG_DISCARD }, - { "factory-reset", required_argument, NULL, ARG_FACTORY_RESET }, - { "can-factory-reset", no_argument, NULL, ARG_CAN_FACTORY_RESET }, - { "root", required_argument, NULL, ARG_ROOT }, - { "image", required_argument, NULL, ARG_IMAGE }, - { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, - { "seed", required_argument, NULL, ARG_SEED }, - { "pretty", required_argument, NULL, ARG_PRETTY }, - { "definitions", required_argument, NULL, ARG_DEFINITIONS }, - { "size", required_argument, NULL, ARG_SIZE }, - { "json", required_argument, NULL, ARG_JSON }, - { "key-file", required_argument, NULL, ARG_KEY_FILE }, - { "private-key", required_argument, NULL, ARG_PRIVATE_KEY }, - { "private-key-source", required_argument, NULL, ARG_PRIVATE_KEY_SOURCE }, - { "certificate", required_argument, NULL, ARG_CERTIFICATE }, - { "certificate-source", required_argument, NULL, ARG_CERTIFICATE_SOURCE }, - { "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-pcrlock", required_argument, NULL, ARG_TPM2_PCRLOCK }, - { "split", required_argument, NULL, ARG_SPLIT }, - { "include-partitions", required_argument, NULL, ARG_INCLUDE_PARTITIONS }, - { "exclude-partitions", required_argument, NULL, ARG_EXCLUDE_PARTITIONS }, - { "defer-partitions", required_argument, NULL, ARG_DEFER_PARTITIONS }, - { "defer-partitions-empty", required_argument, NULL, ARG_DEFER_PARTITIONS_EMPTY }, - { "defer-partitions-factory-reset", required_argument, NULL, ARG_DEFER_PARTITIONS_FACTORY_RESET }, - { "sector-size", required_argument, NULL, ARG_SECTOR_SIZE }, - { "grain-size", required_argument, NULL, ARG_GRAIN_SIZE }, - { "architecture", required_argument, NULL, ARG_ARCHITECTURE }, - { "offline", required_argument, NULL, ARG_OFFLINE }, - { "copy-from", required_argument, NULL, ARG_COPY_FROM }, - { "copy-source", required_argument, NULL, 's' }, - { "make-ddi", required_argument, NULL, ARG_MAKE_DDI }, - { "append-fstab", required_argument, NULL, ARG_APPEND_FSTAB }, - { "generate-fstab", required_argument, NULL, ARG_GENERATE_FSTAB }, - { "generate-crypttab", required_argument, NULL, ARG_GENERATE_CRYPTTAB }, - { "list-devices", no_argument, NULL, ARG_LIST_DEVICES }, - { "join-signature", required_argument, NULL, ARG_JOIN_SIGNATURE }, - { "el-torito", required_argument, NULL, ARG_ELTORITO }, - { "el-torito-system", required_argument, NULL, ARG_ELTORITO_SYSTEM }, - { "el-torito-volume", required_argument, NULL, ARG_ELTORITO_VOLUME }, - { "el-torito-publisher", required_argument, NULL, ARG_ELTORITO_PUBLISHER }, - {} - }; - - bool auto_public_key_pcr_mask = true, auto_pcrlock = true; - int c, r; - assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "hs:SCP", options, NULL)) >= 0) + OptionParser state = { argc, argv }; + const char *arg; + bool auto_public_key_pcr_mask = true, auto_pcrlock = true; + int r; + FOREACH_OPTION(&state, c, &arg, /* on_error= */ return c) switch (c) { - case 'h': + OPTION_GROUP("Options"): {} + + 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_DRY_RUN: - r = parse_boolean_argument("--dry-run=", optarg, &arg_dry_run); + OPTION_GROUP("Operation"): {} + + OPTION_LONG("dry-run", "BOOL", + "Whether to run dry-run operation"): + r = parse_boolean_argument("--dry-run=", arg, &arg_dry_run); if (r < 0) return r; break; - case ARG_EMPTY: - if (isempty(optarg)) { + OPTION_LONG("empty", "MODE", + "How to handle empty disks lacking partition tables (refuse, allow, require, force, create)"): + if (isempty(arg)) { arg_empty = EMPTY_UNSET; break; } - arg_empty = empty_mode_from_string(optarg); + arg_empty = empty_mode_from_string(arg); if (arg_empty < 0) - return log_error_errno(arg_empty, "Failed to parse --empty= parameter: %s", optarg); + return log_error_errno(arg_empty, "Failed to parse --empty= parameter: %s", arg); break; - case ARG_DISCARD: - r = parse_boolean_argument("--discard=", optarg, &arg_discard); - if (r < 0) - return r; - break; - - case ARG_FACTORY_RESET: - r = parse_boolean_argument("--factory-reset=", optarg, NULL); - if (r < 0) - return r; - arg_factory_reset = r; - break; - - case ARG_CAN_FACTORY_RESET: - arg_can_factory_reset = true; - break; - - case ARG_ROOT: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_root); - if (r < 0) - return r; - break; - - case ARG_IMAGE: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image); - if (r < 0) - return r; - - arg_relax_copy_block_security = false; - - break; - - case ARG_IMAGE_POLICY: - r = parse_image_policy_argument(optarg, &arg_image_policy); - if (r < 0) - return r; - break; - - case ARG_SEED: - if (isempty(optarg)) { - arg_seed = SD_ID128_NULL; - arg_randomize = false; - } else if (streq(optarg, "random")) - arg_randomize = true; + OPTION_LONG("offline", "BOOL", + "Whether to build the image offline"): + if (streq(arg, "auto")) + arg_offline = -1; else { - r = sd_id128_from_string(optarg, &arg_seed); + r = parse_boolean_argument("--offline=", arg, NULL); if (r < 0) - return log_error_errno(r, "Failed to parse seed: %s", optarg); + return r; - arg_randomize = false; + arg_offline = r; } break; - case ARG_PRETTY: - r = parse_boolean_argument("--pretty=", optarg, NULL); + OPTION_LONG("discard", "BOOL", + "Whether to discard backing blocks for new partitions"): + r = parse_boolean_argument("--discard=", arg, &arg_discard); if (r < 0) return r; - arg_pretty = r; break; - case ARG_DEFINITIONS: { - _cleanup_free_ char *path = NULL; - r = parse_path_argument(optarg, false, &path); + OPTION_LONG("sector-size", "SIZE", + "Set the logical sector size for the image"): + r = parse_sector_size(arg, &arg_sector_size); if (r < 0) return r; - if (strv_consume(&arg_definitions, TAKE_PTR(path)) < 0) - return log_oom(); - break; - } - case ARG_SIZE: { + break; + + OPTION_LONG("grain-size", "BYTES", + "Set the grain size for partition alignment"): + r = parse_size(arg, 1024, &arg_grain_size); + if (r < 0) + return log_error_errno(r, "Failed to parse --grain-size= parameter: %s", arg); + if (arg_grain_size < 512 || !ISPOWEROF2(arg_grain_size)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Grain size must be a power of 2 >= 512."); + + break; + + OPTION_LONG("architecture", "ARCH", + "Set the generic architecture for the image"): + r = architecture_from_string(arg); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid architecture '%s'.", arg); + + arg_architecture = r; + break; + + OPTION_LONG("size", "BYTES", + "Grow loopback file to specified size"): { uint64_t parsed, rounded; - if (streq(optarg, "auto")) { + if (streq(arg, "auto")) { arg_size = UINT64_MAX; arg_size_auto = true; break; } - r = parse_size(optarg, 1024, &parsed); + r = parse_size(arg, 1024, &parsed); if (r < 0) - return log_error_errno(r, "Failed to parse --size= parameter: %s", optarg); + return log_error_errno(r, "Failed to parse --size= parameter: %s", arg); rounded = round_up_size(parsed, 4096); if (rounded == 0) @@ -9971,59 +9780,168 @@ static int parse_argv(int argc, char *argv[]) { break; } - case ARG_JSON: - r = parse_json_argument(optarg, &arg_json_format_flags); + OPTION_LONG("seed", "UUID", + "128-bit seed UUID to derive all UUIDs from"): + if (isempty(arg)) { + arg_seed = SD_ID128_NULL; + arg_randomize = false; + } else if (streq(arg, "random")) + arg_randomize = true; + else { + r = sd_id128_from_string(arg, &arg_seed); + if (r < 0) + return log_error_errno(r, "Failed to parse seed: %s", arg); + + arg_randomize = false; + } + + break; + + OPTION_LONG("split", "BOOL", + "Whether to generate split artifacts"): + r = parse_boolean_argument("--split=", arg, NULL); + if (r < 0) + return r; + + arg_split = r; + break; + + OPTION_GROUP("Output"): {} + + OPTION_LONG("pretty", "BOOL", + "Whether to show pretty summary before doing changes"): + r = parse_boolean_argument("--pretty=", arg, NULL); + if (r < 0) + return r; + arg_pretty = r; + break; + + OPTION_COMMON_JSON: + r = parse_json_argument(arg, &arg_json_format_flags); if (r <= 0) return r; break; - case ARG_KEY_FILE: { - r = parse_key_file(optarg, &arg_key); + OPTION_GROUP("Factory Reset"): {} + + OPTION_LONG("factory-reset", "BOOL", + "Whether to remove data partitions before recreating them"): + r = parse_boolean_argument("--factory-reset=", arg, NULL); + if (r < 0) + return r; + arg_factory_reset = r; + break; + + OPTION_LONG("can-factory-reset", NULL, + "Test whether factory reset is defined"): + arg_can_factory_reset = true; + break; + + OPTION_GROUP("Configuration & Image Control"): {} + + OPTION_LONG("root", "PATH", + "Operate relative to root path"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_root); if (r < 0) return r; break; - } - case ARG_PRIVATE_KEY: { - r = free_and_strdup_warn(&arg_private_key, optarg); + OPTION_LONG("image", "PATH", + "Operate relative to image file"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_image); + if (r < 0) + return r; + + arg_relax_copy_block_security = false; + + break; + + 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; + + OPTION_LONG("definitions", "DIR", + "Find partition definitions in specified directory"): { + _cleanup_free_ char *path = NULL; + r = parse_path_argument(arg, false, &path); + if (r < 0) + return r; + if (strv_consume(&arg_definitions, TAKE_PTR(path)) < 0) + return log_oom(); + break; } - case ARG_PRIVATE_KEY_SOURCE: + OPTION_LONG("list-devices", NULL, + "List candidate block devices to operate on"): + r = blockdev_list(BLOCKDEV_LIST_REQUIRE_PARTITION_SCANNING|BLOCKDEV_LIST_SHOW_SYMLINKS|BLOCKDEV_LIST_IGNORE_ZRAM, /* ret_devices= */ NULL, /* ret_n_devices= */ NULL); + if (r < 0) + return r; + + return 0; + + OPTION_GROUP("Verity"): {} + + OPTION_COMMON_PRIVATE_KEY("Private key to use when generating verity roothash signatures"): + r = free_and_strdup_warn(&arg_private_key, arg); + if (r < 0) + return r; + break; + + OPTION_COMMON_PRIVATE_KEY_SOURCE: r = parse_openssl_key_source_argument( - optarg, + arg, &arg_private_key_source, &arg_private_key_source_type); if (r < 0) return r; break; - case ARG_CERTIFICATE: - r = free_and_strdup_warn(&arg_certificate, optarg); + OPTION_COMMON_CERTIFICATE("PEM certificate to use when generating verity roothash signatures"): + r = free_and_strdup_warn(&arg_certificate, arg); if (r < 0) return r; break; - case ARG_CERTIFICATE_SOURCE: + OPTION_COMMON_CERTIFICATE_SOURCE: r = parse_openssl_certificate_source_argument( - optarg, + arg, &arg_certificate_source, &arg_certificate_source_type); if (r < 0) return r; break; - case ARG_TPM2_DEVICE: { + OPTION_LONG("join-signature", "HASH:SIG", + "Specify root hash and pkcs7 signature of root hash for verity as a tuple of " + "hex-encoded hash and a DER-encoded PKCS7, either as a path to a file or as an " + "ASCII base64-encoded string prefixed by 'base64:'"): + r = parse_join_signature(arg, &arg_verity_settings); + if (r < 0) + return r; + break; + + OPTION_GROUP("Encryption"): {} + + OPTION_LONG("key-file", "PATH", + "Key to use when encrypting partitions"): + r = parse_key_file(arg, &arg_key); + if (r < 0) + return r; + break; + + OPTION_LONG("tpm2-device", "PATH", + "Path to TPM2 device node to use"): { _cleanup_free_ char *device = NULL; - if (streq(optarg, "list")) + if (streq(arg, "list")) return tpm2_list_devices(/* legend= */ true, /* quiet= */ false); - if (!streq(optarg, "auto")) { - device = strdup(optarg); + if (!streq(arg, "auto")) { + device = strdup(arg); if (!device) return log_oom(); } @@ -10033,64 +9951,65 @@ static int parse_argv(int argc, char *argv[]) { break; } - case ARG_TPM2_DEVICE_KEY: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_device_key); + OPTION_LONG("tpm2-device-key", "PATH", + "Enroll a TPM2 device using its public key"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_tpm2_device_key); if (r < 0) return r; 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+…", + "TPM2 PCR indexes to use for TPM2 enrollment"): + 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: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_public_key); + OPTION_LONG("tpm2-public-key", "PATH", + "Enroll signed TPM2 PCR policy against PEM public key"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_tpm2_public_key); if (r < 0) return r; break; - case ARG_TPM2_PUBLIC_KEY_PCRS: + OPTION_LONG("tpm2-public-key-pcrs", "PCR1+PCR2+…", + "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_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_SPLIT: - r = parse_boolean_argument("--split=", optarg, NULL); - if (r < 0) - return r; + OPTION_GROUP("Partition Control"): {} - arg_split = r; - break; - - case ARG_INCLUDE_PARTITIONS: + OPTION_LONG("include-partitions", "PART1,PART2…", + "Ignore partitions not of the specified types"): if (arg_filter_partitions_type == FILTER_PARTITIONS_EXCLUDE) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Combination of --include-partitions= and --exclude-partitions= is invalid."); - r = parse_partition_types(optarg, &arg_filter_partitions, &arg_n_filter_partitions); + r = parse_partition_types(arg, &arg_filter_partitions, &arg_n_filter_partitions); if (r < 0) return r; @@ -10098,12 +10017,13 @@ static int parse_argv(int argc, char *argv[]) { break; - case ARG_EXCLUDE_PARTITIONS: + OPTION_LONG("exclude-partitions", "PART1,PART2…", + "Ignore partitions of the specified types"): if (arg_filter_partitions_type == FILTER_PARTITIONS_INCLUDE) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Combination of --include-partitions= and --exclude-partitions= is invalid."); - r = parse_partition_types(optarg, &arg_filter_partitions, &arg_n_filter_partitions); + r = parse_partition_types(arg, &arg_filter_partitions, &arg_n_filter_partitions); if (r < 0) return r; @@ -10111,68 +10031,44 @@ static int parse_argv(int argc, char *argv[]) { break; - case ARG_DEFER_PARTITIONS: - r = parse_partition_types(optarg, &arg_defer_partitions, &arg_n_defer_partitions); + OPTION_LONG("defer-partitions", "PART1,PART2…", + "Take partitions of the specified types into account but don't populate them yet"): + r = parse_partition_types(arg, &arg_defer_partitions, &arg_n_defer_partitions); if (r < 0) return r; break; - case ARG_DEFER_PARTITIONS_EMPTY: - r = parse_boolean_argument("--defer-partitions-empty=", optarg, &arg_defer_partitions_empty); + OPTION_LONG("defer-partitions-empty", "BOOL", + "Defer all partitions marked for formatting as empty"): + r = parse_boolean_argument("--defer-partitions-empty=", arg, &arg_defer_partitions_empty); if (r < 0) return r; break; - case ARG_DEFER_PARTITIONS_FACTORY_RESET: - r = parse_boolean_argument("--defer-partitions-factory-reset=", optarg, &arg_defer_partitions_factory_reset); + OPTION_LONG("defer-partitions-factory-reset", "BOOL", + "Defer all partitions marked for factory reset"): + r = parse_boolean_argument("--defer-partitions-factory-reset=", arg, &arg_defer_partitions_factory_reset); if (r < 0) return r; break; - case ARG_SECTOR_SIZE: - r = parse_sector_size(optarg, &arg_sector_size); + OPTION_GROUP("Copying"): {} + + OPTION('s', "copy-source", "PATH", + "Specify the primary source tree to copy files from"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_copy_source); if (r < 0) return r; - break; - case ARG_GRAIN_SIZE: - r = parse_size(optarg, 1024, &arg_grain_size); - if (r < 0) - return log_error_errno(r, "Failed to parse --grain-size= parameter: %s", optarg); - if (arg_grain_size < 512 || !ISPOWEROF2(arg_grain_size)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Grain size must be a power of 2 >= 512."); - - break; - - case ARG_ARCHITECTURE: - r = architecture_from_string(optarg); - if (r < 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid architecture '%s'.", optarg); - - arg_architecture = r; - break; - - case ARG_OFFLINE: - if (streq(optarg, "auto")) - arg_offline = -1; - else { - r = parse_boolean_argument("--offline=", optarg, NULL); - if (r < 0) - return r; - - arg_offline = r; - } - - break; - - case ARG_COPY_FROM: { + OPTION_LONG("copy-from", "IMAGE", + "Copy partitions from the given image"): { _cleanup_free_ char *p = NULL; - r = parse_path_argument(optarg, /* suppress_root= */ false, &p); + r = parse_path_argument(arg, /* suppress_root= */ false, &p); if (r < 0) return r; @@ -10182,120 +10078,110 @@ static int parse_argv(int argc, char *argv[]) { break; } - case 's': - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_copy_source); + OPTION_GROUP("DDI Profile"): {} + + OPTION_LONG("make-ddi", "TYPE", + "Create a DDI of the given type"): + if (!filename_is_valid(arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid DDI type: %s", arg); + + r = free_and_strdup_warn(&arg_make_ddi, arg); if (r < 0) return r; break; - case ARG_MAKE_DDI: - if (!filename_is_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid DDI type: %s", optarg); - - r = free_and_strdup_warn(&arg_make_ddi, optarg); - if (r < 0) - return r; - break; - - case 'S': + OPTION_SHORT('S', NULL, "Same as --make-ddi=sysext, make a system extension"): r = free_and_strdup_warn(&arg_make_ddi, "sysext"); if (r < 0) return r; break; - case 'C': + OPTION_SHORT('C', NULL, "Same as --make-ddi=confext, make a configuration extension"): r = free_and_strdup_warn(&arg_make_ddi, "confext"); if (r < 0) return r; break; - case 'P': + OPTION_SHORT('P', NULL, "Same as --make-ddi=portable, make a portable service"): r = free_and_strdup_warn(&arg_make_ddi, "portable"); if (r < 0) return r; break; - case ARG_APPEND_FSTAB: - if (isempty(optarg)) { + OPTION_GROUP("Auxiliary Resource Generation"): {} + + OPTION_LONG("append-fstab", "MODE", + "How to join the content of a pre-existing fstab with the generated one " + "(no, auto, replace)"): + if (isempty(arg)) { arg_append_fstab = APPEND_AUTO; break; } - arg_append_fstab = append_mode_from_string(optarg); + arg_append_fstab = append_mode_from_string(arg); if (arg_append_fstab < 0) - return log_error_errno(arg_append_fstab, "Failed to parse --append-fstab= parameter: %s", optarg); + return log_error_errno(arg_append_fstab, "Failed to parse --append-fstab= parameter: %s", arg); break; - case ARG_GENERATE_FSTAB: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_generate_fstab); + OPTION_LONG("generate-fstab", "PATH", + "Write fstab configuration to the given path"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_generate_fstab); if (r < 0) return r; break; - case ARG_GENERATE_CRYPTTAB: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_generate_crypttab); + OPTION_LONG("generate-crypttab", "PATH", + "Write crypttab configuration to the given path"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_generate_crypttab); if (r < 0) return r; break; - case ARG_LIST_DEVICES: - r = blockdev_list(BLOCKDEV_LIST_REQUIRE_PARTITION_SCANNING|BLOCKDEV_LIST_SHOW_SYMLINKS|BLOCKDEV_LIST_IGNORE_ZRAM, /* ret_devices= */ NULL, /* ret_n_devices= */ NULL); - if (r < 0) - return r; + OPTION_GROUP("El Torito boot catalog"): {} - return 0; - - case ARG_JOIN_SIGNATURE: - r = parse_join_signature(optarg, &arg_verity_settings); - if (r < 0) - return r; - break; - - case ARG_ELTORITO: - r = parse_boolean_argument("--el-torito=", optarg, &arg_eltorito); + OPTION_LONG("el-torito", "BOOL", + "Whether to add a boot catalog to boot the ESP"): + r = parse_boolean_argument("--el-torito=", arg, &arg_eltorito); if (r < 0) return r; break; - case ARG_ELTORITO_SYSTEM: - if (!iso9660_system_name_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid value '%s' for --el-torito-system=.", optarg); + OPTION_LONG("el-torito-system", "STRING", + "Set the system identifier in the ISO9660 descriptor"): + if (!iso9660_system_name_valid(arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid value '%s' for --el-torito-system=.", arg); - r = free_and_strdup_warn(&arg_eltorito_system, optarg); + r = free_and_strdup_warn(&arg_eltorito_system, arg); if (r < 0) return r; break; - case ARG_ELTORITO_VOLUME: - if (!iso9660_volume_name_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid value '%s' for --el-torito-volume=.", optarg); + OPTION_LONG("el-torito-volume", "STRING", + "Set the volume identifier in the ISO9660 descriptor"): + if (!iso9660_volume_name_valid(arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid value '%s' for --el-torito-volume=.", arg); - r = free_and_strdup_warn(&arg_eltorito_volume, optarg); + r = free_and_strdup_warn(&arg_eltorito_volume, arg); if (r < 0) return r; break; - case ARG_ELTORITO_PUBLISHER: - if (!iso9660_publisher_name_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid value '%s' for --el-torito-publisher=.", optarg); + OPTION_LONG("el-torito-publisher", "STRING", + "Set the publisher identifier in the ISO9660 descriptor"): + if (!iso9660_publisher_name_valid(arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid value '%s' for --el-torito-publisher=.", arg); - r = free_and_strdup_warn(&arg_eltorito_publisher, optarg); + r = free_and_strdup_warn(&arg_eltorito_publisher, arg); if (r < 0) return r; break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } - if (argc - optind > 1) + if (option_parser_get_n_args(&state) > 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected at most one argument, the path to the block device or image file."); @@ -10375,11 +10261,12 @@ static int parse_argv(int argc, char *argv[]) { arg_relax_copy_block_security = true; } - if (argc > optind) { - if (empty_or_dash(argv[optind])) + char **args = option_parser_get_args(&state); + if (!strv_isempty(args)) { + if (empty_or_dash(args[0])) arg_node_none = true; else { - arg_node = strdup(argv[optind]); + arg_node = strdup(args[0]); if (!arg_node) return log_oom(); arg_node_none = false; From aa7ebb1b629a1ce9b1748f50f3d025a1671ea28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 23 Apr 2026 19:07:17 +0200 Subject: [PATCH 10/15] repart: use parse_tristate_argument_with_auto in one more place --- src/repart/repart.c | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/repart/repart.c b/src/repart/repart.c index becdacd554..9982fc7450 100644 --- a/src/repart/repart.c +++ b/src/repart/repart.c @@ -9705,16 +9705,9 @@ static int parse_argv(int argc, char *argv[]) { OPTION_LONG("offline", "BOOL", "Whether to build the image offline"): - if (streq(arg, "auto")) - arg_offline = -1; - else { - r = parse_boolean_argument("--offline=", arg, NULL); - if (r < 0) - return r; - - arg_offline = r; - } - + r = parse_tristate_argument_with_auto("--offline=", arg, &arg_offline); + if (r < 0) + return r; break; OPTION_LONG("discard", "BOOL", From 69d1f600d072b729c6b406519aa349e56c30da50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 23 Apr 2026 09:39:04 +0200 Subject: [PATCH 11/15] measure: reorder verb functions to match --help --- src/measure/measure-tool.c | 294 ++++++++++++++++++------------------- 1 file changed, 147 insertions(+), 147 deletions(-) diff --git a/src/measure/measure-tool.c b/src/measure/measure-tool.c index 4abb77aeb8..bd1339ddfa 100644 --- a/src/measure/measure-tool.c +++ b/src/measure/measure-tool.c @@ -393,6 +393,153 @@ static int parse_argv(int argc, char *argv[]) { return 1; } +static int compare_reported_pcr_nr(uint32_t pcr, const char *varname, const char *description) { + _cleanup_free_ char *s = NULL; + uint32_t v; + int r; + + r = efi_get_variable_string(varname, &s); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to read EFI variable '%s': %m", varname); + + r = safe_atou32(s, &v); + if (r < 0) + return log_error_errno(r, "Failed to parse EFI variable '%s': %s", varname, s); + + if (pcr != v) + log_warning("PCR number reported by stub for %s (%" PRIu32 ") different from our expectation (%" PRIu32 ").\n" + "The measurements are likely inconsistent.", description, v, pcr); + + return 0; +} + +static int validate_stub(void) { + uint64_t features; + bool found = false; + int r; + + if (!tpm2_is_fully_supported()) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Sorry, system lacks full TPM2 support."); + + r = efi_stub_get_features(&features); + if (r < 0) + return log_error_errno(r, "Unable to get stub features: %m"); + + if (!FLAGS_SET(features, EFI_STUB_FEATURE_THREE_PCRS)) + log_warning("Warning: current kernel image does not support measuring itself, the command line or initrd system extension images.\n" + "The PCR measurements seen are unlikely to be valid."); + + r = compare_reported_pcr_nr(TPM2_PCR_KERNEL_BOOT, EFI_LOADER_VARIABLE_STR("StubPcrKernelImage"), "kernel image"); + if (r < 0) + return r; + + STRV_FOREACH(bank, arg_banks) { + _cleanup_free_ char *b = NULL, *p = NULL; + + b = strdup(*bank); + if (!b) + return log_oom(); + + if (asprintf(&p, "/sys/class/tpm/tpm0/pcr-%s/", ascii_strlower(b)) < 0) + return log_oom(); + + if (access(p, F_OK) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to detect if '%s' exists: %m", b); + } else + found = true; + } + + if (!found) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "None of the select PCR banks appear to exist."); + + return 0; +} + +static int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + int r; + + r = validate_stub(); + if (r < 0) + return r; + + STRV_FOREACH(bank, arg_banks) { + _cleanup_free_ char *b = NULL, *p = NULL, *s = NULL; + _cleanup_free_ void *h = NULL; + size_t l; + + b = strdup(*bank); + if (!b) + return log_oom(); + + if (asprintf(&p, "/sys/class/tpm/tpm0/pcr-%s/%" PRIu32, ascii_strlower(b), (uint32_t) TPM2_PCR_KERNEL_BOOT) < 0) + return log_oom(); + + r = read_virtual_file(p, 4096, &s, NULL); + if (r == -ENOENT) + continue; + if (r < 0) + return log_error_errno(r, "Failed to read '%s': %m", p); + + r = unhexmem(strstrip(s), &h, &l); + if (r < 0) + return log_error_errno(r, "Failed to decode PCR value '%s': %m", s); + + if (!sd_json_format_enabled(arg_json_format_flags)) { + _cleanup_free_ char *f = hexmem(h, l); + if (!f) + return log_oom(); + + if (bank == arg_banks) { + /* before the first line for each PCR, write a short descriptive text to + * stderr, and leave the primary content on stdout */ + fflush(stdout); + fprintf(stderr, "%s# PCR[%" PRIu32 "] %s%s%s\n", + ansi_grey(), + (uint32_t) TPM2_PCR_KERNEL_BOOT, + tpm2_pcr_index_to_string(TPM2_PCR_KERNEL_BOOT), + memeqzero(h, l) ? " (NOT SET!)" : "", + ansi_normal()); + fflush(stderr); + } + + printf("%" PRIu32 ":%s=%s\n", (uint32_t) TPM2_PCR_KERNEL_BOOT, b, f); + + } else { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *bv = NULL, *a = NULL; + + r = sd_json_buildo( + &bv, + SD_JSON_BUILD_PAIR_INTEGER("pcr", TPM2_PCR_KERNEL_BOOT), + SD_JSON_BUILD_PAIR_HEX("hash", h, l)); + if (r < 0) + return log_error_errno(r, "Failed to build JSON object: %m"); + + a = sd_json_variant_ref(sd_json_variant_by_key(v, b)); + + r = sd_json_variant_append_array(&a, bv); + if (r < 0) + return log_error_errno(r, "Failed to append PCR entry to JSON array: %m"); + + r = sd_json_variant_set_field(&v, b, a); + if (r < 0) + return log_error_errno(r, "Failed to add bank info to object: %m"); + } + } + + if (sd_json_format_enabled(arg_json_format_flags)) { + if (arg_json_format_flags & (SD_JSON_FORMAT_PRETTY|SD_JSON_FORMAT_PRETTY_AUTO)) + pager_open(arg_pager_flags); + + sd_json_variant_dump(v, arg_json_format_flags, stdout, NULL); + } + + return 0; +} + /* The PCR 11 state for one specific bank */ typedef struct PcrState { char *bank; @@ -1000,153 +1147,6 @@ static int verb_policy_digest(int argc, char *argv[], uintptr_t _data, void *use return build_policy_digest(/* sign= */ false); } -static int compare_reported_pcr_nr(uint32_t pcr, const char *varname, const char *description) { - _cleanup_free_ char *s = NULL; - uint32_t v; - int r; - - r = efi_get_variable_string(varname, &s); - if (r == -ENOENT) - return 0; - if (r < 0) - return log_error_errno(r, "Failed to read EFI variable '%s': %m", varname); - - r = safe_atou32(s, &v); - if (r < 0) - return log_error_errno(r, "Failed to parse EFI variable '%s': %s", varname, s); - - if (pcr != v) - log_warning("PCR number reported by stub for %s (%" PRIu32 ") different from our expectation (%" PRIu32 ").\n" - "The measurements are likely inconsistent.", description, v, pcr); - - return 0; -} - -static int validate_stub(void) { - uint64_t features; - bool found = false; - int r; - - if (!tpm2_is_fully_supported()) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Sorry, system lacks full TPM2 support."); - - r = efi_stub_get_features(&features); - if (r < 0) - return log_error_errno(r, "Unable to get stub features: %m"); - - if (!FLAGS_SET(features, EFI_STUB_FEATURE_THREE_PCRS)) - log_warning("Warning: current kernel image does not support measuring itself, the command line or initrd system extension images.\n" - "The PCR measurements seen are unlikely to be valid."); - - r = compare_reported_pcr_nr(TPM2_PCR_KERNEL_BOOT, EFI_LOADER_VARIABLE_STR("StubPcrKernelImage"), "kernel image"); - if (r < 0) - return r; - - STRV_FOREACH(bank, arg_banks) { - _cleanup_free_ char *b = NULL, *p = NULL; - - b = strdup(*bank); - if (!b) - return log_oom(); - - if (asprintf(&p, "/sys/class/tpm/tpm0/pcr-%s/", ascii_strlower(b)) < 0) - return log_oom(); - - if (access(p, F_OK) < 0) { - if (errno != ENOENT) - return log_error_errno(errno, "Failed to detect if '%s' exists: %m", b); - } else - found = true; - } - - if (!found) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "None of the select PCR banks appear to exist."); - - return 0; -} - -static int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; - int r; - - r = validate_stub(); - if (r < 0) - return r; - - STRV_FOREACH(bank, arg_banks) { - _cleanup_free_ char *b = NULL, *p = NULL, *s = NULL; - _cleanup_free_ void *h = NULL; - size_t l; - - b = strdup(*bank); - if (!b) - return log_oom(); - - if (asprintf(&p, "/sys/class/tpm/tpm0/pcr-%s/%" PRIu32, ascii_strlower(b), (uint32_t) TPM2_PCR_KERNEL_BOOT) < 0) - return log_oom(); - - r = read_virtual_file(p, 4096, &s, NULL); - if (r == -ENOENT) - continue; - if (r < 0) - return log_error_errno(r, "Failed to read '%s': %m", p); - - r = unhexmem(strstrip(s), &h, &l); - if (r < 0) - return log_error_errno(r, "Failed to decode PCR value '%s': %m", s); - - if (!sd_json_format_enabled(arg_json_format_flags)) { - _cleanup_free_ char *f = hexmem(h, l); - if (!f) - return log_oom(); - - if (bank == arg_banks) { - /* before the first line for each PCR, write a short descriptive text to - * stderr, and leave the primary content on stdout */ - fflush(stdout); - fprintf(stderr, "%s# PCR[%" PRIu32 "] %s%s%s\n", - ansi_grey(), - (uint32_t) TPM2_PCR_KERNEL_BOOT, - tpm2_pcr_index_to_string(TPM2_PCR_KERNEL_BOOT), - memeqzero(h, l) ? " (NOT SET!)" : "", - ansi_normal()); - fflush(stderr); - } - - printf("%" PRIu32 ":%s=%s\n", (uint32_t) TPM2_PCR_KERNEL_BOOT, b, f); - - } else { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *bv = NULL, *a = NULL; - - r = sd_json_buildo( - &bv, - SD_JSON_BUILD_PAIR_INTEGER("pcr", TPM2_PCR_KERNEL_BOOT), - SD_JSON_BUILD_PAIR_HEX("hash", h, l)); - if (r < 0) - return log_error_errno(r, "Failed to build JSON object: %m"); - - a = sd_json_variant_ref(sd_json_variant_by_key(v, b)); - - r = sd_json_variant_append_array(&a, bv); - if (r < 0) - return log_error_errno(r, "Failed to append PCR entry to JSON array: %m"); - - r = sd_json_variant_set_field(&v, b, a); - if (r < 0) - return log_error_errno(r, "Failed to add bank info to object: %m"); - } - } - - if (sd_json_format_enabled(arg_json_format_flags)) { - if (arg_json_format_flags & (SD_JSON_FORMAT_PRETTY|SD_JSON_FORMAT_PRETTY_AUTO)) - pager_open(arg_pager_flags); - - sd_json_variant_dump(v, arg_json_format_flags, stdout, NULL); - } - - return 0; -} - static int measure_main(int argc, char *argv[]) { static const Verb verbs[] = { { "help", VERB_ANY, VERB_ANY, 0, verb_help }, From ed469ffc07010bb4a6b4bc2e14c02873b700d151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 23 Apr 2026 09:21:29 +0200 Subject: [PATCH 12/15] measure: convert to the new option and verb parsers Previously, we had a nice third 'UKI PE Section' column with the section names. This is now moved into the help strings, which means that the nice alignment is lost. Previous behaviour could be restored by constructing the table manually, but I'm not sure if this is worth the trouble. Co-developed-by: Claude Opus 4.7 --- src/fundamental/uki.h | 2 +- src/measure/measure-tool.c | 430 ++++++++++++++++--------------------- 2 files changed, 191 insertions(+), 241 deletions(-) diff --git a/src/fundamental/uki.h b/src/fundamental/uki.h index 8d67b13b8b..627538a0eb 100644 --- a/src/fundamental/uki.h +++ b/src/fundamental/uki.h @@ -15,7 +15,7 @@ typedef enum UnifiedSection { UNIFIED_SECTION_DTB, UNIFIED_SECTION_UNAME, UNIFIED_SECTION_SBAT, - UNIFIED_SECTION_PCRSIG, + UNIFIED_SECTION_PCRSIG, /* This is is not measured actually */ UNIFIED_SECTION_PCRPKEY, UNIFIED_SECTION_PROFILE, UNIFIED_SECTION_DTBAUTO, diff --git a/src/measure/measure-tool.c b/src/measure/measure-tool.c index bd1339ddfa..af52e5aeb8 100644 --- a/src/measure/measure-tool.c +++ b/src/measure/measure-tool.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include "sd-json.h" @@ -12,10 +11,12 @@ #include "efivars.h" #include "fd-util.h" #include "fileio.h" +#include "format-table.h" #include "hexdecoct.h" #include "log.h" #include "main-func.h" #include "openssl-util.h" +#include "options.h" #include "pager.h" #include "parse-argument.h" #include "parse-util.h" @@ -65,71 +66,55 @@ STATIC_DESTRUCTOR_REGISTER(arg_sections, free_sections); static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *verbs = NULL, *options = NULL, *options2 = NULL; int r; r = terminal_urlify_man("systemd-measure", "1", &link); if (r < 0) return log_oom(); - printf("%1$s [OPTIONS...] COMMAND ...\n" - "\n%5$sPre-calculate and sign PCR hash for a unified kernel image (UKI).%6$s\n" - "\n%3$sCommands:%4$s\n" - " status Show current PCR values\n" - " calculate Calculate expected PCR values\n" - " sign Calculate and sign expected PCR values\n" - " policy-digest Calculate expected TPM2 policy digests\n" - "\n%3$sOptions:%4$s\n" - " -h --help Show this help\n" - " --version Print version\n" - " --no-pager Do not pipe output into a pager\n" - " -c --current Use current PCR values\n" - " --phase=PHASE Specify a boot phase to sign for\n" - " --bank=DIGEST Select TPM bank (SHA1, SHA256, SHA384, SHA512)\n" - " --tpm2-device=PATH Use specified TPM2 device\n" - " --private-key=KEY Private key (PEM) to sign with\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 for signing\n" - " --public-key=KEY Public key (PEM) to validate against\n" - " --certificate=PATH|URI\n" - " PEM certificate to use for signing, or a provider\n" - " specific designation 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" - " --json=MODE Output as JSON\n" - " -j Same as --json=pretty on tty, --json=short otherwise\n" - " --append=PATH Load specified JSON signature, and append new signature to it\n" - "\n%3$sUKI PE Section Options:%4$s %3$sUKI PE Section%4$s\n" - " --linux=PATH Path to Linux kernel image file %7$s .linux\n" - " --osrel=PATH Path to os-release file %7$s .osrel\n" - " --cmdline=PATH Path to file with kernel command line %7$s .cmdline\n" - " --initrd=PATH Path to initrd image file %7$s .initrd\n" - " --ucode=PATH Path to microcode image file %7$s .ucode\n" - " --splash=PATH Path to splash bitmap file %7$s .splash\n" - " --dtb=PATH Path to DeviceTree file %7$s .dtb\n" - " --dtbauto=PATH Path to DeviceTree file for auto selection %7$s .dtbauto\n" - " --uname=PATH Path to 'uname -r' file %7$s .uname\n" - " --sbat=PATH Path to SBAT file %7$s .sbat\n" - " --pcrpkey=PATH Path to public key for PCR signatures %7$s .pcrpkey\n" - " --profile=PATH Path to profile file %7$s .profile\n" - " --hwids=PATH Path to HWIDs file %7$s .hwids\n" - "\nSee the %2$s for details.\n", - program_invocation_short_name, - link, - ansi_underline(), - ansi_normal(), - ansi_highlight(), - ansi_normal(), - glyph(GLYPH_ARROW_RIGHT)); + r = verbs_get_help_table(&verbs); + if (r < 0) + return r; + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + + r = option_parser_get_help_table_group("UKI PE Section Options", &options2); + if (r < 0) + return r; + + (void) table_sync_column_widths(0, verbs, options, options2); + + printf("%s [OPTIONS...] COMMAND ...\n" + "\n%sPre-calculate and sign PCR hash for a unified kernel image (UKI).%s\n" + "\n%sCommands:%s\n", + program_invocation_short_name, + 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("\n%sUKI PE Section Options:%s\n", ansi_underline(), ansi_normal()); + + r = table_print_or_warn(options2); + 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_HIDDEN(help); static char *normalize_phase(const char *s) { _cleanup_strv_free_ char **l = NULL; @@ -146,187 +131,39 @@ static char *normalize_phase(const char *s) { return strv_join(strv_remove(l, ""), ":"); } -static int parse_argv(int argc, char *argv[]) { - enum { - ARG_VERSION = 0x100, - ARG_NO_PAGER, - _ARG_SECTION_FIRST, - ARG_LINUX = _ARG_SECTION_FIRST, - ARG_OSREL, - ARG_CMDLINE, - ARG_INITRD, - ARG_UCODE, - ARG_SPLASH, - ARG_DTB, - ARG_UNAME, - ARG_SBAT, - _ARG_PCRSIG, /* the .pcrsig section is not input for signing, hence not actually an argument here */ - ARG_PCRPKEY, - ARG_PROFILE, - ARG_DTBAUTO, - ARG_HWIDS, - _ARG_SECTION_LAST, - ARG_EFIFW = _ARG_SECTION_LAST, - ARG_BANK, - ARG_PRIVATE_KEY, - ARG_PRIVATE_KEY_SOURCE, - ARG_PUBLIC_KEY, - ARG_CERTIFICATE, - ARG_CERTIFICATE_SOURCE, - ARG_TPM2_DEVICE, - ARG_JSON, - ARG_PHASE, - ARG_APPEND, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "no-pager", no_argument, NULL, ARG_NO_PAGER }, - { "version", no_argument, NULL, ARG_VERSION }, - { "linux", required_argument, NULL, ARG_LINUX }, - { "osrel", required_argument, NULL, ARG_OSREL }, - { "cmdline", required_argument, NULL, ARG_CMDLINE }, - { "initrd", required_argument, NULL, ARG_INITRD }, - { "ucode", required_argument, NULL, ARG_UCODE }, - { "splash", required_argument, NULL, ARG_SPLASH }, - { "dtb", required_argument, NULL, ARG_DTB }, - { "dtbauto", required_argument, NULL, ARG_DTBAUTO }, - { "uname", required_argument, NULL, ARG_UNAME }, - { "sbat", required_argument, NULL, ARG_SBAT }, - { "pcrpkey", required_argument, NULL, ARG_PCRPKEY }, - { "profile", required_argument, NULL, ARG_PROFILE }, - { "hwids", required_argument, NULL, ARG_HWIDS }, - { "current", no_argument, NULL, 'c' }, - { "bank", required_argument, NULL, ARG_BANK }, - { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, - { "private-key", required_argument, NULL, ARG_PRIVATE_KEY }, - { "private-key-source", required_argument, NULL, ARG_PRIVATE_KEY_SOURCE }, - { "public-key", required_argument, NULL, ARG_PUBLIC_KEY }, - { "certificate", required_argument, NULL, ARG_CERTIFICATE }, - { "certificate-source", required_argument, NULL, ARG_CERTIFICATE_SOURCE }, - { "json", required_argument, NULL, ARG_JSON }, - { "phase", required_argument, NULL, ARG_PHASE }, - { "append", required_argument, NULL, ARG_APPEND }, - {} - }; - - int c, r; - +static int parse_argv(int argc, char *argv[], char ***ret_args) { assert(argc >= 0); assert(argv); + assert(ret_args); - /* Make sure the arguments list and the section list, stays in sync */ - assert_cc(_ARG_SECTION_FIRST + _UNIFIED_SECTION_MAX == _ARG_SECTION_LAST + 1); + OptionParser state = { argc, argv }; + const Option *opt; + const char *arg; + int r; - while ((c = getopt_long(argc, argv, "hjc", options, NULL)) >= 0) + FOREACH_OPTION_FULL(&state, c, &opt, &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_SECTION_FIRST..._ARG_SECTION_LAST: { - UnifiedSection section = c - _ARG_SECTION_FIRST; - - r = parse_path_argument(optarg, /* suppress_root= */ false, arg_sections + section); - if (r < 0) - return r; - break; - } - - case 'c': + OPTION('c', "current", NULL, + "Use current PCR values"): arg_current = true; break; - case ARG_BANK: { - const EVP_MD *implementation; - - implementation = EVP_get_digestbyname(optarg); - if (!implementation) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown bank '%s', refusing.", optarg); - - if (strv_extend(&arg_banks, EVP_MD_name(implementation)) < 0) - return log_oom(); - - break; - } - - case ARG_PRIVATE_KEY: - r = free_and_strdup_warn(&arg_private_key, optarg); - 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 ARG_PUBLIC_KEY: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_public_key); - if (r < 0) - return r; - - break; - - case ARG_CERTIFICATE: - r = free_and_strdup_warn(&arg_certificate, optarg); - if (r < 0) - return r; - break; - - case ARG_CERTIFICATE_SOURCE: - r = parse_openssl_certificate_source_argument( - optarg, - &arg_certificate_source, - &arg_certificate_source_type); - if (r < 0) - return r; - break; - - case ARG_TPM2_DEVICE: { - _cleanup_free_ char *device = NULL; - - if (streq(optarg, "list")) - return tpm2_list_devices(/* legend= */ true, /* quiet= */ false); - - if (!streq(optarg, "auto")) { - device = strdup(optarg); - if (!device) - return log_oom(); - } - - free_and_replace(arg_tpm2_device, device); - break; - } - - case 'j': - arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; - break; - - case ARG_JSON: - r = parse_json_argument(optarg, &arg_json_format_flags); - if (r <= 0) - return r; - - break; - - case ARG_PHASE: { + OPTION_LONG("phase", "PHASE", + "Specify a boot phase to sign for"): { char *n; - n = normalize_phase(optarg); + n = normalize_phase(arg); if (!n) return log_oom(); @@ -337,18 +174,133 @@ static int parse_argv(int argc, char *argv[]) { break; } - case ARG_APPEND: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_append); + OPTION_LONG("bank", "DIGEST", + "Select TPM bank (SHA1, SHA256, SHA384, SHA512)"): { + const EVP_MD *implementation; + + implementation = EVP_get_digestbyname(arg); + if (!implementation) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown bank '%s', refusing.", arg); + + if (strv_extend(&arg_banks, EVP_MD_name(implementation)) < 0) + return log_oom(); + + break; + } + + OPTION_LONG("tpm2-device", "PATH", + "Use specified TPM2 device"): { + _cleanup_free_ char *device = NULL; + + if (streq(arg, "list")) + return tpm2_list_devices(/* legend= */ true, /* quiet= */ false); + + if (!streq(arg, "auto")) { + device = strdup(arg); + if (!device) + return log_oom(); + } + + free_and_replace(arg_tpm2_device, device); + break; + } + + OPTION_COMMON_PRIVATE_KEY("Private key (PEM) to sign with"): + r = free_and_strdup_warn(&arg_private_key, arg); if (r < 0) return r; break; - case '?': - return -EINVAL; + OPTION_COMMON_PRIVATE_KEY_SOURCE: + r = parse_openssl_key_source_argument( + arg, + &arg_private_key_source, + &arg_private_key_source_type); + if (r < 0) + return r; - default: - assert_not_reached(); + break; + + OPTION_LONG("public-key", "KEY", + "Public key (PEM) to validate against"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_public_key); + if (r < 0) + return r; + + break; + + OPTION_COMMON_CERTIFICATE("PEM certificate to use for signing"): + r = free_and_strdup_warn(&arg_certificate, arg); + if (r < 0) + return r; + break; + + OPTION_COMMON_CERTIFICATE_SOURCE: + r = parse_openssl_certificate_source_argument( + arg, + &arg_certificate_source, + &arg_certificate_source_type); + if (r < 0) + return r; + break; + + OPTION_COMMON_JSON: + r = parse_json_argument(arg, &arg_json_format_flags); + if (r <= 0) + return r; + + break; + + OPTION_COMMON_LOWERCASE_J: + arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; + break; + + OPTION_LONG("append", "PATH", + "Load specified JSON signature, and append new signature to it"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_append); + if (r < 0) + return r; + + break; + + OPTION_GROUP("UKI PE Section Options"): {} + + OPTION_LONG_DATA("linux", "PATH", UNIFIED_SECTION_LINUX, + "Path to Linux kernel image file (→ .linux)"): {} + OPTION_LONG_DATA("osrel", "PATH", UNIFIED_SECTION_OSREL, + "Path to os-release file (→ .osrel)"): {} + OPTION_LONG_DATA("cmdline", "PATH", UNIFIED_SECTION_CMDLINE, + "Path to file with kernel command line (→ .cmdline)"): {} + OPTION_LONG_DATA("initrd", "PATH", UNIFIED_SECTION_INITRD, + "Path to initrd image file (→ .initrd)"): {} + OPTION_LONG_DATA("ucode", "PATH", UNIFIED_SECTION_UCODE, + "Path to microcode image file (→ .ucode)"): {} + OPTION_LONG_DATA("splash", "PATH", UNIFIED_SECTION_SPLASH, + "Path to splash bitmap file (→ .splash)"): {} + OPTION_LONG_DATA("dtb", "PATH", UNIFIED_SECTION_DTB, + "Path to DeviceTree file (→ .dtb)"): {} + OPTION_LONG_DATA("dtbauto", "PATH", UNIFIED_SECTION_DTBAUTO, + "Path to DeviceTree file for auto selection (→ .dtbauto)"): {} + OPTION_LONG_DATA("uname", "PATH", UNIFIED_SECTION_UNAME, + "Path to 'uname -r' file (→ .uname)"): {} + OPTION_LONG_DATA("sbat", "PATH", UNIFIED_SECTION_SBAT, + "Path to SBAT file (→ .sbat)"): {} + /* The .pcrsig section is not input for signing, hence not actually an argument here */ + OPTION_LONG_DATA("pcrpkey", "PATH", UNIFIED_SECTION_PCRPKEY, + "Path to public key for PCR signatures (→ .pcrpkey)"): {} + OPTION_LONG_DATA("profile", "PATH", UNIFIED_SECTION_PROFILE, + "Path to profile file (→ .profile)"): {} + OPTION_LONG_DATA("hwids", "PATH", UNIFIED_SECTION_HWIDS, + "Path to HWIDs file (→ .hwids)"): + /* Make sure that if new sections are added, the list here is updated. */ + assert_cc(UNIFIED_SECTION_HWIDS + 1 + 1 /* FIXME */ == _UNIFIED_SECTION_MAX); + assert(opt->data < _UNIFIED_SECTION_MAX); + + r = parse_path_argument(arg, /* suppress_root= */ false, arg_sections + opt->data); + if (r < 0) + return r; + break; } if (arg_public_key && arg_certificate) @@ -390,6 +342,8 @@ static int parse_argv(int argc, char *argv[]) { return log_oom(); log_debug("Measuring boot phases: %s", j); + + *ret_args = option_parser_get_args(&state); return 1; } @@ -458,6 +412,8 @@ static int validate_stub(void) { return 0; } +VERB(verb_status, "status", NULL, VERB_ANY, 1, VERB_DEFAULT, + "Show current PCR values"); static int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; int r; @@ -858,6 +814,8 @@ static void pcr_states_restore(PcrState *pcr_states, size_t n) { } } +VERB_NOARG(verb_calculate, "calculate", + "Calculate expected PCR values"); static int verb_calculate(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *w = NULL; _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL; @@ -1139,37 +1097,29 @@ static int build_policy_digest(bool sign) { return 0; } +VERB_NOARG(verb_sign, "sign", + "Calculate and sign expected PCR values"); static int verb_sign(int argc, char *argv[], uintptr_t _data, void *userdata) { return build_policy_digest(/* sign= */ true); } +VERB_NOARG(verb_policy_digest, "policy-digest", + "Calculate expected TPM2 policy digests"); static int verb_policy_digest(int argc, char *argv[], uintptr_t _data, void *userdata) { return build_policy_digest(/* sign= */ false); } -static int measure_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 }, - { "calculate", VERB_ANY, 1, 0, verb_calculate }, - { "policy-digest", VERB_ANY, 1, 0, verb_policy_digest }, - { "sign", VERB_ANY, 1, 0, verb_sign }, - {} - }; - - return dispatch_verb(argc, argv, verbs, NULL); -} - static int run(int argc, char *argv[]) { int r; log_setup(); - r = parse_argv(argc, argv); + char **args = NULL; + r = parse_argv(argc, argv, &args); if (r <= 0) return r; - return measure_main(argc, argv); + return dispatch_verb_with_args(args, NULL); } DEFINE_MAIN_FUNCTION(run); From 6fdc0f96f778bfce6dec92adf3031660bb1daf12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 23 Apr 2026 11:48:56 +0200 Subject: [PATCH 13/15] measure: also measure forgotten .efifw section --- src/measure/measure-tool.c | 6 ++++-- test/units/TEST-70-TPM2.measure.sh | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/measure/measure-tool.c b/src/measure/measure-tool.c index af52e5aeb8..09c04d888c 100644 --- a/src/measure/measure-tool.c +++ b/src/measure/measure-tool.c @@ -292,9 +292,11 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { OPTION_LONG_DATA("profile", "PATH", UNIFIED_SECTION_PROFILE, "Path to profile file (→ .profile)"): {} OPTION_LONG_DATA("hwids", "PATH", UNIFIED_SECTION_HWIDS, - "Path to HWIDs file (→ .hwids)"): + "Path to HWIDs file (→ .hwids)"): {} + OPTION_LONG_DATA("efifw", "PATH", UNIFIED_SECTION_EFIFW, + "Path to EFI firmware file (→ .efifw)"): {} /* Make sure that if new sections are added, the list here is updated. */ - assert_cc(UNIFIED_SECTION_HWIDS + 1 + 1 /* FIXME */ == _UNIFIED_SECTION_MAX); + assert_cc(UNIFIED_SECTION_EFIFW + 1 == _UNIFIED_SECTION_MAX); assert(opt->data < _UNIFIED_SECTION_MAX); r = parse_path_argument(arg, /* suppress_root= */ false, arg_sections + opt->data); diff --git a/test/units/TEST-70-TPM2.measure.sh b/test/units/TEST-70-TPM2.measure.sh index bf30bd57b3..30fa51e521 100755 --- a/test/units/TEST-70-TPM2.measure.sh +++ b/test/units/TEST-70-TPM2.measure.sh @@ -45,6 +45,12 @@ cat >/tmp/result.json </tmp/result < Date: Thu, 23 Apr 2026 11:57:18 +0200 Subject: [PATCH 14/15] TODO: add one more entry This is something that should be fixed for usability, but it's something between a missing feature and a bug. Since nobody has complained about this, it probably can wait. --- TODO.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/TODO.md b/TODO.md index a178e8b9fb..d77225c572 100644 --- a/TODO.md +++ b/TODO.md @@ -2411,10 +2411,12 @@ SPDX-License-Identifier: LGPL-2.1-or-later - **systemd-measure tool:** - pre-calculate PCR 12 (command line) + PCR 13 (sysext) the same way we can precalculate PCR 11 - -- systemd-measure: add --pcrpkey-auto as an alternative to --pcrpkey=, where it - would just use the same public key specified with --public-key= (or the one - automatically derived from --private-key=). + - add --pcrpkey-auto as an alternative to --pcrpkey=, where it would just use + the same public key specified with --public-key= (or the one automatically + derived from --private-key=). + - allow multiple --initrd=, --efifw=, --dtbauto=, etc., params and pad and + concatenate the contents in the same way that ukify does, so we end up with + the expected measurement. - systemd-mount should only consider modern file systems when mounting, similar to systemd-dissect From b4764836396465521181a1d8d2817d078cec0864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 23 Apr 2026 18:46:57 +0200 Subject: [PATCH 15/15] various: use empty block not break after OPTION_GROUP Use the same style everywhere. --- src/bootctl/bootctl.c | 6 ++---- src/imds/imdsd.c | 3 +-- src/sysusers/sysusers.c | 3 +-- src/test/test-options.c | 3 +-- src/tmpfiles/tmpfiles.c | 3 +-- src/vmspawn/vmspawn.c | 30 ++++++++++-------------------- 6 files changed, 16 insertions(+), 32 deletions(-) diff --git a/src/bootctl/bootctl.c b/src/bootctl/bootctl.c index 9ec2596285..59a93d07c3 100644 --- a/src/bootctl/bootctl.c +++ b/src/bootctl/bootctl.c @@ -422,8 +422,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { FOREACH_OPTION(&state, c, &arg, /* on_error= */ return c) switch (c) { - OPTION_GROUP("Block Device Discovery Commands"): - break; + OPTION_GROUP("Block Device Discovery Commands"): {} OPTION('p', "print-esp-path", NULL, "Print path to the EFI System Partition mount point"): {} OPTION_LONG("print-path", NULL, /* help= */ NULL): /* Compatibility alias */ @@ -455,8 +454,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { arg_print_efi_architecture = true; break; - OPTION_GROUP("Options"): - break; + OPTION_GROUP("Options"): {} OPTION_COMMON_HELP: return help(); diff --git a/src/imds/imdsd.c b/src/imds/imdsd.c index 8ab3498656..b3d1771161 100644 --- a/src/imds/imdsd.c +++ b/src/imds/imdsd.c @@ -2348,8 +2348,7 @@ static int parse_argv(int argc, char *argv[]) { break; /* The following all configure endpoint information explicitly */ - OPTION_GROUP("Manual Endpoint Configuration"): - break; + OPTION_GROUP("Manual Endpoint Configuration"): {} OPTION_LONG("vendor", "VENDOR", "Specify IMDS vendor literally"): if (isempty(arg)) { diff --git a/src/sysusers/sysusers.c b/src/sysusers/sysusers.c index 1f79e2face..ff391cb316 100644 --- a/src/sysusers/sysusers.c +++ b/src/sysusers/sysusers.c @@ -2121,8 +2121,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { OPTION_COMMON_VERSION: return version(); - OPTION_GROUP("Options"): - break; + OPTION_GROUP("Options"): {} OPTION_LONG("root", "PATH", "Operate on an alternate filesystem root"): r = parse_path_argument(arg, /* suppress_root= */ false, &arg_root); diff --git a/src/test/test-options.c b/src/test/test-options.c index 687bafd4a2..1c9073b7a5 100644 --- a/src/test/test-options.c +++ b/src/test/test-options.c @@ -811,8 +811,7 @@ static void test_macros_parse_one( break; /* OPTION_GROUP: group marker (never returned by parser) */ - OPTION_GROUP("Advanced"): - break; + OPTION_GROUP("Advanced"): {} /* OPTION_LONG: long only, in the "Advanced" group */ OPTION_LONG("debug", NULL, "Enable debug mode"): diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c index 9975ffca39..0c133b08c8 100644 --- a/src/tmpfiles/tmpfiles.c +++ b/src/tmpfiles/tmpfiles.c @@ -4225,8 +4225,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { OPTION_COMMON_VERSION: return version(); - OPTION_GROUP("Options"): - break; + OPTION_GROUP("Options"): {} OPTION_LONG("user", NULL, "Execute user configuration"): arg_runtime_scope = RUNTIME_SCOPE_USER; diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index fa8c3402ff..fef8353a2c 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -359,8 +359,7 @@ static int parse_argv(int argc, char *argv[]) { arg_runtime_scope = RUNTIME_SCOPE_USER; break; - OPTION_GROUP("Image"): - break; + OPTION_GROUP("Image"): {} OPTION('D', "directory", "PATH", "Root directory for the VM"): r = parse_path_argument(arg, /* suppress_root= */ false, &arg_directory); @@ -393,8 +392,7 @@ static int parse_argv(int argc, char *argv[]) { "Invalid image disk type: %s", arg); break; - OPTION_GROUP("Host Configuration"): - break; + OPTION_GROUP("Host Configuration"): {} OPTION_LONG("cpus", "CPUS", "Configure number of CPUs in guest"): {} OPTION_LONG("qemu-smp", "CPUS", /* help= */ NULL): /* Compat alias */ @@ -655,8 +653,7 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(r, "Failed to parse --grow-image= parameter: %s", arg); break; - OPTION_GROUP("Execution"): - break; + OPTION_GROUP("Execution"): {} OPTION('s', "smbios11", "STRING", "Pass an arbitrary SMBIOS Type #11 string to the VM"): if (isempty(arg)) { @@ -677,8 +674,7 @@ static int parse_argv(int argc, char *argv[]) { return r; break; - OPTION_GROUP("System Identity"): - break; + OPTION_GROUP("System Identity"): {} OPTION('M', "machine", "NAME", "Set the machine name for the VM"): if (isempty(arg)) @@ -702,8 +698,7 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(r, "Invalid UUID: %s", arg); break; - OPTION_GROUP("Properties"): - break; + OPTION_GROUP("Properties"): {} OPTION('S', "slice", "SLICE", "Place the VM in the specified slice"): { _cleanup_free_ char *mangled = NULL; @@ -732,8 +727,7 @@ static int parse_argv(int argc, char *argv[]) { arg_keep_unit = true; break; - OPTION_GROUP("User Namespacing"): - break; + OPTION_GROUP("User Namespacing"): {} OPTION_LONG("private-users", "UIDBASE[:NUIDS]", "Configure the UID/GID range to map into the virtiofsd namespace"): @@ -742,8 +736,7 @@ static int parse_argv(int argc, char *argv[]) { return r; break; - OPTION_GROUP("Mounts"): - break; + OPTION_GROUP("Mounts"): {} OPTION_LONG("bind", "SOURCE[:TARGET]", "Mount a file or directory from the host into the VM"): {} OPTION_LONG("bind-ro", "SOURCE[:TARGET]", "Mount a file or directory, but read-only"): { @@ -835,8 +828,7 @@ static int parse_argv(int argc, char *argv[]) { return log_oom(); break; - OPTION_GROUP("Integration"): - break; + OPTION_GROUP("Integration"): {} OPTION_LONG("forward-journal", "FILE|DIR", "Forward the VM's journal to the host"): r = parse_path_argument(arg, /* suppress_root= */ false, &arg_forward_journal); @@ -864,8 +856,7 @@ static int parse_argv(int argc, char *argv[]) { return r; break; - OPTION_GROUP("Input/Output"): - break; + OPTION_GROUP("Input/Output"): {} OPTION_LONG("console", "MODE", "Console mode (interactive, native, gui, read-only or headless)"): @@ -890,8 +881,7 @@ static int parse_argv(int argc, char *argv[]) { return r; break; - OPTION_GROUP("Credentials"): - break; + OPTION_GROUP("Credentials"): {} OPTION_LONG("set-credential", "ID:VALUE", "Pass a credential with literal value to the VM"): r = machine_credential_set(&arg_credentials, arg);