From 6adc9abc4a6958741e9b0242cbee22912e128745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sat, 4 Apr 2026 19:43:04 +0200 Subject: [PATCH 01/13] imds: convert to the new option parser Cosmetic changes in --help output only. Co-developed-by: Claude Opus 4.6 --- src/imds/imds-tool.c | 121 ++++++++++++++++++------------------------- 1 file changed, 51 insertions(+), 70 deletions(-) diff --git a/src/imds/imds-tool.c b/src/imds/imds-tool.c index ff5a9b317a..0d71801aec 100644 --- a/src/imds/imds-tool.c +++ b/src/imds/imds-tool.c @@ -1,7 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include -#include #include #include @@ -26,6 +25,7 @@ #include "json-util.h" #include "log.h" #include "main-func.h" +#include "options.h" #include "parse-argument.h" #include "pcrextend-util.h" #include "pretty-print.h" @@ -51,148 +51,129 @@ STATIC_DESTRUCTOR_REGISTER(arg_key, freep); static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *options = NULL; int r; r = terminal_urlify_man("systemd-imds", "1", &link); if (r < 0) return log_oom(); + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + printf("%s [OPTIONS...] [KEY]\n" - "\n%sIMDS data acquisition.%s\n\n" - " -h --help Show this help\n" - " --version Show package version\n" - " -K --well-known=[hostname|region|zone|ipv4-public|ipv6-public|ssh-key|\n" - " userdata|userdata-base|userdata-base64]\n" - " Select well-known key/base\n" - " --refresh=SEC Set minimum freshness time for returned data\n" - " --cache=no Disable cache use\n" - " -u --userdata Dump user data\n" - " --import Import system credentials from IMDS userdata\n" - " and place them in /run/credstore/\n" - "\nSee the %s for details.\n", + "\n%sIMDS data acquisition.%s\n\n", program_invocation_short_name, ansi_highlight(), - ansi_normal(), - link); + ansi_normal()); + r = table_print_or_warn(options); + if (r < 0) + return r; + + printf("\nSee the %s for details.\n", link); return 0; } static int parse_argv(int argc, char *argv[]) { - - enum { - ARG_VERSION = 0x100, - ARG_REFRESH, - ARG_CACHE, - ARG_IMPORT, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "well-known", required_argument, NULL, 'K' }, - { "refresh", required_argument, NULL, ARG_REFRESH }, - { "cache", required_argument, NULL, ARG_CACHE }, - { "userdata", no_argument, NULL, 'u' }, - { "import", no_argument, NULL, ARG_IMPORT }, - {} - }; - - int c, r; + int r; assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "hK:u", 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 'K': { - if (isempty(optarg)) { + OPTION('K', "well-known", "KEY", + "Select well-known key/base, one of:" + " hostname, region, zone, ipv4-public, ipv6-public, ssh-key," + " userdata, userdata-base, userdata-base64"): { + if (isempty(arg)) { arg_well_known = _IMDS_WELL_KNOWN_INVALID; break; } - if (streq(optarg, "help")) + if (streq(arg, "help")) return DUMP_STRING_TABLE(imds_well_known, ImdsWellKnown, _IMDS_WELL_KNOWN_MAX); - ImdsWellKnown wk = imds_well_known_from_string(optarg); + ImdsWellKnown wk = imds_well_known_from_string(arg); if (wk < 0) - return log_error_errno(wk, "Failed to parse --well-known= argument: %s", optarg); + return log_error_errno(wk, "Failed to parse --well-known= argument: %s", arg); arg_well_known = wk; break; } - case ARG_CACHE: - r = parse_tristate_argument_with_auto("--cache=", optarg, &arg_cache); - if (r < 0) - return r; - - break; - - case ARG_REFRESH: { - if (isempty(optarg)) { + OPTION_LONG("refresh", "SEC", "Set minimum freshness time for returned data"): { + if (isempty(arg)) { arg_refresh_usec_set = false; break; } usec_t t; - r = parse_sec(optarg, &t); + r = parse_sec(arg, &t); if (r < 0) - return log_error_errno(r, "Failed to parse refresh timeout: %s", optarg); + return log_error_errno(r, "Failed to parse refresh timeout: %s", arg); arg_refresh_usec = t; arg_refresh_usec_set = true; break; } - case 'u': + OPTION_LONG("cache", "BOOL", "Control cache use"): + r = parse_tristate_argument_with_auto("--cache=", arg, &arg_cache); + if (r < 0) + return r; + break; + + OPTION('u', "userdata", NULL, "Dump user data"): arg_action = ACTION_USERDATA; break; - case ARG_IMPORT: + OPTION_LONG("import", NULL, + "Import system credentials from IMDS userdata" + " and place them in /run/credstore/"): arg_action = ACTION_IMPORT; break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } - } + + char **args = option_parser_get_args(&state); + size_t n_args = option_parser_get_n_args(&state); if (IN_SET(arg_action, ACTION_USERDATA, ACTION_IMPORT)) { - if (argc != optind) + if (n_args != 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No parameters expected."); } else { assert(arg_action < 0); - if (argc > optind + 1) + if (n_args > 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "None or one argument expected."); - if (argc == optind && arg_well_known < 0) + if (n_args == 0 && arg_well_known < 0) arg_action = ACTION_SUMMARY; else { if (arg_well_known < 0) arg_well_known = IMDS_BASE; - if (argc > optind) { - if (!imds_key_is_valid(argv[optind])) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified IMDS key is not valid, refusing: %s", argv[optind]); + if (n_args > 0) { + if (!imds_key_is_valid(args[0])) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified IMDS key is not valid, refusing: %s", args[0]); if (!imds_well_known_can_suffix(arg_well_known)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Well known key '%s' does not take a key suffix, refusing.", imds_well_known_to_string(arg_well_known)); - r = free_and_strdup_warn(&arg_key, argv[optind]); + r = free_and_strdup_warn(&arg_key, args[0]); if (r < 0) return r; } From e59fea008841c0050297692109922b4a34fe8dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sat, 4 Apr 2026 20:38:52 +0200 Subject: [PATCH 02/13] shared/options: quote the metavar in --help output imdsd uses --extra-header='NAME: VALUE'. We could include the quotes in the metavar string, but I think it's nicer to only do that in the printed output, so that later, when we add introspection, the value there will not include the quotes. --- src/shared/options.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/shared/options.c b/src/shared/options.c index e6b82fe34d..f94ba28432 100644 --- a/src/shared/options.c +++ b/src/shared/options.c @@ -297,6 +297,7 @@ int _option_parser_get_help_table( * "=" is shown only when a long option is defined: -l --long=ARG, --long=ARG, -s ARG. */ bool need_eq = option_takes_arg(opt) && opt->long_code; + bool need_quote = opt->metavar && strchr(opt->metavar, ' '); _cleanup_free_ char *s = strjoin( " ", sc, @@ -305,7 +306,9 @@ int _option_parser_get_help_table( strempty(opt->long_code), option_arg_optional(opt) ? "[" : "", need_eq ? "=" : "", + need_quote ? "'" : "", strempty(opt->metavar), + need_quote ? "'" : "", option_arg_optional(opt) ? "]" : ""); if (!s) return log_oom(); From e3d25c82abc56e95d00fd844dca46877ac922b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sat, 4 Apr 2026 19:58:52 +0200 Subject: [PATCH 03/13] imdsd: convert to the new option parser Previously -w was ambiguously described in --help as taking an argument, but it is in fact an argumentless alias for --wait=yes. Co-developed-by: Claude Opus 4.6 --- src/imds/imdsd.c | 297 +++++++++++++++++++---------------------------- 1 file changed, 120 insertions(+), 177 deletions(-) diff --git a/src/imds/imdsd.c b/src/imds/imdsd.c index 68a03b7232..c0ab089830 100644 --- a/src/imds/imdsd.c +++ b/src/imds/imdsd.c @@ -1,7 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include -#include #include #include #include @@ -26,6 +25,7 @@ #include "event-util.h" #include "fd-util.h" #include "format-ifname.h" +#include "format-table.h" #include "hash-funcs.h" #include "hashmap.h" #include "imds-util.h" @@ -36,6 +36,7 @@ #include "log.h" #include "main-func.h" #include "netlink-util.h" +#include "options.h" #include "parse-argument.h" #include "parse-util.h" #include "path-util.h" @@ -2203,50 +2204,45 @@ static int vl_server(void) { static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *options = NULL, *endpoint_options = NULL; int r; r = terminal_urlify_man("systemd-imdsd@.service", "8", &link); if (r < 0) return log_oom(); + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + + r = option_parser_get_help_table_group("Manual Endpoint Configuration", &endpoint_options); + if (r < 0) + return r; + + (void) table_sync_column_widths(0, options, endpoint_options); + printf("%1$s [OPTIONS...] KEY\n" - "\n%5$sLow-level IMDS data acquisition.%6$s\n" - "\n%3$sOptions:%4$s\n" - " -h --help Show this help\n" - " --version Show package version\n" - " -i --interface=INTERFACE\n" - " Use the specified interface\n" - " --refresh=SEC Set token refresh time\n" - " --fwmark=INTEGER Choose firewall mark for HTTP traffic\n" - " --cache=no Disable cache use\n" - " -w --wait=yes Wait for connectivity\n" - " -K --well-known= Select well-known key\n" - " --setup-network Generate .network and .rr files\n" - "\n%3$sManual Endpoint Configuration:%4$s\n" - " --vendor=VENDOR Specify IMDS vendor literally\n" - " --token-url=URL URL for acquiring token\n" - " --refresh-header-name=NAME\n" - " Header name for passing refresh time\n" - " --data-url=URL Base URL for acquiring data\n" - " --data-url-suffix=STRING\n" - " Suffix to append to data URL\n" - " --token-header-name=NAME\n" - " Header name for passing token string\n" - " --extra-header='NAME: VALUE'\n" - " Additional header to pass to data transfer\n" - " --address-ipv4=ADDRESS\n" - " --address-ipv6=ADDRESS\n" - " Configure the IPv4 and IPv6 address of the IMDS server\n" - " --well-known-key=NAME:KEY\n" - " Configure the location of well-known keys\n" - "\nSee the %2$s for details.\n", + "\n%2$sLow-level IMDS data acquisition.%3$s\n" + "\n%4$sOptions:%5$s\n", program_invocation_short_name, - link, - ansi_underline(), - ansi_normal(), ansi_highlight(), + ansi_normal(), + ansi_underline(), ansi_normal()); + r = table_print_or_warn(options); + if (r < 0) + return r; + + printf("\n%sManual Endpoint Configuration:%s\n", + ansi_underline(), + ansi_normal()); + + r = table_print_or_warn(endpoint_options); + if (r < 0) + return r; + + printf("\nSee the %s for details.\n", link); return 0; } @@ -2259,91 +2255,48 @@ static bool http_header_valid(const char *a) { } static int parse_argv(int argc, char *argv[]) { - - enum { - ARG_VERSION = 0x100, - ARG_REFRESH, - ARG_FWMARK, - ARG_CACHE, - ARG_WAIT, - ARG_VENDOR, - ARG_TOKEN_URL, - ARG_REFRESH_HEADER_NAME, - ARG_DATA_URL, - ARG_DATA_URL_SUFFIX, - ARG_TOKEN_HEADER_NAME, - ARG_EXTRA_HEADER, - ARG_ADDRESS_IPV4, - ARG_ADDRESS_IPV6, - ARG_WELL_KNOWN_KEY, - ARG_SETUP_NETWORK, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "interface", required_argument, NULL, 'i' }, - { "refresh", required_argument, NULL, ARG_REFRESH }, - { "fwmark", required_argument, NULL, ARG_FWMARK }, - { "cache", required_argument, NULL, ARG_CACHE }, - { "wait", required_argument, NULL, ARG_WAIT }, - { "well-known", required_argument, NULL, 'K' }, - { "setup-network", no_argument, NULL, ARG_SETUP_NETWORK }, - - /* The following all configure endpoint information explicitly */ - { "vendor", required_argument, NULL, ARG_VENDOR }, - { "token-url", required_argument, NULL, ARG_TOKEN_URL }, - { "refresh-header-name", required_argument, NULL, ARG_REFRESH_HEADER_NAME }, - { "data-url", required_argument, NULL, ARG_DATA_URL }, - { "data-url-suffix", required_argument, NULL, ARG_DATA_URL_SUFFIX }, - { "token-header-name", required_argument, NULL, ARG_TOKEN_HEADER_NAME }, - { "extra-header", required_argument, NULL, ARG_EXTRA_HEADER }, - { "address-ipv4", required_argument, NULL, ARG_ADDRESS_IPV4 }, - { "address-ipv6", required_argument, NULL, ARG_ADDRESS_IPV6 }, - { "well-known-key", required_argument, NULL, ARG_WELL_KNOWN_KEY }, - {} - }; - - int c, r; + int r; assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "hi:wK:", 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 'i': - if (isempty(optarg)) { + OPTION('i', "interface", "INTERFACE", "Use the specified interface"): + if (isempty(arg)) { arg_ifname = mfree(arg_ifname); break; } - if (!ifname_valid_full(optarg, IFNAME_VALID_ALTERNATIVE|IFNAME_VALID_NUMERIC)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Interface name '%s' is not valid.", optarg); + if (!ifname_valid_full(arg, IFNAME_VALID_ALTERNATIVE|IFNAME_VALID_NUMERIC)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Interface name '%s' is not valid.", arg); - r = free_and_strdup_warn(&arg_ifname, optarg); + r = free_and_strdup_warn(&arg_ifname, arg); if (r < 0) return r; break; - case ARG_REFRESH: { - if (isempty(optarg)) { + OPTION_LONG("refresh", "SEC", "Set token refresh time"): { + if (isempty(arg)) { arg_refresh_usec = REFRESH_USEC_DEFAULT; break; } usec_t t; - r = parse_sec(optarg, &t); + r = parse_sec(arg, &t); if (r < 0) - return log_error_errno(r, "Failed to parse refresh timeout: %s", optarg); + return log_error_errno(r, "Failed to parse refresh timeout: %s", arg); if (t < REFRESH_USEC_MIN) { log_warning("Increasing specified refresh time to %s, lower values are not supported.", FORMAT_TIMESPAN(REFRESH_USEC_MIN, 0)); arg_refresh_usec = REFRESH_USEC_MIN; @@ -2352,50 +2305,48 @@ static int parse_argv(int argc, char *argv[]) { break; } - case ARG_FWMARK: - if (isempty(optarg)) { + OPTION_LONG("fwmark", "INTEGER", "Choose firewall mark for HTTP traffic"): + if (isempty(arg)) { arg_fwmark_set = false; break; } - if (streq(optarg, "default")) { + if (streq(arg, "default")) { arg_fwmark = FWMARK_DEFAULT; arg_fwmark_set = true; break; } - r = safe_atou32(optarg, &arg_fwmark); + r = safe_atou32(arg, &arg_fwmark); if (r < 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse --fwmark= parameter: %s", optarg); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse --fwmark= parameter: %s", arg); arg_fwmark_set = true; break; - case ARG_CACHE: - r = parse_boolean_argument("--cache", optarg, &arg_cache); + OPTION_LONG("cache", "BOOL", "Enable/disable cache use"): + r = parse_boolean_argument("--cache", arg, &arg_cache); if (r < 0) return r; - break; - case ARG_WAIT: - r = parse_boolean_argument("--wait", optarg, &arg_wait); + OPTION_LONG("wait", "BOOL", "Whether to wait for connectivity"): + r = parse_boolean_argument("--wait", arg, &arg_wait); if (r < 0) return r; - break; - case 'w': + OPTION_SHORT('w', NULL, "Same as --wait=yes"): arg_wait = true; break; - case 'K': { - if (isempty(optarg)) { + OPTION('K', "well-known", "KEY", "Select well-known key"): { + if (isempty(arg)) { arg_well_known = _IMDS_WELL_KNOWN_INVALID; break; } - ImdsWellKnown wk = imds_well_known_from_string(optarg); + ImdsWellKnown wk = imds_well_known_from_string(arg); if (wk < 0) return log_error_errno(wk, "Failed to parse --well-known= parameter: %m"); @@ -2403,146 +2354,148 @@ static int parse_argv(int argc, char *argv[]) { break; } - case ARG_VENDOR: - if (isempty(optarg)) { + OPTION_LONG("setup-network", NULL, "Generate .network and .rr files"): + arg_setup_network = true; + break; + + /* The following all configure endpoint information explicitly */ + OPTION_GROUP("Manual Endpoint Configuration"): + break; + + OPTION_LONG("vendor", "VENDOR", "Specify IMDS vendor literally"): + if (isempty(arg)) { arg_vendor = mfree(arg_vendor); break; } - r = free_and_strdup_warn(&arg_vendor, optarg); + r = free_and_strdup_warn(&arg_vendor, arg); if (r < 0) return r; break; - case ARG_TOKEN_URL: - if (isempty(optarg)) { + OPTION_LONG("token-url", "URL", "URL for acquiring token"): + if (isempty(arg)) { arg_token_url = mfree(arg_token_url); break; } - if (!http_url_is_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid URL: %s", optarg); + if (!http_url_is_valid(arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid URL: %s", arg); - r = free_and_strdup_warn(&arg_token_url, optarg); + r = free_and_strdup_warn(&arg_token_url, arg); if (r < 0) return r; - break; - case ARG_REFRESH_HEADER_NAME: - if (isempty(optarg)) { + OPTION_LONG("refresh-header-name", "NAME", "Header name for passing refresh time"): + if (isempty(arg)) { arg_refresh_header_name = mfree(arg_refresh_header_name); break; } - if (!http_header_name_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid HTTP header name: %s", optarg); + if (!http_header_name_valid(arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid HTTP header name: %s", arg); - r = free_and_strdup_warn(&arg_refresh_header_name, optarg); + r = free_and_strdup_warn(&arg_refresh_header_name, arg); if (r < 0) return r; - break; - case ARG_DATA_URL: - if (isempty(optarg)) { + OPTION_LONG("data-url", "URL", "Base URL for acquiring data"): + if (isempty(arg)) { arg_data_url = mfree(arg_data_url); break; } - if (!http_url_is_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid URL: %s", optarg); + if (!http_url_is_valid(arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid URL: %s", arg); - r = free_and_strdup_warn(&arg_data_url, optarg); + r = free_and_strdup_warn(&arg_data_url, arg); if (r < 0) return r; - break; - case ARG_DATA_URL_SUFFIX: - if (isempty(optarg)) { + OPTION_LONG("data-url-suffix", "STRING", "Suffix to append to data URL"): + if (isempty(arg)) { arg_data_url_suffix = mfree(arg_data_url_suffix); break; } - if (!ascii_is_valid(optarg) || string_has_cc(optarg, /* ok= */ NULL)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid URL suffix: %s", optarg); + if (!ascii_is_valid(arg) || string_has_cc(arg, /* ok= */ NULL)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid URL suffix: %s", arg); - r = free_and_strdup_warn(&arg_data_url_suffix, optarg); + r = free_and_strdup_warn(&arg_data_url_suffix, arg); if (r < 0) return r; - break; - case ARG_TOKEN_HEADER_NAME: - if (isempty(optarg)) { + OPTION_LONG("token-header-name", "NAME", "Header name for passing token string"): + if (isempty(arg)) { arg_token_header_name = mfree(arg_token_header_name); break; } - if (!http_header_name_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid HTTP header name: %s", optarg); + if (!http_header_name_valid(arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid HTTP header name: %s", arg); - r = free_and_strdup_warn(&arg_token_header_name, optarg); + r = free_and_strdup_warn(&arg_token_header_name, arg); if (r < 0) return r; - break; - case ARG_EXTRA_HEADER: - if (isempty(optarg)) { + OPTION_LONG("extra-header", "NAME: VALUE", "Additional header to pass to data transfer"): + if (isempty(arg)) { arg_extra_header = strv_free(arg_extra_header); break; } - if (!http_header_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid HTTP header: %s", optarg); + if (!http_header_valid(arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid HTTP header: %s", arg); - if (strv_extend(&arg_extra_header, optarg) < 0) + if (strv_extend(&arg_extra_header, arg) < 0) return log_oom(); - break; - case ARG_ADDRESS_IPV4: { - if (isempty(optarg)) { + OPTION_LONG("address-ipv4", "ADDRESS", "Configure IPv4 address of the IMDS server"): { + if (isempty(arg)) { arg_address_ipv4 = (struct in_addr) {}; break; } union in_addr_union u; - r = in_addr_from_string(AF_INET, optarg, &u); + r = in_addr_from_string(AF_INET, arg, &u); if (r < 0) - return log_error_errno(r, "Failed to parse IPv4 address: %s", optarg); + return log_error_errno(r, "Failed to parse IPv4 address: %s", arg); arg_address_ipv4 = u.in; break; } - case ARG_ADDRESS_IPV6: { - if (isempty(optarg)) { + OPTION_LONG("address-ipv6", "ADDRESS", "Configure IPv6 address of the IMDS server"): { + if (isempty(arg)) { arg_address_ipv6 = (struct in6_addr) {}; break; } union in_addr_union u; - r = in_addr_from_string(AF_INET6, optarg, &u); + r = in_addr_from_string(AF_INET6, arg, &u); if (r < 0) - return log_error_errno(r, "Failed to parse IPv6 address: %s", optarg); + return log_error_errno(r, "Failed to parse IPv6 address: %s", arg); arg_address_ipv6 = u.in6; break; } - case ARG_WELL_KNOWN_KEY: { - if (isempty(optarg)) { + OPTION_LONG("well-known-key", "NAME:KEY", "Configure the location of well-known keys"): { + if (isempty(arg)) { for (ImdsWellKnown wk = 0; wk < _IMDS_WELL_KNOWN_MAX; wk++) arg_well_known_key[wk] = mfree(arg_well_known_key[wk]); break; } - const char *e = strchr(optarg, ':'); + const char *e = strchr(arg, ':'); if (!e) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--well-known-key= expects colon separated name and key pairs."); - _cleanup_free_ char *name = strndup(optarg, e - optarg); + _cleanup_free_ char *name = strndup(arg, e - arg); if (!name) return log_oom(); @@ -2557,21 +2510,9 @@ static int parse_argv(int argc, char *argv[]) { r = free_and_strdup_warn(arg_well_known_key + wk, e); if (r < 0) return r; - break; } - - case ARG_SETUP_NETWORK: - arg_setup_network = true; - break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } - } if (arg_vendor || arg_token_url || arg_refresh_header_name || arg_data_url || arg_data_url_suffix || arg_token_header_name || arg_extra_header) arg_endpoint_source = ENDPOINT_USER; @@ -2583,23 +2524,25 @@ static int parse_argv(int argc, char *argv[]) { arg_varlink = r; if (!arg_varlink) { + char **args = option_parser_get_args(&state); + size_t n_args = option_parser_get_n_args(&state); if (arg_setup_network) { - if (optind != argc) + if (n_args != 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No argument expected."); } else { if (arg_well_known < 0) { /* if no --well-known= parameter was specified we require an argument */ - if (argc != optind+1) + if (n_args != 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "A single argument expected."); - } else if (argc > optind+1) /* if not, then the additional parameter is optional */ + } else if (n_args > 1) /* if not, then the additional parameter is optional */ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "At most a single argument expected."); - if (argc > optind) { - if (!imds_key_is_valid(argv[optind])) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified IMDS key is not valid, refusing: %s", argv[optind]); + if (n_args > 0) { + if (!imds_key_is_valid(args[0])) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified IMDS key is not valid, refusing: %s", args[0]); - r = free_and_strdup_warn(&arg_key, argv[optind]); + r = free_and_strdup_warn(&arg_key, args[0]); if (r < 0) return r; } From e1eae0cf7816db8c7d3eca8d0d64b2705b37b153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sat, 4 Apr 2026 20:16:28 +0200 Subject: [PATCH 04/13] varlinkctl: convert to the new option parser The -E short option previously used fallthrough into the --more case; since macro-generated case labels don't support fallthrough (with some older compilers), the --more logic is now duplicated inline in the -E handler. Co-developed-by: Claude Opus 4.6 --- src/varlinkctl/varlinkctl.c | 191 +++++++++++++----------------------- 1 file changed, 69 insertions(+), 122 deletions(-) diff --git a/src/varlinkctl/varlinkctl.c b/src/varlinkctl/varlinkctl.c index 4124d0d570..2dc191bff3 100644 --- a/src/varlinkctl/varlinkctl.c +++ b/src/varlinkctl/varlinkctl.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include #include @@ -20,6 +19,7 @@ #include "log.h" #include "main-func.h" #include "memfd-util.h" +#include "options.h" #include "pager.h" #include "parse-argument.h" #include "parse-util.h" @@ -72,17 +72,22 @@ STATIC_DESTRUCTOR_REGISTER(arg_push_fds, push_fds_done); static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *options = NULL; int r; r = terminal_urlify_man("varlinkctl", "1", &link); if (r < 0) return log_oom(); + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + pager_open(arg_pager_flags); printf("%1$s [OPTIONS...] COMMAND ...\n\n" - "%5$sIntrospect Varlink Services.%6$s\n" - "\n%3$sCommands:%4$s\n" + "%3$sIntrospect Varlink Services.%4$s\n" + "\n%2$sCommands:%4$s\n" " info ADDRESS Show service information\n" " list-interfaces ADDRESS\n" " List interfaces implemented by service\n" @@ -98,33 +103,17 @@ static int help(void) { " list-registry Show list of services in the service registry\n" " validate-idl [FILE] Validate interface description\n" " help Show this help\n" - "\n%3$sOptions:%4$s\n" - " -h --help Show this help\n" - " --version Show package version\n" - " --no-ask-password Do not prompt for password\n" - " --no-pager Do not pipe output into a pager\n" - " --system Enumerate system registry\n" - " --user Enumerate user registry\n" - " --more Request multiple responses\n" - " --collect Collect multiple responses in a JSON array\n" - " --oneway Do not request response\n" - " --json=MODE Output as JSON\n" - " -j Same as --json=pretty on tty, --json=short otherwise\n" - " -q --quiet Do not output method reply\n" - " --graceful=ERROR Treat specified Varlink error as success\n" - " --timeout=SECS Maximum time to wait for method call completion\n" - " -E Short for --more --timeout=infinity\n" - " --upgrade Request protocol upgrade (connection becomes raw\n" - " bidirectional pipe on stdin/stdout after reply)\n" - " --push-fd=FD Pass the specified fd along with method call\n" - "\nSee the %2$s for details.\n", + "\n%2$sOptions:%4$s\n", program_invocation_short_name, - link, ansi_underline(), - ansi_normal(), ansi_highlight(), ansi_normal()); + r = table_print_or_warn(options); + if (r < 0) + return r; + + printf("\nSee the %s for details.\n", link); return 0; } @@ -133,145 +122,121 @@ static int verb_help(int argc, char *argv[], uintptr_t _data, void *userdata) { } static int parse_argv(int argc, char *argv[]) { - - enum { - ARG_VERSION = 0x100, - ARG_NO_PAGER, - ARG_MORE, - ARG_ONEWAY, - ARG_JSON, - ARG_COLLECT, - ARG_GRACEFUL, - ARG_TIMEOUT, - ARG_EXEC, - ARG_UPGRADE, - ARG_PUSH_FD, - ARG_NO_ASK_PASSWORD, - ARG_USER, - ARG_SYSTEM, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "no-pager", no_argument, NULL, ARG_NO_PAGER }, - { "more", no_argument, NULL, ARG_MORE }, - { "oneway", no_argument, NULL, ARG_ONEWAY }, - { "json", required_argument, NULL, ARG_JSON }, - { "collect", no_argument, NULL, ARG_COLLECT }, - { "quiet", no_argument, NULL, 'q' }, - { "graceful", required_argument, NULL, ARG_GRACEFUL }, - { "timeout", required_argument, NULL, ARG_TIMEOUT }, - { "exec", no_argument, NULL, ARG_EXEC }, - { "upgrade", no_argument, NULL, ARG_UPGRADE }, - { "push-fd", required_argument, NULL, ARG_PUSH_FD }, - { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD }, - { "user", no_argument, NULL, ARG_USER }, - { "system", no_argument, NULL, ARG_SYSTEM }, - {}, - }; - - int c, r; + int r; assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "hjqE", 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_NO_PAGER: + OPTION_COMMON_NO_ASK_PASSWORD: + arg_ask_password = false; + break; + + OPTION_COMMON_NO_PAGER: arg_pager_flags |= PAGER_DISABLE; break; - case 'E': - arg_timeout = USEC_INFINITY; - _fallthrough_; + OPTION_LONG("system", NULL, "Enumerate system registry"): + arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; + break; - case ARG_MORE: + OPTION_LONG("user", NULL, "Enumerate user registry"): + arg_runtime_scope = RUNTIME_SCOPE_USER; + break; + + OPTION_LONG("more", NULL, "Request multiple responses"): arg_method_flags = (arg_method_flags & ~SD_VARLINK_METHOD_ONEWAY) | SD_VARLINK_METHOD_MORE; break; - case ARG_ONEWAY: - arg_method_flags = (arg_method_flags & ~SD_VARLINK_METHOD_MORE) | SD_VARLINK_METHOD_ONEWAY; - break; - - case ARG_COLLECT: + OPTION_LONG("collect", NULL, "Collect multiple responses in a JSON array"): arg_collect = true; break; - case ARG_JSON: - r = parse_json_argument(optarg, &arg_json_format_flags); - if (r <= 0) - return r; - + OPTION_LONG("oneway", NULL, "Do not request response"): + arg_method_flags = (arg_method_flags & ~SD_VARLINK_METHOD_MORE) | SD_VARLINK_METHOD_ONEWAY; break; - case 'j': + 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; - case 'q': + OPTION('q', "quiet", NULL, "Do not output method reply"): arg_quiet = true; break; - case ARG_GRACEFUL: - r = varlink_idl_qualified_symbol_name_is_valid(optarg); + OPTION_LONG("graceful", "ERROR", "Treat specified Varlink error as success"): + r = varlink_idl_qualified_symbol_name_is_valid(arg); if (r < 0) - return log_error_errno(r, "Failed to validate Varlink error name '%s': %m", optarg); + return log_error_errno(r, "Failed to validate Varlink error name '%s': %m", arg); if (r == 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid Varlink error name: %s", optarg); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid Varlink error name: %s", arg); - if (strv_extend(&arg_graceful, optarg) < 0) + if (strv_extend(&arg_graceful, arg) < 0) return log_oom(); - break; - case ARG_TIMEOUT: - if (isempty(optarg)) { + OPTION_LONG("timeout", "SECS", "Maximum time to wait for method call completion"): + if (isempty(arg)) { arg_timeout = USEC_INFINITY; break; } - r = parse_sec(optarg, &arg_timeout); + r = parse_sec(arg, &arg_timeout); if (r < 0) - return log_error_errno(r, "Failed to parse --timeout= parameter '%s': %m", optarg); + return log_error_errno(r, "Failed to parse --timeout= parameter '%s': %m", arg); if (arg_timeout == 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Timeout cannot be zero."); - break; - case ARG_EXEC: - arg_exec = true; + OPTION_SHORT('E', NULL, "Short for --more --timeout=infinity"): + arg_timeout = USEC_INFINITY; + arg_method_flags = (arg_method_flags & ~SD_VARLINK_METHOD_ONEWAY) | SD_VARLINK_METHOD_MORE; break; - case ARG_UPGRADE: + OPTION_LONG("upgrade", NULL, + "Request protocol upgrade (connection becomes raw" + " bidirectional pipe on stdin/stdout after reply)"): arg_upgrade = true; break; - case ARG_PUSH_FD: { + OPTION_LONG("exec", NULL, "Invoke method and pass response and fds to command"): + arg_exec = true; + break; + + OPTION_LONG("push-fd", "FD", "Pass the specified fd along with method call"): { if (!GREEDY_REALLOC(arg_push_fds.fds, arg_push_fds.n_fds + 1)) return log_oom(); _cleanup_close_ int add_fd = -EBADF; - if (STARTSWITH_SET(optarg, "/", "./")) { + if (STARTSWITH_SET(arg, "/", "./")) { /* We usually expect a numeric fd spec, but as an extension let's treat this * as a path to open in read-only mode in case this is clearly an absolute or * relative path */ - add_fd = open(optarg, O_CLOEXEC|O_RDONLY|O_NOCTTY); + add_fd = open(arg, O_CLOEXEC|O_RDONLY|O_NOCTTY); if (add_fd < 0) - return log_error_errno(errno, "Failed to open '%s': %m", optarg); + return log_error_errno(errno, "Failed to open '%s': %m", arg); } else { - int parsed_fd = parse_fd(optarg); + int parsed_fd = parse_fd(arg); if (parsed_fd < 0) - return log_error_errno(parsed_fd, "Failed to parse --push-fd= parameter: %s", optarg); + return log_error_errno(parsed_fd, "Failed to parse --push-fd= parameter: %s", arg); /* Make a copy, so that the same fd could be used multiple times in a reasonable * way. This also validates the fd early */ @@ -283,24 +248,6 @@ static int parse_argv(int argc, char *argv[]) { arg_push_fds.fds[arg_push_fds.n_fds++] = TAKE_FD(add_fd); break; } - - case ARG_NO_ASK_PASSWORD: - arg_ask_password = false; - break; - - case ARG_USER: - arg_runtime_scope = RUNTIME_SCOPE_USER; - break; - - case ARG_SYSTEM: - arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; - break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } /* If more than one reply is expected, imply JSON-SEQ output, and set SD_JSON_FORMAT_FLUSH */ From fd5c19e8d0a38199c64b1fcb947332d61337f4a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sat, 4 Apr 2026 20:24:16 +0200 Subject: [PATCH 05/13] varlinkctl: convert to the new verb macros The description of --exec is moved to a separate footer. It requires special formatting and doesn't fit in the autogenerated table of verbs. Co-developed-by: Claude Opus 4.6 --- src/varlinkctl/varlinkctl.c | 77 ++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 43 deletions(-) diff --git a/src/varlinkctl/varlinkctl.c b/src/varlinkctl/varlinkctl.c index 2dc191bff3..00bd71f34d 100644 --- a/src/varlinkctl/varlinkctl.c +++ b/src/varlinkctl/varlinkctl.c @@ -72,7 +72,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_push_fds, push_fds_done); static int help(void) { _cleanup_free_ char *link = NULL; - _cleanup_(table_unrefp) Table *options = NULL; + _cleanup_(table_unrefp) Table *options = NULL, *verbs = NULL; int r; r = terminal_urlify_man("varlinkctl", "1", &link); @@ -83,45 +83,42 @@ static int help(void) { if (r < 0) return r; + r = verbs_get_help_table(&verbs); + if (r < 0) + return r; + + (void) table_sync_column_widths(0, options, verbs); + pager_open(arg_pager_flags); - printf("%1$s [OPTIONS...] COMMAND ...\n\n" - "%3$sIntrospect Varlink Services.%4$s\n" - "\n%2$sCommands:%4$s\n" - " info ADDRESS Show service information\n" - " list-interfaces ADDRESS\n" - " List interfaces implemented by service\n" - " list-methods ADDRESS [INTERFACE…]\n" - " List methods implemented by services or specific\n" - " interfaces\n" - " introspect ADDRESS [INTERFACE…]\n" - " Show interface definition\n" - " call ADDRESS METHOD [PARAMS]\n" - " Invoke method\n" - " --exec call ADDRESS METHOD PARAMS -- CMDLINE…\n" - " Invoke method and pass response and fds to command\n" - " list-registry Show list of services in the service registry\n" - " validate-idl [FILE] Validate interface description\n" - " help Show this help\n" - "\n%2$sOptions:%4$s\n", + printf("%s [OPTIONS...] COMMAND ...\n\n" + "%sIntrospect Varlink Services.%s\n" + "\nCommands:\n", program_invocation_short_name, - ansi_underline(), ansi_highlight(), ansi_normal()); + r = table_print_or_warn(verbs); + if (r < 0) + return r; + + printf("\nOptions:\n"); + r = table_print_or_warn(options); if (r < 0) return r; + printf("\nWith --exec, specify the command to invoke:\n" + " %s --exec call ADDRESS METHOD PARAMS -- CMDLINE…\n", + program_invocation_short_name); + printf("\nSee the %s for details.\n", link); return 0; } -static int verb_help(int argc, char *argv[], uintptr_t _data, void *userdata) { - return help(); -} +VERB_COMMON_HELP(help); -static int parse_argv(int argc, char *argv[]) { +static int parse_argv(int argc, char *argv[], char ***ret_args) { int r; assert(argc >= 0); @@ -256,6 +253,7 @@ static int parse_argv(int argc, char *argv[]) { strv_sort_uniq(arg_graceful); + *ret_args = option_parser_get_args(&state); return 1; } @@ -324,6 +322,8 @@ static void get_info_data_done(GetInfoData *d) { d->interfaces = strv_free(d->interfaces); } +VERB(verb_info, "info", "ADDRESS", 2, 2, 0, "Show service information"); +VERB(verb_info, "list-interfaces", "ADDRESS", 2, 2, 0, "List interfaces implemented by service"); static int verb_info(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; const char *url; @@ -417,6 +417,9 @@ typedef struct GetInterfaceDescriptionData { const char *description; } GetInterfaceDescriptionData; +VERB(verb_introspect, "introspect", "ADDRESS [INTERFACE…]", 2, VERB_ANY, 0, "Show interface definition"); +VERB(verb_introspect, "list-methods", "ADDRESS [INTERFACE…]", 2, VERB_ANY, 0, + "List methods implemented by services or specific interfaces"); static int verb_introspect(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; _cleanup_strv_free_ char **auto_interfaces = NULL; @@ -742,6 +745,7 @@ static int varlink_call_and_upgrade(const char *url, const char *method, sd_json return 0; } +VERB(verb_call, "call", "ADDRESS METHOD [PARAMS]", 3, VERB_ANY, 0, "Invoke method"); static int verb_call(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *jp = NULL; _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; @@ -1015,6 +1019,7 @@ static int verb_call(int argc, char *argv[], uintptr_t _data, void *userdata) { return 0; } +VERB(verb_validate_idl, "validate-idl", "[FILE]", 1, 2, 0, "Validate interface description"); static int verb_validate_idl(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(sd_varlink_interface_freep) sd_varlink_interface *vi = NULL; _cleanup_free_ char *text = NULL; @@ -1064,6 +1069,7 @@ static int verb_validate_idl(int argc, char *argv[], uintptr_t _data, void *user return 0; } +VERB_NOARG(verb_list_registry, "list-registry", "Show list of services in the service registry"); static int verb_list_registry(int argc, char *argv[], uintptr_t _data, void *userdata) { int r; @@ -1163,32 +1169,17 @@ static int verb_list_registry(int argc, char *argv[], uintptr_t _data, void *use return 0; } -static int varlinkctl_main(int argc, char *argv[]) { - static const Verb verbs[] = { - { "info", 2, 2, 0, verb_info }, - { "list-interfaces", 2, 2, 0, verb_info }, - { "introspect", 2, VERB_ANY, 0, verb_introspect }, - { "list-methods", 2, VERB_ANY, 0, verb_introspect }, - { "call", 3, VERB_ANY, 0, verb_call }, - { "list-registry", VERB_ANY, 1, 0, verb_list_registry }, - { "validate-idl", 1, 2, 0, verb_validate_idl }, - { "help", VERB_ANY, VERB_ANY, 0, verb_help }, - {} - }; - - 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; /* unnecessary initialization to appease gcc <= 13 */ + r = parse_argv(argc, argv, &args); if (r <= 0) return r; - return varlinkctl_main(argc, argv); + return dispatch_verb_with_args(args, NULL); } DEFINE_MAIN_FUNCTION(run); From 386dc2175d0b32f7de9730e756e1bedcde5ad610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sat, 4 Apr 2026 20:54:02 +0200 Subject: [PATCH 06/13] tmpfiles: convert to the new option parser The --image fallthrough into -E is replaced by duplicating the exclude_default_prefixes() call inline. Cosmetic differences in --help only. Co-developed-by: Claude Opus 4.6 --- src/tmpfiles/tmpfiles.c | 243 +++++++++++++++++----------------------- 1 file changed, 101 insertions(+), 142 deletions(-) diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c index 17f263790e..e869c3da69 100644 --- a/src/tmpfiles/tmpfiles.c +++ b/src/tmpfiles/tmpfiles.c @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -31,6 +30,7 @@ #include "extract-word.h" #include "fd-util.h" #include "fileio.h" +#include "format-table.h" #include "format-util.h" #include "fs-util.h" #include "glob-util.h" @@ -45,6 +45,7 @@ #include "mount-util.h" #include "mountpoint-util.h" #include "offline-passwd.h" +#include "options.h" #include "pager.h" #include "parse-argument.h" #include "parse-util.h" @@ -209,7 +210,7 @@ static char **arg_include_prefixes = NULL; static char **arg_exclude_prefixes = NULL; static char *arg_root = NULL; static char *arg_image = NULL; -static char *arg_replace = NULL; +static const char *arg_replace = NULL; static ImagePolicy *arg_image_policy = NULL; #define MAX_DEPTH 256 @@ -4129,218 +4130,174 @@ static int exclude_default_prefixes(void) { static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *cmds = NULL, *opts = NULL; int r; r = terminal_urlify_man("systemd-tmpfiles", "8", &link); if (r < 0) return log_oom(); - printf("%1$s COMMAND [OPTIONS...] [CONFIGURATION FILE...]\n" - "\n%2$sCreate, delete, and clean up files and directories.%4$s\n" - "\n%3$sCommands:%4$s\n" - " --create Create and adjust files and directories\n" - " --clean Clean up files and directories\n" - " --remove Remove files and directories marked for removal\n" - " --purge Delete files and directories marked for creation in\n" - " specified configuration files (careful!)\n" - " --cat-config Show configuration files\n" - " --tldr Show non-comment parts of configuration files\n" - " -h --help Show this help\n" - " --version Show package version\n" - "\n%3$sOptions:%4$s\n" - " --user Execute user configuration\n" - " --boot Execute actions only safe at boot\n" - " --graceful Quietly ignore unknown users or groups\n" - " --prefix=PATH Only apply rules with the specified prefix\n" - " --exclude-prefix=PATH Ignore rules with the specified prefix\n" - " -E Ignore rules prefixed with /dev, /proc, /run, /sys\n" - " --root=PATH Operate on an alternate filesystem root\n" - " --image=PATH Operate on disk image as filesystem root\n" - " --image-policy=POLICY Specify disk image dissection policy\n" - " --replace=PATH Treat arguments as replacement for PATH\n" - " --dry-run Just print what would be done\n" - " --inline Treat arguments as configuration lines\n" - " --no-pager Do not pipe output into a pager\n" - "\nSee the %5$s for details.\n", + r = option_parser_get_help_table(&cmds); + if (r < 0) + return r; + + r = option_parser_get_help_table_group("Options", &opts); + if (r < 0) + return r; + + (void) table_sync_column_widths(0, cmds, opts); + + printf("%s COMMAND [OPTIONS...] [CONFIGURATION FILE...]\n" + "\n%sCreate, delete, and clean up files and directories.%s\n" + "\nCommands:\n", program_invocation_short_name, ansi_highlight(), - ansi_underline(), - ansi_normal(), - link); + ansi_normal()); + r = table_print_or_warn(cmds); + if (r < 0) + return r; + + printf("\nOptions:\n"); + + r = table_print_or_warn(opts); + 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_CAT_CONFIG, - ARG_TLDR, - ARG_USER, - ARG_CREATE, - ARG_CLEAN, - ARG_REMOVE, - ARG_PURGE, - ARG_BOOT, - ARG_GRACEFUL, - ARG_PREFIX, - ARG_EXCLUDE_PREFIX, - ARG_ROOT, - ARG_IMAGE, - ARG_IMAGE_POLICY, - ARG_REPLACE, - ARG_DRY_RUN, - ARG_INLINE, - ARG_NO_PAGER, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "user", no_argument, NULL, ARG_USER }, - { "version", no_argument, NULL, ARG_VERSION }, - { "cat-config", no_argument, NULL, ARG_CAT_CONFIG }, - { "tldr", no_argument, NULL, ARG_TLDR }, - { "create", no_argument, NULL, ARG_CREATE }, - { "clean", no_argument, NULL, ARG_CLEAN }, - { "remove", no_argument, NULL, ARG_REMOVE }, - { "purge", no_argument, NULL, ARG_PURGE }, - { "boot", no_argument, NULL, ARG_BOOT }, - { "graceful", no_argument, NULL, ARG_GRACEFUL }, - { "prefix", required_argument, NULL, ARG_PREFIX }, - { "exclude-prefix", required_argument, NULL, ARG_EXCLUDE_PREFIX }, - { "root", required_argument, NULL, ARG_ROOT }, - { "image", required_argument, NULL, ARG_IMAGE }, - { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, - { "replace", required_argument, NULL, ARG_REPLACE }, - { "dry-run", no_argument, NULL, ARG_DRY_RUN }, - { "inline", no_argument, NULL, ARG_INLINE }, - { "no-pager", no_argument, NULL, ARG_NO_PAGER }, - {} - }; - - int c, r; +static int parse_argv(int argc, char *argv[], char ***ret_args) { + int r; assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "hE", options, NULL)) >= 0) + OptionParser state = { argc, argv }; + const char *arg; + FOREACH_OPTION(&state, c, &arg, /* on_error= */ return c) switch (c) { - case 'h': - return help(); - - case ARG_VERSION: - return version(); - - case ARG_CAT_CONFIG: - arg_cat_flags = CAT_CONFIG_ON; - break; - - case ARG_TLDR: - arg_cat_flags = CAT_TLDR; - break; - - case ARG_USER: - arg_runtime_scope = RUNTIME_SCOPE_USER; - break; - - case ARG_CREATE: + OPTION_LONG("create", NULL, "Create and adjust files and directories"): arg_operation |= OPERATION_CREATE; break; - case ARG_CLEAN: + OPTION_LONG("clean", NULL, "Clean up files and directories"): arg_operation |= OPERATION_CLEAN; break; - case ARG_REMOVE: + OPTION_LONG("remove", NULL, "Remove files and directories marked for removal"): arg_operation |= OPERATION_REMOVE; break; - case ARG_BOOT: - arg_boot = true; - break; - - case ARG_PURGE: + OPTION_LONG("purge", NULL, + "Delete files and directories marked for creation in" + " specified configuration files (careful!)"): arg_operation |= OPERATION_PURGE; break; - case ARG_GRACEFUL: + OPTION_COMMON_CAT_CONFIG: + arg_cat_flags = CAT_CONFIG_ON; + break; + + OPTION_COMMON_TLDR: + arg_cat_flags = CAT_TLDR; + break; + + OPTION_COMMON_HELP: + return help(); + + OPTION_COMMON_VERSION: + return version(); + + OPTION_GROUP("Options"): + break; + + OPTION_LONG("user", NULL, "Execute user configuration"): + arg_runtime_scope = RUNTIME_SCOPE_USER; + break; + + OPTION_LONG("boot", NULL, "Execute actions only safe at boot"): + arg_boot = true; + break; + + OPTION_LONG("graceful", NULL, "Quietly ignore unknown users or groups"): arg_graceful = true; break; - case ARG_PREFIX: - if (strv_extend(&arg_include_prefixes, optarg) < 0) + OPTION_LONG("prefix", "PATH", "Only apply rules with the specified prefix"): + if (strv_extend(&arg_include_prefixes, arg) < 0) return log_oom(); break; - case ARG_EXCLUDE_PREFIX: - if (strv_extend(&arg_exclude_prefixes, optarg) < 0) + OPTION_LONG("exclude-prefix", "PATH", "Ignore rules with the specified prefix"): + if (strv_extend(&arg_exclude_prefixes, arg) < 0) return log_oom(); break; - case ARG_ROOT: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_root); + OPTION_SHORT('E', NULL, "Ignore rules prefixed with /dev, /proc, /run, /sys"): + r = exclude_default_prefixes(); if (r < 0) return r; break; - case ARG_IMAGE: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image); + OPTION_LONG("root", "PATH", "Operate on an alternate filesystem root"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_root); + if (r < 0) + return r; + break; + + OPTION_LONG("image", "PATH", "Operate on disk image as filesystem root"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_image); if (r < 0) return r; /* Imply -E here since it makes little sense to create files persistently in the /run mountpoint of a disk image */ - _fallthrough_; - - case 'E': r = exclude_default_prefixes(); if (r < 0) return r; - break; - case ARG_IMAGE_POLICY: - r = parse_image_policy_argument(optarg, &arg_image_policy); + OPTION_LONG("image-policy", "POLICY", "Specify disk image dissection policy"): + r = parse_image_policy_argument(arg, &arg_image_policy); if (r < 0) return r; break; - case ARG_REPLACE: - if (!path_is_absolute(optarg)) + OPTION_LONG("replace", "PATH", "Treat arguments as replacement for PATH"): + if (!path_is_absolute(arg)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The argument to --replace= must be an absolute path."); - if (!endswith(optarg, ".conf")) + if (!endswith(arg, ".conf")) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The argument to --replace= must have the extension '.conf'."); - arg_replace = optarg; + arg_replace = arg; break; - case ARG_DRY_RUN: + OPTION_LONG("dry-run", NULL, "Just print what would be done"): arg_dry_run = true; break; - case ARG_INLINE: + OPTION_LONG("inline", NULL, "Treat arguments as configuration lines"): arg_inline = true; break; - case ARG_NO_PAGER: + OPTION_COMMON_NO_PAGER: arg_pager_flags |= PAGER_DISABLE; break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } + char **args = option_parser_get_args(&state); + size_t n_args = option_parser_get_n_args(&state); + if (arg_operation == 0 && arg_cat_flags == CAT_CONFIG_OFF) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "You need to specify at least one of --clean, --create, --remove, or --purge."); - if (FLAGS_SET(arg_operation, OPERATION_PURGE) && optind >= argc) + if (FLAGS_SET(arg_operation, OPERATION_PURGE) && n_args == 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing --purge without specification of a configuration file."); @@ -4352,7 +4309,7 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --inline is not supported with --cat-config/--tldr."); - if (arg_replace && optind >= argc) + if (arg_replace && n_args == 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "When --replace= is given, some configuration items must be specified."); @@ -4364,6 +4321,7 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported."); + *ret_args = args; return 1; } @@ -4564,7 +4522,8 @@ static int run(int argc, char *argv[]) { } phase; int r; - r = parse_argv(argc, argv); + char **args = NULL; + r = parse_argv(argc, argv, &args); if (r <= 0) return r; @@ -4619,7 +4578,7 @@ static int run(int argc, char *argv[]) { } if (arg_cat_flags != CAT_CONFIG_OFF) - return cat_config(config_dirs, argv + optind); + return cat_config(config_dirs, args); if (should_bypass("SYSTEMD_TMPFILES")) return 0; @@ -4663,10 +4622,10 @@ static int run(int argc, char *argv[]) { * insert the positional arguments at the specified place. Otherwise, if command line arguments are * specified, execute just them, and finally, without --replace= or any positional arguments, just * read configuration and execute it. */ - if (arg_replace || optind >= argc) - r = read_config_files(&c, config_dirs, argv + optind, &invalid_config); + if (arg_replace || strv_isempty(args)) + r = read_config_files(&c, config_dirs, args, &invalid_config); else - r = parse_arguments(&c, config_dirs, argv + optind, &invalid_config); + r = parse_arguments(&c, config_dirs, args, &invalid_config); if (r < 0) return r; From e106ebbcab4626cf2c9b260e7c8a4e647c4914b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sat, 4 Apr 2026 21:08:51 +0200 Subject: [PATCH 07/13] sysusers: convert to the new option parser Cosmetic differences in --help only. Co-developed-by: Claude Opus 4.6 --- src/sysusers/sysusers.c | 151 ++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 84 deletions(-) diff --git a/src/sysusers/sysusers.c b/src/sysusers/sysusers.c index d1570eda56..015043a0a4 100644 --- a/src/sysusers/sysusers.c +++ b/src/sysusers/sysusers.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include @@ -16,6 +15,7 @@ #include "extract-word.h" #include "fd-util.h" #include "fileio.h" +#include "format-table.h" #include "format-util.h" #include "fs-util.h" #include "hashmap.h" @@ -28,6 +28,7 @@ #include "loop-util.h" #include "main-func.h" #include "mount-util.h" +#include "options.h" #include "pager.h" #include "parse-argument.h" #include "path-util.h" @@ -2048,138 +2049,118 @@ static int cat_config(void) { static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *cmds = NULL, *opts = NULL; int r; r = terminal_urlify_man("systemd-sysusers.service", "8", &link); if (r < 0) return log_oom(); - printf("%1$s [OPTIONS...] [CONFIGURATION FILE...]\n" - "\n%2$sCreates system user and group accounts.%4$s\n" - "\n%3$sCommands:%4$s\n" - " --cat-config Show configuration files\n" - " --tldr Show non-comment parts of configuration\n" - " -h --help Show this help\n" - " --version Show package version\n" - "\n%3$sOptions:%4$s\n" - " --root=PATH Operate on an alternate filesystem root\n" - " --image=PATH Operate on disk image as filesystem root\n" - " --image-policy=POLICY Specify disk image dissection policy\n" - " --replace=PATH Treat arguments as replacement for PATH\n" - " --dry-run Just print what would be done\n" - " --inline Treat arguments as configuration lines\n" - " --no-pager Do not pipe output into a pager\n" - "\nSee the %5$s for details.\n", + r = option_parser_get_help_table(&cmds); + if (r < 0) + return r; + + r = option_parser_get_help_table_group("Options", &opts); + if (r < 0) + return r; + + (void) table_sync_column_widths(0, cmds, opts); + + printf("%s [OPTIONS...] [CONFIGURATION FILE...]\n" + "\n%sCreates system user and group accounts.%s\n" + "\nCommands:\n", program_invocation_short_name, ansi_highlight(), - ansi_underline(), - ansi_normal(), - link); + ansi_normal()); + r = table_print_or_warn(cmds); + if (r < 0) + return r; + + printf("\nOptions:\n"); + + r = table_print_or_warn(opts); + 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_CAT_CONFIG, - ARG_TLDR, - ARG_ROOT, - ARG_IMAGE, - ARG_IMAGE_POLICY, - ARG_REPLACE, - ARG_DRY_RUN, - ARG_INLINE, - ARG_NO_PAGER, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "cat-config", no_argument, NULL, ARG_CAT_CONFIG }, - { "tldr", no_argument, NULL, ARG_TLDR }, - { "root", required_argument, NULL, ARG_ROOT }, - { "image", required_argument, NULL, ARG_IMAGE }, - { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, - { "replace", required_argument, NULL, ARG_REPLACE }, - { "dry-run", no_argument, NULL, ARG_DRY_RUN }, - { "inline", no_argument, NULL, ARG_INLINE }, - { "no-pager", no_argument, NULL, ARG_NO_PAGER }, - {} - }; - - int c, r; +static int parse_argv(int argc, char *argv[], char ***ret_args) { + int r; assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + OptionParser state = { argc, argv }; + const char *arg; + FOREACH_OPTION(&state, c, &arg, /* on_error= */ return c) switch (c) { - case 'h': - return help(); - - case ARG_VERSION: - return version(); - - case ARG_CAT_CONFIG: + OPTION_COMMON_CAT_CONFIG: arg_cat_flags = CAT_CONFIG_ON; break; - case ARG_TLDR: + OPTION_COMMON_TLDR: arg_cat_flags = CAT_TLDR; break; - case ARG_ROOT: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_root); + OPTION_COMMON_HELP: + return help(); + + OPTION_COMMON_VERSION: + return version(); + + OPTION_GROUP("Options"): + break; + + OPTION_LONG("root", "PATH", "Operate on an alternate filesystem root"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_root); if (r < 0) return r; break; - case ARG_IMAGE: - r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image); + OPTION_LONG("image", "PATH", "Operate on disk image as filesystem root"): + r = parse_path_argument(arg, /* suppress_root= */ false, &arg_image); if (r < 0) return r; break; - case ARG_IMAGE_POLICY: - r = parse_image_policy_argument(optarg, &arg_image_policy); + OPTION_LONG("image-policy", "POLICY", "Specify disk image dissection policy"): + r = parse_image_policy_argument(arg, &arg_image_policy); if (r < 0) return r; break; - case ARG_REPLACE: - if (!path_is_absolute(optarg)) + OPTION_LONG("replace", "PATH", "Treat arguments as replacement for PATH"): + if (!path_is_absolute(arg)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The argument to --replace= must be an absolute path."); - if (!endswith(optarg, ".conf")) + if (!endswith(arg, ".conf")) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The argument to --replace= must have the extension '.conf'."); - arg_replace = optarg; + arg_replace = arg; break; - case ARG_DRY_RUN: + OPTION_LONG("dry-run", NULL, "Just print what would be done"): arg_dry_run = true; break; - case ARG_INLINE: + OPTION_LONG("inline", NULL, "Treat arguments as configuration lines"): arg_inline = true; break; - case ARG_NO_PAGER: + OPTION_COMMON_NO_PAGER: arg_pager_flags |= PAGER_DISABLE; break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } + char **args = option_parser_get_args(&state); + size_t n_args = option_parser_get_n_args(&state); + if (arg_replace && arg_cat_flags != CAT_CONFIG_OFF) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --replace= is not supported with --cat-config/--tldr."); @@ -2188,7 +2169,7 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --inline is not supported with --cat-config/--tldr."); - if (arg_replace && optind >= argc) + if (arg_replace && n_args == 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "When --replace= is given, some configuration items must be specified."); @@ -2196,6 +2177,7 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Use either --root= or --image=, the combination of both is not supported."); + *ret_args = args; return 1; } @@ -2281,7 +2263,8 @@ static int run(int argc, char *argv[]) { Item *i; int r; - r = parse_argv(argc, argv); + char **args = NULL; + r = parse_argv(argc, argv, &args); if (r <= 0) return r; @@ -2331,10 +2314,10 @@ static int run(int argc, char *argv[]) { * insert the positional arguments at the specified place. Otherwise, if command line arguments are * specified, execute just them, and finally, without --replace= or any positional arguments, just * read configuration and execute it. */ - if (arg_replace || optind >= argc) - r = read_config_files(&c, argv + optind); + if (arg_replace || strv_isempty(args)) + r = read_config_files(&c, args); else - r = parse_arguments(&c, argv + optind); + r = parse_arguments(&c, args); if (r < 0) return r; From 35d7a0f04796b3948dfb63cccc5fdeb632be4009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sat, 4 Apr 2026 21:15:54 +0200 Subject: [PATCH 08/13] test-offline-passwd: convert to the new option parser Co-developed-by: Claude Opus 4.6 --- src/tmpfiles/test-offline-passwd.c | 41 +++++++++++++----------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/src/tmpfiles/test-offline-passwd.c b/src/tmpfiles/test-offline-passwd.c index 7be29ff798..9695ba9b63 100644 --- a/src/tmpfiles/test-offline-passwd.c +++ b/src/tmpfiles/test-offline-passwd.c @@ -1,13 +1,13 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include - #include "format-util.h" #include "hashmap.h" #include "offline-passwd.h" +#include "options.h" +#include "strv.h" #include "tests.h" -static char *arg_root = NULL; +static const char *arg_root = NULL; static void test_resolve_one(const char *name) { bool relaxed = name || arg_root; @@ -39,30 +39,22 @@ static void test_resolve_one(const char *name) { assert_se(relaxed || r == 0); } -static int parse_argv(int argc, char *argv[]) { - static const struct option options[] = { - { "root", required_argument, NULL, 'r' }, - {} - }; - - int c; - +static int parse_argv(int argc, char *argv[], char ***ret_args) { assert_se(argc >= 0); assert_se(argv); - while ((c = getopt_long(argc, argv, "r:", options, NULL)) >= 0) + OptionParser state = { argc, argv }; + const char *arg; + + FOREACH_OPTION(&state, c, &arg, /* on_error= */ return c) switch (c) { - case 'r': - arg_root = optarg; + + OPTION('r', "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 0; } @@ -71,15 +63,16 @@ int main(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; - if (optind >= argc) + if (strv_isempty(args)) test_resolve_one(NULL); else - while (optind < argc) - test_resolve_one(argv[optind++]); + STRV_FOREACH(a, args) + test_resolve_one(*a); return 0; } From 3ed2abf2877a7519063cd0cd12f2501d927f08f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sat, 4 Apr 2026 21:26:01 +0200 Subject: [PATCH 09/13] tpm2-clear: convert to the new option parser --help is identical except for whitespace changes. Co-developed-by: Claude Opus 4.6 --- src/tpm2-setup/tpm2-clear.c | 61 +++++++++++++++---------------------- 1 file changed, 24 insertions(+), 37 deletions(-) diff --git a/src/tpm2-setup/tpm2-clear.c b/src/tpm2-setup/tpm2-clear.c index 0800a90747..e6a063f2a8 100644 --- a/src/tpm2-setup/tpm2-clear.c +++ b/src/tpm2-setup/tpm2-clear.c @@ -1,15 +1,15 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include - #include "sd-messages.h" #include "alloc-util.h" #include "build.h" #include "env-util.h" #include "fileio.h" +#include "format-table.h" #include "log.h" #include "main-func.h" +#include "options.h" #include "pretty-print.h" #include "proc-cmdline.h" #include "tpm2-util.h" @@ -18,68 +18,55 @@ static bool arg_graceful = false; static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *options = NULL; int r; r = terminal_urlify_man("systemd-tpm2-clear", "8", &link); if (r < 0) return log_oom(); - printf("%1$s [OPTIONS...]\n" - "\n%5$sRequest clearing of the TPM2 from PC firmware.%6$s\n" - "\n%3$sOptions:%4$s\n" - " -h --help Show this help\n" - " --version Show package version\n" - " --graceful Exit gracefully if no TPM2 device is found\n" - "\nSee the %2$s for details.\n", + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + + printf("%s [OPTIONS...]\n" + "\n%sRequest clearing of the TPM2 from PC firmware.%s\n" + "\n%sOptions:%s\n", program_invocation_short_name, - link, - ansi_underline(), - ansi_normal(), ansi_highlight(), + ansi_normal(), + ansi_underline(), ansi_normal()); + r = table_print_or_warn(options); + if (r < 0) + return r; + + printf("\nSee the %s for details.\n", link); return 0; } static int parse_argv(int argc, char *argv[]) { - enum { - ARG_VERSION = 0x100, - ARG_GRACEFUL, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "graceful", no_argument, NULL, ARG_GRACEFUL }, - {} - }; - - int c; - assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + OptionParser state = { argc, argv }; + + FOREACH_OPTION(&state, c, /* ret_a= */ NULL, /* on_error= */ return c) switch (c) { - case 'h': + OPTION_COMMON_HELP: return help(); - case ARG_VERSION: + OPTION_COMMON_VERSION: return version(); - case ARG_GRACEFUL: + OPTION_LONG("graceful", NULL, "Exit gracefully if no TPM2 device is found"): arg_graceful = true; break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } - if (optind != argc) + if (option_parser_get_n_args(&state) != 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program expects no arguments."); return 1; From 358827d09e0e1de5c871ff7ba4bfce70dde99126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sat, 4 Apr 2026 21:29:38 +0200 Subject: [PATCH 10/13] tpm2-setup: convert to the new option parser --help is identical except for whitespace changes. Co-developed-by: Claude Opus 4.6 --- src/tpm2-setup/tpm2-setup.c | 85 +++++++++++++++---------------------- 1 file changed, 34 insertions(+), 51 deletions(-) diff --git a/src/tpm2-setup/tpm2-setup.c b/src/tpm2-setup/tpm2-setup.c index 92a4bfa12a..74e13219f7 100644 --- a/src/tpm2-setup/tpm2-setup.c +++ b/src/tpm2-setup/tpm2-setup.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include @@ -13,11 +12,13 @@ #include "errno-util.h" #include "fd-util.h" #include "fileio.h" +#include "format-table.h" #include "fs-util.h" #include "hexdecoct.h" #include "log.h" #include "main-func.h" #include "mkdir.h" +#include "options.h" #include "parse-util.h" #include "pretty-print.h" #include "set.h" @@ -38,94 +39,76 @@ STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); #define TPM2_SRK_TPM2B_PUBLIC_PERSISTENT_PATH "/var/lib/systemd/tpm2-srk-public-key.tpm2b_public" #define TPM2_SRK_TPM2B_PUBLIC_RUNTIME_PATH "/run/systemd/tpm2-srk-public-key.tpm2b_public" -static int help(int argc, char *argv[], void *userdata) { +static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *options = NULL; int r; r = terminal_urlify_man("systemd-tpm2-setup", "8", &link); if (r < 0) return log_oom(); - printf("%1$s [OPTIONS...]\n" - "\n%5$sSet up the TPM2 Storage Root Key (SRK), and initialize NvPCRs.%6$s\n" - "\n%3$sOptions:%4$s\n" - " -h --help Show this help\n" - " --version Show package version\n" - " --tpm2-device=PATH\n" - " Pick TPM2 device\n" - " --early=BOOL Store SRK public key in /run/ rather than /var/lib/\n" - " --graceful Exit gracefully if no TPM2 device is found\n" - "\nSee the %2$s for details.\n", + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + + printf("%s [OPTIONS...]\n" + "\n%sSet up the TPM2 Storage Root Key (SRK), and initialize NvPCRs.%s\n" + "\n%sOptions:%s\n", program_invocation_short_name, - link, - ansi_underline(), - ansi_normal(), ansi_highlight(), + ansi_normal(), + ansi_underline(), ansi_normal()); + r = table_print_or_warn(options); + if (r < 0) + return r; + + printf("\nSee the %s for details.\n", link); return 0; } static int parse_argv(int argc, char *argv[]) { - enum { - ARG_VERSION = 0x100, - ARG_TPM2_DEVICE, - ARG_EARLY, - ARG_GRACEFUL, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, - { "early", required_argument, NULL, ARG_EARLY }, - { "graceful", no_argument, NULL, ARG_GRACEFUL }, - {} - }; - - int c, r; + int r; assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + OptionParser state = { argc, argv }; + const char *arg; + + FOREACH_OPTION(&state, c, &arg, /* on_error= */ return c) switch (c) { - case 'h': - return help(0, NULL, NULL); + OPTION_COMMON_HELP: + return help(); - case ARG_VERSION: + OPTION_COMMON_VERSION: return version(); - case ARG_TPM2_DEVICE: - if (streq(optarg, "list")) + OPTION_LONG("tpm2-device", "PATH", "Pick TPM2 device"): + if (streq(arg, "list")) return tpm2_list_devices(/* legend= */ true, /* quiet= */ false); - if (free_and_strdup(&arg_tpm2_device, streq(optarg, "auto") ? NULL : optarg) < 0) + if (free_and_strdup(&arg_tpm2_device, streq(arg, "auto") ? NULL : arg) < 0) return log_oom(); - break; - case ARG_EARLY: - r = parse_boolean(optarg); + OPTION_LONG("early", "BOOL", "Store SRK public key in /run/ rather than /var/lib/"): + r = parse_boolean(arg); if (r < 0) - return log_error_errno(r, "Failed to parse --early= argument: %s", optarg); + return log_error_errno(r, "Failed to parse --early= argument: %s", arg); arg_early = r; break; - case ARG_GRACEFUL: + OPTION_LONG("graceful", NULL, "Exit gracefully if no TPM2 device is found"): arg_graceful = true; break; - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } - if (optind != argc) + if (option_parser_get_n_args(&state) != 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program expects no argument."); return 1; From 3ec64fa2bade54263872b9dde4e4ba5ed66d7a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sat, 4 Apr 2026 21:52:46 +0200 Subject: [PATCH 11/13] updatectl: convert to the new option and verb parsers Cosmetic differences in --help only. Co-developed-by: Claude Opus 4.6 --- src/sysupdate/updatectl.c | 177 ++++++++++++++++++-------------------- 1 file changed, 86 insertions(+), 91 deletions(-) diff --git a/src/sysupdate/updatectl.c b/src/sysupdate/updatectl.c index 636a3f064b..c6e5c33fdf 100644 --- a/src/sysupdate/updatectl.c +++ b/src/sysupdate/updatectl.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include "sd-bus.h" @@ -19,6 +18,7 @@ #include "hashmap.h" #include "json-util.h" #include "main-func.h" +#include "options.h" #include "pager.h" #include "polkit-agent.h" #include "pretty-print.h" @@ -644,6 +644,8 @@ static int describe(sd_bus *bus, const char *target_path, const char *version) { return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, arg_legend); } +VERB(verb_list, "list", "[TARGET[@VERSION]]", VERB_ANY, 2, VERB_DEFAULT|VERB_ONLINE_ONLY, + "List available targets and versions"); static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) { sd_bus *bus = ASSERT_PTR(userdata); _cleanup_free_ char *target_path = NULL, *version = NULL; @@ -755,6 +757,8 @@ static int check_finished(sd_bus_message *reply, void *userdata, sd_bus_error *r return 0; } +VERB(verb_check, "check", "[TARGET...]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "Check for updates"); static int verb_check(int argc, char *argv[], uintptr_t _data, void *userdata) { sd_bus *bus = ASSERT_PTR(userdata); _cleanup_(table_unrefp) Table *table = NULL; @@ -1291,6 +1295,8 @@ static int do_update(sd_bus *bus, char **targets) { return did_anything ? 1 : 0; } +VERB(verb_update, "update", "[TARGET[@VERSION]...]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "Install updates"); static int verb_update(int argc, char *argv[], uintptr_t _data, void *userdata) { sd_bus *bus = ASSERT_PTR(userdata); _cleanup_strv_free_ char **targets = NULL; @@ -1344,6 +1350,8 @@ static int do_vacuum(sd_bus *bus, const char *target, const char *path) { return count + disabled > 0 ? 1 : 0; } +VERB(verb_vacuum, "vacuum", "[TARGET...]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "Clean up old updates"); static int verb_vacuum(int argc, char *argv[], uintptr_t _data, void *userdata) { sd_bus *bus = ASSERT_PTR(userdata); _cleanup_strv_free_ char **targets = NULL, **target_paths = NULL; @@ -1488,6 +1496,8 @@ static int list_features(sd_bus *bus) { return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, arg_legend); } +VERB(verb_features, "features", "[FEATURE]", VERB_ANY, 2, VERB_ONLINE_ONLY, + "List and inspect optional features on host OS"); static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata) { sd_bus *bus = ASSERT_PTR(userdata); _cleanup_(table_unrefp) Table *table = NULL; @@ -1537,6 +1547,10 @@ static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, false); } +VERB(verb_enable, "enable", "FEATURE...", 2, VERB_ANY, VERB_ONLINE_ONLY, + "Enable optional feature on host OS"); +VERB(verb_enable, "disable", "FEATURE...", 2, VERB_ANY, VERB_ONLINE_ONLY, + "Disable optional feature on host OS"); static int verb_enable(int argc, char *argv[], uintptr_t _data, void *userdata) { sd_bus *bus = ASSERT_PTR(userdata); bool did_anything = false, enable; @@ -1618,111 +1632,102 @@ static int verb_enable(int argc, char *argv[], uintptr_t _data, void *userdata) static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *verbs = NULL, *verbs2 = NULL, *options = NULL; int r; r = terminal_urlify_man("updatectl", "1", &link); if (r < 0) return log_oom(); - printf("%1$s [OPTIONS...] [VERSION]\n" - "\n%5$sManage system updates.%6$s\n" - "\n%3$sCommands:%4$s\n" - " list [TARGET[@VERSION]] List available targets and versions\n" - " check [TARGET...] Check for updates\n" - " update [TARGET[@VERSION]...] Install updates\n" - " vacuum [TARGET...] Clean up old updates\n" - " features [FEATURE] List and inspect optional features on host OS\n" - " enable FEATURE... Enable optional feature on host OS\n" - " disable FEATURE... Disable optional feature on host OS\n" - " -h --help Show this help\n" - " --version Show package version\n" - "\n%3$sOptions:%4$s\n" - " --reboot Reboot after updating to newer version\n" - " --offline Do not fetch metadata from the network\n" - " --now Download/delete resources immediately\n" - " -H --host=[USER@]HOST Operate on remote host\n" - " --no-pager Do not pipe output into a pager\n" - " --no-legend Do not show the headers and footers\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_group("Verbs", &verbs2); + if (r < 0) + return r; + + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + + (void) table_sync_column_widths(0, verbs, verbs2, options); + + printf("%s [OPTIONS...] [VERSION]\n" + "\n%sManage system updates.%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; + r = table_print_or_warn(verbs2); + if (r < 0) + return r; + + printf("\n%sOptions:%s\n", + ansi_underline(), + ansi_normal()); + + r = table_print_or_warn(options); + if (r < 0) + return r; + + printf("\nSee the %s for details.\n", link); return 0; } -static int parse_argv(int argc, char *argv[]) { - - enum { - ARG_VERSION = 0x100, - ARG_NO_PAGER, - ARG_NO_LEGEND, - ARG_REBOOT, - ARG_OFFLINE, - ARG_NOW, - }; - - 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 }, - { "host", required_argument, NULL, 'H' }, - { "reboot", no_argument, NULL, ARG_REBOOT }, - { "offline", no_argument, NULL, ARG_OFFLINE }, - { "now", no_argument, NULL, ARG_NOW }, - {} - }; - - int c; +VERB_COMMON_HELP_HIDDEN(help); +static int parse_argv(int argc, char *argv[], char ***ret_args) { assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "hH:", options, NULL)) >= 0) { + OptionParser state = { argc, argv }; + const char *arg; + + FOREACH_OPTION(&state, c, &arg, /* on_error= */ return c) switch (c) { - case 'h': - return help(); - - case ARG_VERSION: - return version(); - - case ARG_NO_PAGER: - arg_pager_flags |= PAGER_DISABLE; - break; - - case ARG_NO_LEGEND: - arg_legend = false; - break; - - case 'H': - arg_transport = BUS_TRANSPORT_REMOTE; - arg_host = optarg; - break; - - case ARG_REBOOT: + OPTION_LONG("reboot", NULL, "Reboot after updating to newer version"): arg_reboot = true; break; - case ARG_OFFLINE: + OPTION_LONG("offline", NULL, "Do not fetch metadata from the network"): arg_offline = true; break; - case ARG_NOW: + OPTION_LONG("now", NULL, "Download/delete resources immediately"): arg_now = true; break; - case '?': - return -EINVAL; + OPTION_COMMON_HOST: + arg_transport = BUS_TRANSPORT_REMOTE; + arg_host = arg; + break; - default: - assert_not_reached(); + OPTION_COMMON_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + OPTION_COMMON_NO_LEGEND: + arg_legend = false; + break; + + OPTION_GROUP("Verbs"): {} + + OPTION_COMMON_HELP: + return help(); + + OPTION_COMMON_VERSION: + return version(); } - } + *ret_args = option_parser_get_args(&state); return 1; } @@ -1730,23 +1735,13 @@ static int run(int argc, char *argv[]) { _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; int r; - static const Verb verbs[] = { - { "list", VERB_ANY, 2, VERB_DEFAULT|VERB_ONLINE_ONLY, verb_list }, - { "check", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_check }, - { "update", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_update }, - { "vacuum", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_vacuum }, - { "features", VERB_ANY, 2, VERB_ONLINE_ONLY, verb_features }, - { "enable", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_enable }, - { "disable", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_enable }, - {} - }; - setlocale(LC_ALL, ""); log_setup(); (void) signal(SIGWINCH, columns_lines_cache_reset); - r = parse_argv(argc, argv); + char **args = NULL; + r = parse_argv(argc, argv, &args); if (r <= 0) return r; @@ -1759,7 +1754,7 @@ static int run(int argc, char *argv[]) { (void) sd_bus_set_allow_interactive_authorization(bus, true); - return dispatch_verb(argc, argv, verbs, bus); + return dispatch_verb_with_args(args, bus); } DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); From c074adda05e71d622bcef55ec0224b13f1507913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sun, 5 Apr 2026 17:38:46 +0200 Subject: [PATCH 12/13] battery-check: convert to the new option parser --help is identical except for whitespace changes. Co-developed-by: Claude Opus 4.6 --- src/battery-check/battery-check.c | 51 ++++++++++++------------------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/src/battery-check/battery-check.c b/src/battery-check/battery-check.c index 43ff0e53e0..706a7d869c 100644 --- a/src/battery-check/battery-check.c +++ b/src/battery-check/battery-check.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include /* IWYU pragma: keep */ #include "sd-messages.h" @@ -9,9 +8,11 @@ #include "battery-util.h" #include "build.h" #include "fd-util.h" +#include "format-table.h" #include "glyph-util.h" #include "log.h" #include "main-func.h" +#include "options.h" #include "plymouth-util.h" #include "pretty-print.h" #include "proc-cmdline.h" @@ -27,22 +28,28 @@ static bool arg_doit = true; static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *options = NULL; int r; r = terminal_urlify_man("systemd-battery-check", "8", &link); if (r < 0) return log_oom(); + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + printf("%s\n\n" - "%sCheck battery level to see whether there's enough charge.%s\n\n" - " -h --help Show this help\n" - " --version Show package version\n" - "\nSee the %s for details.\n", + "%sCheck battery level to see whether there's enough charge.%s\n\n", program_invocation_short_name, ansi_highlight(), - ansi_normal(), - link); + ansi_normal()); + r = table_print_or_warn(options); + if (r < 0) + return r; + + printf("\nSee the %s for details.\n", link); return 0; } @@ -70,41 +77,23 @@ static int plymouth_send_message(const char *mode, const char *message) { return 0; } -static int parse_argv(int argc, char * argv[]) { - - enum { - ARG_VERSION = 0x100, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - {} - }; - - int c; - +static int parse_argv(int argc, char *argv[]) { assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + OptionParser state = { argc, argv }; + FOREACH_OPTION(&state, c, /* ret_a= */ NULL, /* on_error= */ return c) switch (c) { - case 'h': + OPTION_COMMON_HELP: return help(); - case ARG_VERSION: + OPTION_COMMON_VERSION: return version(); - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } - if (optind < argc) + if (option_parser_get_n_args(&state) != 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "%s takes no argument.", program_invocation_short_name); From 88aa9672b916576fdabce39f722030a5282d821e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sun, 5 Apr 2026 17:41:12 +0200 Subject: [PATCH 13/13] boot-check-no-failures: convert to the new option parser --help is identical except for whitespace changes. Co-developed-by: Claude Opus 4.6 --- src/bless-boot/boot-check-no-failures.c | 51 ++++++++++--------------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/src/bless-boot/boot-check-no-failures.c b/src/bless-boot/boot-check-no-failures.c index b301892474..bea5e57916 100644 --- a/src/bless-boot/boot-check-no-failures.c +++ b/src/bless-boot/boot-check-no-failures.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include "sd-bus.h" @@ -8,63 +7,53 @@ #include "alloc-util.h" #include "build.h" #include "bus-error.h" +#include "format-table.h" #include "log.h" #include "main-func.h" +#include "options.h" #include "pretty-print.h" static int help(void) { _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *options = NULL; int r; r = terminal_urlify_man("systemd-boot-check-no-failures.service", "8", &link); if (r < 0) return log_oom(); + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + printf("%s [OPTIONS...]\n" - "\n%sVerify system operational state.%s\n\n" - " -h --help Show this help\n" - " --version Print version\n" - "\nSee the %s for details.\n", + "\n%sVerify system operational state.%s\n\n", program_invocation_short_name, ansi_highlight(), - ansi_normal(), - link); + ansi_normal()); + r = table_print_or_warn(options); + if (r < 0) + return r; + + printf("\nSee the %s for details.\n", link); return 0; } static int parse_argv(int argc, char *argv[]) { - enum { - ARG_PATH = 0x100, - ARG_VERSION, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - {} - }; - - int c; - assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + OptionParser state = { argc, argv }; + + FOREACH_OPTION(&state, c, /* ret_a= */ NULL, /* on_error= */ return c) switch (c) { - case 'h': - help(); - return 0; + OPTION_COMMON_HELP: + return help(); - case ARG_VERSION: + OPTION_COMMON_VERSION: return version(); - - case '?': - return -EINVAL; - - default: - assert_not_reached(); } return 1;