From 4d620b49f50065f5c1e32f3ad20ad6069db3bb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 31 Jul 2026 18:27:47 -0600 Subject: [PATCH 01/26] shared/format-table: add helper to sync widths of a table array --- src/shared/format-table.c | 23 +++++++++++++++++++++++ src/shared/format-table.h | 1 + 2 files changed, 24 insertions(+) diff --git a/src/shared/format-table.c b/src/shared/format-table.c index 772d3475c1..b0c15ca806 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -2250,6 +2250,29 @@ int _table_sync_column_widths(size_t column, Table *a, ...) { return r; } +int table_sync_all_column_widths(size_t column, Table **a) { + size_t max = 0; + int r; + + /* Make the specified column have the same width in the tables. */ + + for (Table **t = a; t && *t; t++) { + size_t w; + + r = table_data_requested_width(*t, column, &w); + if (r < 0) + return log_error_errno(r, "Failed to query table column width: %m"); + + max = MAX(max, w); + } + + r = 0; + for (Table **t = a; t && *t; t++) + RET_GATHER(r, table_set_column_width(*t, column, max)); + + return r; +} + int table_print_full(Table *t, FILE *f, bool flush) { _cleanup_free_ size_t *sorted = NULL; int r; diff --git a/src/shared/format-table.h b/src/shared/format-table.h index 8c0acac217..8087abfb9e 100644 --- a/src/shared/format-table.h +++ b/src/shared/format-table.h @@ -149,6 +149,7 @@ int table_data_requested_width(Table *table, size_t column, size_t *ret); int table_set_column_width(Table *t, size_t column, size_t width); int _table_sync_column_widths(size_t column, Table *a, ...); #define table_sync_column_widths(column, a, ...) _table_sync_column_widths(column, a, __VA_ARGS__, NULL) +int table_sync_all_column_widths(size_t column, Table **a); int table_print_full(Table *t, FILE *f, bool flush); static inline int table_print(Table *t) { From 8616e5487291866203304bc4dd2792577ce4e967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 19 Jun 2026 18:28:19 +0200 Subject: [PATCH 02/26] verbs: add "program header" and generate all help from data We add a CommandDescription that contains basic info about the program: - names (the main name and deprecated aliases) - abstract, man page list, footer This gets stored as a fake verb entry through VERB_COMMAND macro. We can then generate the full help for the program from the verbs and option arrays. The formatting of man page links is changed from the form used previously: the section is printed as ".N" instead of "(N)". This for has the following advantages: - the form with parenthesis looks like a function call, so e.g. close(1) is both a valid call and a man page link. - the new form can be automatically selected with a double click in the typical terminal configurations, while '(' would usually terminate the selection area. - parentheses are special in shell syntax, so even the user manages to successfully select and paste them, they still have to add quoting or escaping on the command line. - the new form is shorter - it looks better --- src/shared/forward.h | 2 + src/shared/options.c | 95 ++++++++++++++++- src/shared/options.h | 11 ++ src/shared/pretty-print.c | 9 +- src/shared/verbs.c | 215 +++++++++++++++++++++++++++++++++++++- src/shared/verbs.h | 37 ++++++- 6 files changed, 356 insertions(+), 13 deletions(-) diff --git a/src/shared/forward.h b/src/shared/forward.h index 38c8dc03e5..323163a1d6 100644 --- a/src/shared/forward.h +++ b/src/shared/forward.h @@ -85,6 +85,7 @@ typedef struct MachineCredentialContext MachineCredentialContext; typedef struct MountOptions MountOptions; typedef struct MStack MStack; typedef struct OpenFile OpenFile; +typedef struct Option Option; typedef struct Pkcs11EncryptedKey Pkcs11EncryptedKey; typedef struct QmpClient QmpClient; typedef struct QmpSlot QmpSlot; @@ -94,4 +95,5 @@ typedef struct Tpm2Handle Tpm2Handle; typedef struct Tpm2PCRValue Tpm2PCRValue; typedef struct UnitInfo UnitInfo; typedef struct UserRecord UserRecord; +typedef struct Verb Verb; typedef struct VeritySettings VeritySettings; diff --git a/src/shared/options.c b/src/shared/options.c index 41f9c6fd28..ec6c142af3 100644 --- a/src/shared/options.c +++ b/src/shared/options.c @@ -470,6 +470,24 @@ char* option_get_synopsis(const Option *opt, const char *joiner, bool show_metav option_arg_optional(opt) ? "]" : ""); } +const Option* options_find_namespace( + const Option options[], + const Option options_end[], + const char *namespace) { + + if (!namespace) + /* The first part is the default unnamed namespace, so + * if the namespace was not specified, we are in it. */ + return options; + + for (const Option *opt = options; opt < options_end; opt++) + if (FLAGS_SET(opt->flags, OPTION_NAMESPACE_MARKER) && + streq(namespace, ASSERT_PTR(opt->long_code))) + return opt + 1; + + return NULL; /* not found :/ */ +} + int _option_parser_get_help_table_full( const Option options[], const Option options_end[], @@ -478,6 +496,8 @@ int _option_parser_get_help_table_full( Table **ret) { int r; + // TODO: drop this function and all its wrappers + assert(ret); _cleanup_(table_unrefp) Table *table = table_new("names", "help"); @@ -492,7 +512,8 @@ int _option_parser_get_help_table_full( * ? The first part is the default group, so if the group was * not specified, we are in it. */ - for (const Option *opt = options; opt < options_end; opt++) { + const Option *opt; + for (opt = options; opt < options_end; opt++) { bool ns_marker = FLAGS_SET(opt->flags, OPTION_NAMESPACE_MARKER); if (!in_ns) { in_ns = ns_marker && streq(namespace, opt->long_code); @@ -543,5 +564,75 @@ int _option_parser_get_help_table_full( table_set_header(table, false); *ret = TAKE_PTR(table); - return 0; + assert(opt - options < INT_MAX); + return opt - options; +} + +int options_get_help_table_group( + const Option options[], + const Option options_end[], + Table **ret, + const char **ret_group) { + int r; + + assert(ret); + assert(ret_group); + + _cleanup_(table_unrefp) Table *table = table_new("names", "help"); + if (!table) + return log_oom(); + + assert(options_end > options); + + bool group_marker = FLAGS_SET(options[0].flags, OPTION_GROUP_MARKER); + const char *group = group_marker ? ASSERT_PTR(options[0].long_code) : NULL; + + const Option *opt; + for (opt = options + group_marker; opt < options_end; opt++) { + if (FLAGS_SET(opt->flags, OPTION_NAMESPACE_MARKER)) + /* End of our namespace */ + break; + + if (FLAGS_SET(opt->flags, OPTION_GROUP_MARKER)) + /* End of the group */ + break; + + if (!opt->help) + /* No help string — we do not show the option */ + continue; + + _cleanup_free_ char *s = option_get_synopsis(opt, " ", /* show_metavar= */ true); + if (!s) + return log_oom(); + + /* We indent the option string by two spaces. We could set the minimum cell width and + * right-align for a similar result, but that'd be more work. This is only used for + * display. */ + bool shift_left = opt->short_code != 0 || FLAGS_SET(opt->flags, OPTION_HELP_ENTRY_VERBATIM); + const char *prefix = shift_left ? " " : " "; + _cleanup_free_ char *t = strjoin(prefix, s); + if (!t) + return log_oom(); + + r = table_add_many(table, TABLE_STRING, t); + if (r < 0) + return table_log_add_error(r); + + _cleanup_strv_free_ char **split = strv_split(opt->help, /* separators= */ NULL); + if (!split) + return log_oom(); + + r = table_add_many(table, TABLE_STRV_WRAPPED, split); + if (r < 0) + return table_log_add_error(r); + } + + assert(!table_isempty(table)); /* Empty group? Something is off. */ + + table_set_header(table, false); + *ret = TAKE_PTR(table); + *ret_group = group; + + assert(opt - options < INT_MAX); + return opt - options; } diff --git a/src/shared/options.h b/src/shared/options.h index 561cd349e8..f4f62ff9b2 100644 --- a/src/shared/options.h +++ b/src/shared/options.h @@ -266,6 +266,11 @@ char* option_parser_get_arg(const OptionParser *state, size_t i); char* option_get_synopsis(const Option *opt, const char *joiner, bool show_metavar); +const Option* options_find_namespace( + const Option options[], + const Option options_end[], + const char *namespace); + int _option_parser_get_help_table_full( const Option options[], const Option options_end[], @@ -280,3 +285,9 @@ int _option_parser_get_help_table_full( option_parser_get_help_table_full(/* namespace= */ NULL, group, ret) #define option_parser_get_help_table(ret) \ option_parser_get_help_table_group(/* group= */ NULL, ret) + +int options_get_help_table_group( + const Option options[], + const Option options_end[], + Table **ret, + const char **ret_group); diff --git a/src/shared/pretty-print.c b/src/shared/pretty-print.c index c5ace49f60..1b978832f7 100644 --- a/src/shared/pretty-print.c +++ b/src/shared/pretty-print.c @@ -180,8 +180,13 @@ int terminal_urlify_path(const char *path, const char *text, char **ret) { int terminal_urlify_man_full(const char *page, const char *section, const char *suffix, char **ret) { const char *url, *text; - url = strjoina("man:", page, "(", section, ")"); - text = strjoina(page, "(", section, ")", suffix); + if (section) { + url = strjoina("man:", page, "(", section, ")"); + text = strjoina(page, "(", section, ")", suffix); + } else { + url = strjoina("man:", page); + text = strjoina(page, suffix); + } return terminal_urlify(url, text, ret); } diff --git a/src/shared/verbs.c b/src/shared/verbs.c index 34e8a897d6..726ed30ac2 100644 --- a/src/shared/verbs.c +++ b/src/shared/verbs.c @@ -2,7 +2,11 @@ #include "env-util.h" #include "format-table.h" +#include "help-util.h" #include "log.h" +#include "nulstr-util.h" +#include "options.h" +#include "pretty-print.h" #include "string-util.h" #include "strv.h" #include "terminal-util.h" @@ -58,15 +62,16 @@ bool should_bypass(const char *env_prefix) { } static bool verb_is_metadata(const Verb *verb) { - /* A metadata entry that is not a real verb, like the group marker */ - return FLAGS_SET(ASSERT_PTR(verb)->flags, VERB_GROUP_MARKER); + /* A metadata entry that is not a real verb, i.e. either the command and group markers. */ + return FLAGS_SET(ASSERT_PTR(verb)->flags, VERB_COMMAND_MARKER) || + FLAGS_SET(ASSERT_PTR(verb)->flags, VERB_GROUP_MARKER); } const Verb* verbs_find_verb(const char *name, const Verb verbs[], const Verb verbs_end[]) { assert(verbs); assert(verbs_end > verbs); assert((uintptr_t) verbs % sizeof(void*) == 0); - assert(verbs[0].verb); + assert(FLAGS_SET(verbs[0].flags, VERB_COMMAND_MARKER) || verbs[0].verb); for (const Verb *verb = verbs; verb < verbs_end; verb++) { if (verb_is_metadata(verb)) @@ -86,7 +91,7 @@ int _dispatch_verb(char **args, const Verb verbs[], const Verb verbs_end[], void assert(verbs); assert(verbs_end > verbs); assert((uintptr_t) verbs % sizeof(void*) == 0); - assert(verbs[0].verb); + assert(FLAGS_SET(verbs[0].flags, VERB_COMMAND_MARKER) || verbs[0].verb); const char *name = args ? args[0] : NULL; size_t left = strv_length(args); @@ -214,6 +219,26 @@ static int verb_add_help_one(Table *table, const Verb *verb) { return 0; } +static const Verb* verbs_get_command(const Verb verbs[], const Verb verbs_end[], const char *name) { + assert(verbs); + assert(verbs_end > verbs); + assert((uintptr_t) verbs % sizeof(void*) == 0); + assert(FLAGS_SET(verbs[0].flags, VERB_COMMAND_MARKER) || verbs[0].verb); + assert(name); + + for (const Verb *verb = verbs; verb < verbs_end; verb++) { + if (!FLAGS_SET(verb->flags, VERB_COMMAND_MARKER)) + continue; + + const CommandDescription *cmd = (const CommandDescription*) ASSERT_PTR(verb->data); + if (nulstr_contains(cmd->names, name)) + return verb; + } + + /* At the end of the list? */ + return NULL; +} + int _verbs_get_help_table( const Verb verbs[], const Verb verbs_end[], @@ -234,7 +259,18 @@ int _verbs_get_help_table( * group ? The first part is the default group, so * if the group was not specified, we are in. */ - for (const Verb *verb = verbs; verb < verbs_end; verb++) { + const Verb *verb; + for (verb = verbs; verb < verbs_end; verb++) { + /* Binaries with one or more VERB_COMMAND entries call this function + * through print_verb_option_help() which provides a verbs slice that + * starts after the VERB_COMMAND entry. + * + * In unconverted binaries, this function is called directly. */ + // TODO: make this function static after everything has been converted. + + if (FLAGS_SET(verb->flags, VERB_COMMAND_MARKER)) + break; + assert(verb->verb); bool group_marker = FLAGS_SET(verb->flags, VERB_GROUP_MARKER); @@ -256,5 +292,174 @@ int _verbs_get_help_table( table_set_header(table, false); *ret = TAKE_PTR(table); + + assert(verb - verbs < INT_MAX); + return verb - verbs; /* Return the count of verb entries "consumed". */ +} + +static void table_array_freep(Table ***tables) { + assert(tables); + + for (Table **t = *tables; t && *t; t++) + *t = table_unref(*t); + free(*tables); +} + +static int print_verb_option_help( + const CommandDescription *cmd, + const Verb verbs[], + const Verb verbs_end[], + const Option options[], + const Option options_end[]) { + + assert(verbs); + assert(verbs_end >= verbs); + assert((uintptr_t) verbs % sizeof(void*) == 0); + + _cleanup_free_ const char **groups = NULL; + _cleanup_(table_array_freep) Table **tables = NULL; + size_t n_verbs = 0, n_opts = 0; + int r; + + for (const Verb *verb = verbs; verb < verbs_end;) { + if (FLAGS_SET(verb->flags, VERB_COMMAND_MARKER)) + /* Start of entries for another command */ + break; + + assert(verb->verb); + + /* What is the the first group? */ + const char *group = FLAGS_SET(verb->flags, VERB_GROUP_MARKER) ? verb->verb : NULL; + + _cleanup_(table_unrefp) Table *table = NULL; + r = _verbs_get_help_table(verb, verbs_end, group, &table); + if (r < 0) + return r; + + if (!GREEDY_REALLOC0(tables, n_verbs + 2)) + return -ENOMEM; + if (!GREEDY_REALLOC0(groups, n_verbs + 2)) + return -ENOMEM; + + tables[n_verbs] = TAKE_PTR(table); + groups[n_verbs++] = group ?: "Commands"; + + verb += r; /* Skip over the whole verb group */ + } + + const Option *optns = options_find_namespace(options, options_end, cmd->option_namespace); + if (!optns) + return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), + "Option namespace %s not found.", + cmd->option_namespace ?: "(unnamed)"); + + for (const Option *opt = optns; opt < options_end;) { + _cleanup_(table_unrefp) Table *table = NULL; + const char *group; + + if (FLAGS_SET(opt->flags, OPTION_NAMESPACE_MARKER)) + /* End of our namespace */ + break; + + r = options_get_help_table_group(opt, options_end, &table, &group); + if (r < 0) + return r; + + if (!GREEDY_REALLOC0(tables, n_verbs + n_opts + 2)) + return -ENOMEM; + if (!GREEDY_REALLOC0(groups, n_verbs + n_opts + 2)) + return -ENOMEM; + + tables[n_verbs + n_opts] = TAKE_PTR(table); + groups[n_verbs + n_opts++] = group ?: "Options"; + + opt += r; /* Skip over the whole option group */ + } + + (void) table_sync_all_column_widths(0, tables); + + for (size_t i = 0; i < n_verbs + n_opts; i++) { + if (table_isempty(tables[i])) + continue; + + help_section(groups[i]); + + r = table_print_or_warn(tables[i]); + if (r < 0) + return r; + } + + return 0; +} + +static int print_man_links(const char *pages) { + int r; + + NULSTR_FOREACH(page, pages) { + _cleanup_free_ char *link = NULL; + + r = terminal_urlify_man(page, /* section= */ NULL, &link); + if (r < 0) + return r; + + printf("\nSee the %s for details.\n", link); + } + + return 0; +} + +static bool verbs_array_has_verbs(const Verb verbs[], const Verb verbs_end[]) { + for (const Verb *verb = verbs; verb < verbs_end; verb++) { + if (FLAGS_SET(verb->flags, VERB_COMMAND_MARKER)) /* start of another command section */ + break; + if (!verb->help) /* a hidden verb */ + continue; + return true; + } + + return false; +} + +int _command_print_help( + const Verb verbs[], + const Verb verbs_end[], + const Option options[], + const Option options_end[], + const char *name) { + int r; + + const Verb *cmdverb = verbs_get_command(verbs, verbs_end, name); + if (!cmdverb) + return log_error_errno(SYNTHETIC_ERRNO(ENODATA), + "Command %s not known", name); + + const CommandDescription *cmd = (const CommandDescription*) ASSERT_PTR(cmdverb->data); + + if (cmd->argspec) + NULSTR_FOREACH(spec, cmd->argspec) { + _cleanup_free_ char *line = strjoin("[OPTION…] ", spec); + if (!line) + return log_oom(); + help_cmdline(line); + } + else { + bool have_verbs = verbs_array_has_verbs(cmdverb + 1, verbs_end); + help_cmdline(have_verbs ? "[OPTION…] COMMAND …" : "[OPTION…]"); + } + + if (cmd->abstract) + help_abstract(cmd->abstract); + + r = print_verb_option_help(cmd, cmdverb + 1, verbs_end, options, options_end); + if (r < 0) + return log_error_errno(r, "Failed to print verb&option help: %m"); + + if (cmd->footer) + printf("\n%s\n", cmd->footer); + + r = print_man_links(cmd->man_pages); + if (r < 0) + return log_error_errno(r, "Failed to print man page links: %m"); + return 0; } diff --git a/src/shared/verbs.h b/src/shared/verbs.h index b7ecfbcb6d..0cbaba8203 100644 --- a/src/shared/verbs.h +++ b/src/shared/verbs.h @@ -2,17 +2,28 @@ #pragma once #include "forward.h" +#include "options.h" #define VERB_ANY (UINT_MAX) +typedef struct CommandDescription { + const char *names; /* nulstr with the main command name and potentially aliases */ + const char *abstract; /* concrete abstract */ + const char *argspec; /* optional specification of positional args in synopsis */ + const char *footer; /* optional footer to print right above man page links */ + const char *man_pages; /* nulstr with man page names */ + const char *option_namespace; /* to be used when the options are in a namespace */ +} CommandDescription; + typedef enum VerbFlags { - VERB_DEFAULT = 1 << 0, /* The verb to run if no verb is specified */ - VERB_ONLINE_ONLY = 1 << 1, /* Just do nothing when running in chroot or offline */ - VERB_GROUP_MARKER = 1 << 2, /* Fake verb entry to separate groups */ + VERB_DEFAULT = 1 << 0, /* The verb to run if no verb is specified */ + VERB_ONLINE_ONLY = 1 << 1, /* Just do nothing when running in chroot or offline */ + VERB_COMMAND_MARKER = 1 << 2, /* Header entry with command description */ + VERB_GROUP_MARKER = 1 << 3, /* Fake verb entry to separate groups */ } VerbFlags; /* Note: see the comment on struct Option in options.h for why _alignptr_ is required here. */ -typedef struct _alignptr_ { +typedef struct _alignptr_ Verb { const char *verb; unsigned min_args, max_args; VerbFlags flags; @@ -71,6 +82,12 @@ assert_cc(sizeof(Verb) % sizeof(void*) == 0); _VERB_DATA(/* d= */ NULL, /* v= */ gr, /* a= */ NULL, /* amin= */ 0, /* amax= */ 0, \ /* f= */ VERB_GROUP_MARKER, /* dat= */ 0, /* h= */ NULL) +#define _COMMAND(u, ...) \ + static const CommandDescription UNIQ_T(description, u) = { __VA_ARGS__ }; \ + _VERB_DATA(/* d= */ NULL, /* v= */ NULL, /* a= */ NULL, /* amin= */ 0, /* amax= */ 0, \ + /* f= */ VERB_COMMAND_MARKER, /* dat= */ (uintptr_t) &UNIQ_T(description, u), /* h= */ NULL) +#define COMMAND(...) _COMMAND(UNIQ, __VA_ARGS__) + /* This is magically mapped to the beginning and end of the section */ extern const Verb __start_SYSTEMD_VERBS[]; extern const Verb __stop_SYSTEMD_VERBS[]; @@ -107,3 +124,15 @@ int _verbs_get_help_table( #define VERB_COMMON_HELP_HIDDEN(impl) \ VERB(verb_help, "help", NULL, VERB_ANY, VERB_ANY, 0, NULL); \ _VERB_COMMON_HELP_IMPL(impl) + +int _command_print_help( + const Verb verbs[], + const Verb verbs_end[], + const Option options[], + const Option options_end[], + const char *name); +#define command_print_help(name) \ + _command_print_help( \ + __start_SYSTEMD_VERBS, __stop_SYSTEMD_VERBS, \ + __start_SYSTEMD_OPTIONS, __stop_SYSTEMD_OPTIONS, \ + name) From 0ebe40080e6aaee501b749695fac13eefe7b6f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 24 Jul 2026 15:25:37 +0200 Subject: [PATCH 03/26] id128: convert to VERB_COMMAND --- src/id128/id128.c | 43 +++++++------------------------------------ 1 file changed, 7 insertions(+), 36 deletions(-) diff --git a/src/id128/id128.c b/src/id128/id128.c index 5326ef1f51..7ddbdffbef 100644 --- a/src/id128/id128.c +++ b/src/id128/id128.c @@ -26,6 +26,12 @@ static PagerFlags arg_pager_flags = 0; static bool arg_legend = true; static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF; +COMMAND( + "systemd-id128\0", + "Generate and print 128-bit identifiers.", + .man_pages = "systemd-id128.1\0", +); + static int print_id(sd_id128_t id) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL; int r; @@ -229,42 +235,7 @@ static int verb_show(int argc, char *argv[], uintptr_t _data, void *userdata) { } static int help(void) { - _cleanup_free_ char *link = NULL; - _cleanup_(table_unrefp) Table *options = NULL, *verbs = NULL; - int r; - - r = terminal_urlify_man("systemd-id128", "1", &link); - if (r < 0) - return log_oom(); - - r = option_parser_get_help_table(&options); - if (r < 0) - return r; - - r = verbs_get_help_table(&verbs); - if (r < 0) - return r; - - /* Make the 1st column same width in both tables */ - (void) table_sync_column_widths(0, options, verbs); - - printf("%s [OPTIONS...] COMMAND\n\n" - "%sGenerate and print 128-bit identifiers.%s\n" - "\nCommands:\n", - program_invocation_short_name, - 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("\nSee the %s for details.\n", link); - return 0; + return command_print_help("systemd-id128"); } VERB_COMMON_HELP(help); From 0059c3f5f249a977ce0fe89e59937452e507925d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 24 Jul 2026 15:35:00 +0200 Subject: [PATCH 04/26] verbs: introduce VERB_COMMON_HELP_AUTO{,_HIDDEN} --- src/id128/id128.c | 9 ++------- src/shared/verbs.h | 10 ++++++++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/id128/id128.c b/src/id128/id128.c index 7ddbdffbef..20490ef5b9 100644 --- a/src/id128/id128.c +++ b/src/id128/id128.c @@ -14,7 +14,6 @@ #include "main-func.h" #include "options.h" #include "parse-argument.h" -#include "pretty-print.h" #include "string-util.h" #include "strv.h" #include "verbs.h" @@ -234,11 +233,7 @@ static int verb_show(int argc, char *argv[], uintptr_t _data, void *userdata) { return 0; } -static int help(void) { - return command_print_help("systemd-id128"); -} - -VERB_COMMON_HELP(help); +VERB_COMMON_HELP_AUTO("systemd-id128"); static int parse_argv(int argc, char *argv[], char ***ret_args) { int r; @@ -251,7 +246,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { OPTION_COMMON_HELP: - return help(); + return command_print_help("systemd-id128"); OPTION_COMMON_VERSION: return version(); diff --git a/src/shared/verbs.h b/src/shared/verbs.h index 0cbaba8203..59fb8a3951 100644 --- a/src/shared/verbs.h +++ b/src/shared/verbs.h @@ -125,6 +125,12 @@ int _verbs_get_help_table( VERB(verb_help, "help", NULL, VERB_ANY, VERB_ANY, 0, NULL); \ _VERB_COMMON_HELP_IMPL(impl) +#define VERB_COMMON_HELP_AUTO(program) \ + VERB_FULL(verb_help_auto, "help", NULL, VERB_ANY, VERB_ANY, 0, (uintptr_t) program, "Show this help") + +#define VERB_COMMON_HELP_AUTO_HIDDEN(program) \ + VERB_FULL(verb_help_auto, "help", NULL, VERB_ANY, VERB_ANY, 0, (uintptr_t) program, NULL) + int _command_print_help( const Verb verbs[], const Verb verbs_end[], @@ -136,3 +142,7 @@ int _command_print_help( __start_SYSTEMD_VERBS, __stop_SYSTEMD_VERBS, \ __start_SYSTEMD_OPTIONS, __stop_SYSTEMD_OPTIONS, \ name) + +static inline int verb_help_auto(int argc, char **argv, uintptr_t data, void *userdata) { + return command_print_help((const char*) ASSERT_PTR(data)); +} From 7e2ef99cfb17c1be0043fb59897ad47eb7302062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 24 Jul 2026 15:43:54 +0200 Subject: [PATCH 05/26] timedatectl: convert to VERB_COMMAND --- src/timedate/timedatectl.c | 58 ++++++-------------------------------- 1 file changed, 8 insertions(+), 50 deletions(-) diff --git a/src/timedate/timedatectl.c b/src/timedate/timedatectl.c index 2d76488f90..24432b82c2 100644 --- a/src/timedate/timedatectl.c +++ b/src/timedate/timedatectl.c @@ -24,7 +24,6 @@ #include "parse-argument.h" #include "parse-util.h" #include "polkit-agent.h" -#include "pretty-print.h" #include "runtime-scope.h" #include "sparse-endian.h" #include "string-table.h" @@ -180,6 +179,12 @@ static int print_status_info(const StatusInfo *i) { return 0; } +COMMAND( + "timedatectl\0", + .abstract = "Query or change system time and date settings.", + .man_pages = "timedatectl.1\0", +); + VERB_DEFAULT_NOARG(verb_status, "status", "Show current time settings"); static int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) { StatusInfo info = {}; @@ -893,54 +898,7 @@ static int verb_revert(int argc, char *argv[], uintptr_t _data, void *userdata) return 0; } -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("timedatectl", "1", &link); - if (r < 0) - return log_oom(); - - r = verbs_get_help_table(&verbs); - if (r < 0) - return r; - - r = verbs_get_help_table_group("systemd-timesyncd Commands", &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...] COMMAND ...\n" - "\n%sQuery or change system time and date settings.%s\n" - "\nCommands:\n", - program_invocation_short_name, - ansi_highlight(), - ansi_normal()); - r = table_print_or_warn(verbs); - if (r < 0) - return r; - - printf("\nsystemd-timesyncd Commands:\n"); - r = table_print_or_warn(verbs2); - if (r < 0) - return r; - - printf("\nOptions:\n"); - r = table_print_or_warn(options); - if (r < 0) - return r; - - printf("\nSee the %s for details.\n", link); - return 0; -} - -VERB_COMMON_HELP_HIDDEN(help); +VERB_COMMON_HELP_AUTO_HIDDEN("timedatectl"); static int parse_argv(int argc, char *argv[], char ***ret_args) { int r; @@ -953,7 +911,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { OPTION_COMMON_HELP: - return help(); + return command_print_help("timedatectl"); OPTION_COMMON_VERSION: return version(); From e256153186f98a05fa470ecc4bc91b34ec827078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 24 Jul 2026 16:32:27 +0200 Subject: [PATCH 06/26] verbs: add flags and allow verbs&options tables to not be synchronized --- src/shared/verbs.c | 8 +++++++- src/shared/verbs.h | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/shared/verbs.c b/src/shared/verbs.c index 726ed30ac2..cc0b358fcd 100644 --- a/src/shared/verbs.c +++ b/src/shared/verbs.c @@ -347,6 +347,9 @@ static int print_verb_option_help( verb += r; /* Skip over the whole verb group */ } + if (cmd->flags & COMMAND_HELP_SEPARATE) + (void) table_sync_all_column_widths(0, tables); + const Option *optns = options_find_namespace(options, options_end, cmd->option_namespace); if (!optns) return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), @@ -376,7 +379,10 @@ static int print_verb_option_help( opt += r; /* Skip over the whole option group */ } - (void) table_sync_all_column_widths(0, tables); + if (cmd->flags & COMMAND_HELP_SEPARATE) + (void) table_sync_all_column_widths(0, tables + n_verbs); + else + (void) table_sync_all_column_widths(0, tables); for (size_t i = 0; i < n_verbs + n_opts; i++) { if (table_isempty(tables[i])) diff --git a/src/shared/verbs.h b/src/shared/verbs.h index 59fb8a3951..52b3cfd9ba 100644 --- a/src/shared/verbs.h +++ b/src/shared/verbs.h @@ -6,6 +6,10 @@ #define VERB_ANY (UINT_MAX) +typedef enum CommandFlags { + COMMAND_HELP_SEPARATE = 1 << 0, /* Do not synchronize the width between verbs and options */ +} CommandFlags; + typedef struct CommandDescription { const char *names; /* nulstr with the main command name and potentially aliases */ const char *abstract; /* concrete abstract */ @@ -13,6 +17,7 @@ typedef struct CommandDescription { const char *footer; /* optional footer to print right above man page links */ const char *man_pages; /* nulstr with man page names */ const char *option_namespace; /* to be used when the options are in a namespace */ + CommandFlags flags; } CommandDescription; typedef enum VerbFlags { From 65662644050757ecc1ba73785a4c6ede8b6db213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 24 Jul 2026 16:32:56 +0200 Subject: [PATCH 07/26] homectl: convert to VERB_COMMAND --- src/home/homectl.c | 95 +++++----------------------------------------- 1 file changed, 9 insertions(+), 86 deletions(-) diff --git a/src/home/homectl.c b/src/home/homectl.c index 98c24bafe1..72abf01525 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -32,7 +32,6 @@ #include "fs-util.h" #include "glyph-util.h" #include "hashmap.h" -#include "help-util.h" #include "hexdecoct.h" #include "home-util.h" #include "homectl-fido2.h" @@ -180,6 +179,13 @@ static int acquire_bus(sd_bus **bus) { return 0; } +COMMAND( + "homectl\0", + "Create, manipulate or inspect home directories.", + .man_pages = "homectl.1\0", + .flags = COMMAND_HELP_SEPARATE, /* the option table is very wide */ +); + VERB_GROUP("Basic User Manipulation Commands"); VERB(verb_list_homes, "list", /* argspec= */ NULL, VERB_ANY, 1, VERB_DEFAULT, "List home areas"); @@ -4002,90 +4008,7 @@ static int parse_fido2_device_field(const char *arg) { return 1; } -static int help(void) { - static const char* const vgroups[] = { - "Basic User Manipulation Commands", - "Advanced User Manipulation Commands", - "User Migration Commands", - "Signing Keys Commands", - "Lock/Unlock Commands", - "Other Commands", - }; - - static const char* const ogroups[] = { - NULL, - "General User Record Properties", - "Authentication User Record Properties", - "Blob Directory User Record Properties", - "Account Management User Record Properties", - "Password Policy User Record Properties", - "Resource Management User Record Properties", - "Storage User Record Properties", - "LUKS Storage User Record Properties", - "Mounting User Record Properties", - "CIFS User Record Properties", - "Login Behaviour User Record Properties", - }; - - Table *vtables[ELEMENTSOF(vgroups)] = {}; - CLEANUP_ELEMENTS(vtables, table_unref_array_clear); - Table *otables[ELEMENTSOF(ogroups)] = {}; - CLEANUP_ELEMENTS(otables, table_unref_array_clear); - int r; - - for (size_t i = 0; i < ELEMENTSOF(vgroups); i++) { - r = verbs_get_help_table_group(vgroups[i], &vtables[i]); - if (r < 0) - return r; - } - - for (size_t i = 0; i < ELEMENTSOF(ogroups); i++) { - r = option_parser_get_help_table_group(ogroups[i], &otables[i]); - if (r < 0) - return r; - } - - /* The two groups are not synchronized because the option table is very wide. */ - - assert_cc(ELEMENTSOF(vtables) == 6); - (void) table_sync_column_widths(0, vtables[0], vtables[1], vtables[2], - vtables[3], vtables[4], vtables[5]); - - assert_cc(ELEMENTSOF(otables) == 12); - (void) table_sync_column_widths(0, otables[0], otables[1], otables[2], otables[3], - otables[4], otables[5], otables[6], otables[7], - otables[8], otables[9], otables[10], otables[11]); - - pager_open(arg_pager_flags); - - help_cmdline("[OPTIONS…] COMMAND …"); - help_abstract("Create, manipulate or inspect home directories."); - - for (size_t i = 0; i < ELEMENTSOF(vgroups); i++) { - help_section(vgroups[i]); - r = table_print_or_warn(vtables[i]); - if (r < 0) - return r; - } - - help_section("Options"); - r = table_print_or_warn(otables[0]); - if (r < 0) - return r; - - for (size_t i = 1; i < ELEMENTSOF(ogroups); i++) { - help_section(ogroups[i]); - r = table_print_or_warn(otables[i]); - if (r < 0) - return r; - } - - help_man_page_reference("homectl", "1"); - - return 0; -} - -VERB_COMMON_HELP_HIDDEN(help); +VERB_COMMON_HELP_AUTO_HIDDEN("homectl"); static int parse_argv(int argc, char *argv[], char ***remaining_args) { _cleanup_strv_free_ char **arg_languages = NULL; @@ -4114,7 +4037,7 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { switch (c) { OPTION_COMMON_HELP: - return help(); + return command_print_help("homectl"); OPTION_COMMON_VERSION: return version(); From d94e88fe14d5ab07f39b99057887d11b499e8424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 24 Jul 2026 16:42:13 +0200 Subject: [PATCH 08/26] varlinkctl: convert to VERB_COMMAND help verb is moved to the end as in most other commands. --- src/varlinkctl/varlinkctl.c | 54 ++++++++----------------------------- 1 file changed, 11 insertions(+), 43 deletions(-) diff --git a/src/varlinkctl/varlinkctl.c b/src/varlinkctl/varlinkctl.c index 48c500f7c4..2e825aff7f 100644 --- a/src/varlinkctl/varlinkctl.c +++ b/src/varlinkctl/varlinkctl.c @@ -23,7 +23,6 @@ #include "format-table.h" #include "format-util.h" #include "fs-util.h" -#include "help-util.h" #include "log.h" #include "main-func.h" #include "memfd-util.h" @@ -81,47 +80,6 @@ static void push_fds_done(PushFds *p) { STATIC_DESTRUCTOR_REGISTER(arg_graceful, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_push_fds, push_fds_done); -static int help(void) { - _cleanup_(table_unrefp) Table *options = NULL, *verbs = NULL; - int r; - - r = option_parser_get_help_table(&options); - 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); - - help_cmdline("[OPTIONS...] COMMAND ..."); - help_abstract("Introspect Varlink Services."); - - help_section("Commands"); - - r = table_print_or_warn(verbs); - if (r < 0) - return r; - - help_section("Options"); - - 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); - - help_man_page_reference("varlinkctl", "1"); - return 0; -} - -VERB_COMMON_HELP(help); - static int parse_argv(int argc, char *argv[], char ***ret_args) { int r; @@ -134,7 +92,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { switch (c) { OPTION_COMMON_HELP: - return help(); + return command_print_help("varlinkctl"); OPTION_COMMON_VERSION: return version(); @@ -325,6 +283,14 @@ static void get_info_data_done(GetInfoData *d) { d->interfaces = strv_free(d->interfaces); } +COMMAND( + "varlinkctl\0", + "Introspect Varlink Services.", + .man_pages = "varlinkctl.1\0", + .footer = "With --exec, specify the command to invoke:\n" + " varlinkctl --exec call ADDRESS METHOD PARAMS -- CMDLINE…", +); + 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) { @@ -1488,6 +1454,8 @@ static int verb_serve(int argc, char *argv[], uintptr_t _data, void *userdata) { return 0; } +VERB_COMMON_HELP_AUTO("varlinkctl"); + static int run(int argc, char *argv[]) { int r; From c81b5d36b9be4403feb76b92a7505e0b2b53c5ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 28 Jul 2026 01:06:35 +0200 Subject: [PATCH 09/26] veritysetup: add the usual parse_argv() function More options will be added later, so let's use the usual setup. --- src/veritysetup/veritysetup.c | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/veritysetup/veritysetup.c b/src/veritysetup/veritysetup.c index 8e8543ba5b..b4cdba6562 100644 --- a/src/veritysetup/veritysetup.c +++ b/src/veritysetup/veritysetup.c @@ -6,7 +6,7 @@ #include "sd-device.h" #include "alloc-util.h" -#include "argv-util.h" +#include "build.h" #include "cryptsetup-util.h" #include "dlopen-note.h" #include "extract-word.h" @@ -486,11 +486,36 @@ static int verb_detach(int argc, char *argv[], uintptr_t _data, void *userdata) return 0; } +VERB_COMMON_HELP_HIDDEN(help); + +static int parse_argv(int argc, char *argv[], char ***ret_args) { + assert(argc >= 0); + assert(argv); + assert(ret_args); + + OptionParser opts = { argc, argv }; + + FOREACH_OPTION_OR_RETURN(c, &opts) + switch (c) { + + OPTION_COMMON_HELP: + return help(); + + OPTION_COMMON_VERSION: + return version(); + } + + *ret_args = option_parser_get_args(&opts); + return 1; +} + static int run(int argc, char *argv[]) { int r; - if (argv_looks_like_help(argc, argv)) - return help(); + char **args = NULL; + r = parse_argv(argc, argv, &args); + if (r <= 0) + return r; log_setup(); @@ -503,7 +528,7 @@ static int run(int argc, char *argv[]) { umask(0022); - return dispatch_verb(strv_skip(argv, 1), /* userdata= */ NULL); + return dispatch_verb(args, /* userdata= */ NULL); } DEFINE_MAIN_FUNCTION(run); From edb15285d0460df69202280fdf8817246ee2986e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 24 Jul 2026 19:53:43 +0200 Subject: [PATCH 10/26] systemd-veritysetup: convert to VERB_COMMAND The --help output now shows -h/--help. "verity protected" is changed to the more correct spelling of "verity-protected". --- src/veritysetup/veritysetup.c | 37 +++++++++++------------------------ 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/src/veritysetup/veritysetup.c b/src/veritysetup/veritysetup.c index b4cdba6562..50bea8d66b 100644 --- a/src/veritysetup/veritysetup.c +++ b/src/veritysetup/veritysetup.c @@ -4,6 +4,7 @@ #include #include "sd-device.h" +#include "sd-json.h" #include "alloc-util.h" #include "build.h" @@ -11,9 +12,7 @@ #include "dlopen-note.h" #include "extract-word.h" #include "fileio.h" -#include "format-table.h" #include "fstab-util.h" -#include "help-util.h" #include "hexdecoct.h" #include "log.h" #include "main-func.h" @@ -51,26 +50,6 @@ STATIC_DESTRUCTOR_REGISTER(arg_fec_what, freep); STATIC_DESTRUCTOR_REGISTER(arg_root_hash_signature, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_measure_nvpcr, freep); -static int help(void) { - _cleanup_(table_unrefp) Table *verbs = NULL; - int r; - - r = verbs_get_help_table(&verbs); - if (r < 0) - return r; - - help_cmdline("COMMAND ..."); - help_abstract("Attach or detach a verity protected block device."); - - help_section("Commands"); - r = table_print_or_warn(verbs); - if (r < 0) - return r; - - help_man_page_reference("systemd-veritysetup@.service", "8"); - return 0; -} - static int parse_roothashsig_option(const char *option, bool strict) { _cleanup_free_ void *rhs = NULL; size_t rhss = 0; @@ -318,8 +297,14 @@ static int parse_options(const char *options) { return r; } +COMMAND( + "systemd-veritysetup\0", + "Attach or detach a verity-protected block device.", + .man_pages = "systemd-veritysetup@.service.8\0", +); + VERB(verb_attach, "attach", "VOLUME DATADEVICE HASHDEVICE ROOTHASH [OPTIONS]", 5, 6, 0, - "Attach a verity protected block device"); + "Attach a verity-protected block device"); static int verb_attach(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(crypt_freep) struct crypt_device *cd = NULL; _cleanup_free_ void *rh = NULL; @@ -457,7 +442,7 @@ static int verb_attach(int argc, char *argv[], uintptr_t _data, void *userdata) } VERB(verb_detach, "detach", "VOLUME", 2, 2, 0, - "Detach a verity protected block device"); + "Detach a verity-protected block device"); static int verb_detach(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(crypt_freep) struct crypt_device *cd = NULL; int r; @@ -486,7 +471,7 @@ static int verb_detach(int argc, char *argv[], uintptr_t _data, void *userdata) return 0; } -VERB_COMMON_HELP_HIDDEN(help); +VERB_COMMON_HELP_AUTO_HIDDEN("systemd-veritysetup"); static int parse_argv(int argc, char *argv[], char ***ret_args) { assert(argc >= 0); @@ -499,7 +484,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { switch (c) { OPTION_COMMON_HELP: - return help(); + return command_print_help("systemd-veritysetup"); OPTION_COMMON_VERSION: return version(); From 37e35682367ecc7415f8e1ae6f15450445d45496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 24 Jul 2026 20:15:25 +0200 Subject: [PATCH 11/26] systemd-ac-power: convert to VERB_COMMAND --- src/ac-power/ac-power.c | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/src/ac-power/ac-power.c b/src/ac-power/ac-power.c index 87242a3b08..f94908873b 100644 --- a/src/ac-power/ac-power.c +++ b/src/ac-power/ac-power.c @@ -1,13 +1,13 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "sd-json.h" + #include "battery-util.h" #include "build.h" -#include "format-table.h" -#include "help-util.h" #include "log.h" #include "main-func.h" -#include "options.h" #include "string-util.h" +#include "verbs.h" static bool arg_verbose = false; @@ -16,25 +16,11 @@ static enum { ACTION_LOW, } arg_action = ACTION_AC_POWER; -static int help(void) { - int r; - - _cleanup_(table_unrefp) Table *options = NULL; - r = option_parser_get_help_table(&options); - if (r < 0) - return r; - - help_cmdline("[OPTIONS...]"); - help_abstract("Report whether we are connected to an external power source."); - - help_section("Options"); - r = table_print_or_warn(options); - if (r < 0) - return r; - - help_man_page_reference("systemd-ac-power", "1"); - return 0; -} +COMMAND( + "systemd-ac-power\0", + "Report whether we are connected to an external power source.", + .man_pages = "systemd-ac-power.1\0", +); static int parse_argv(int argc, char *argv[]) { @@ -46,7 +32,7 @@ static int parse_argv(int argc, char *argv[]) { FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { OPTION_COMMON_HELP: - return help(); + return command_print_help("systemd-ac-power"); OPTION_COMMON_VERSION: return version(); From 24acd23d8d27a66071e677437b739319cc726f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 24 Jul 2026 20:24:30 +0200 Subject: [PATCH 12/26] systemd-analyze: convert to VERB_COMMAND --- src/analyze/analyze.c | 65 +++++-------------------------------------- 1 file changed, 7 insertions(+), 58 deletions(-) diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index d6675f5eb0..ba2dc0a070 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -57,8 +57,6 @@ #include "dissect-image.h" #include "dlopen-note.h" #include "extract-word.h" -#include "format-table.h" -#include "help-util.h" #include "image-policy.h" #include "log.h" #include "loop-util.h" @@ -198,62 +196,13 @@ static int verb_transient_settings(int argc, char *argv[], uintptr_t _data, void return 0; } -static int help(void) { - static const char *const vgroups[] = { - "Boot Analysis", - "Dependency Analysis", - "Configuration Files and Search Paths", - "Enumerate OS Concepts", - "Expression Evaluation", - "Clock & Time", - "Unit & Service Analysis", - "Executable Analysis", - "TPM Operations", - }; +COMMAND( + "systemd-analyze\0", + .abstract = "Profile systemd, show unit dependencies, check unit files.", + .man_pages = "systemd-analyze.1\0", +); - Table *vtables[ELEMENTSOF(vgroups)] = {}; - CLEANUP_ELEMENTS(vtables, table_unref_array_clear); - _cleanup_(table_unrefp) Table *options = NULL; - int r; - - pager_open(arg_pager_flags); - - for (size_t i = 0; i < ELEMENTSOF(vgroups); i++) { - r = verbs_get_help_table_group(vgroups[i], &vtables[i]); - if (r < 0) - return r; - } - - r = option_parser_get_help_table(&options); - if (r < 0) - return r; - - assert_cc(ELEMENTSOF(vtables) == 9); - (void) table_sync_column_widths(0, options, vtables[0], vtables[1], vtables[2], - vtables[3], vtables[4], vtables[5], vtables[6], - vtables[7], vtables[8]); - - help_cmdline("[OPTIONS...] COMMAND ..."); - help_abstract("Profile systemd, show unit dependencies, check unit files."); - - for (size_t i = 0; i < ELEMENTSOF(vgroups); i++) { - help_section(vgroups[i]); - r = table_print_or_warn(vtables[i]); - if (r < 0) - return r; - } - - help_section("Options"); - r = table_print_or_warn(options); - if (r < 0) - return r; - - help_man_page_reference("systemd-analyze", "1"); - - return 0; -} - -VERB_COMMON_HELP_HIDDEN(help); +VERB_COMMON_HELP_AUTO_HIDDEN("systemd-analyze"); /* When updating this list, including descriptions, apply changes to * shell-completion/bash/systemd-analyze and shell-completion/zsh/_systemd-analyze too. */ @@ -386,7 +335,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { break; OPTION_COMMON_HELP: - return help(); + return command_print_help("systemd-analyze"); OPTION_COMMON_VERSION: return version(); From 92462517a1197b4d19b452dfccb5cee9746cd8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 24 Jul 2026 20:28:33 +0200 Subject: [PATCH 13/26] systemd-ask-password: convert to VERB_COMMAND --- src/ask-password/ask-password.c | 39 +++++++-------------------------- 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/src/ask-password/ask-password.c b/src/ask-password/ask-password.c index fd0def44f1..4f75a94c39 100644 --- a/src/ask-password/ask-password.c +++ b/src/ask-password/ask-password.c @@ -9,20 +9,18 @@ #include "build.h" #include "bus-polkit.h" #include "constants.h" -#include "format-table.h" #include "hashmap.h" #include "json-util.h" #include "log.h" #include "main-func.h" -#include "options.h" #include "parse-argument.h" -#include "pretty-print.h" #include "string-table.h" #include "string-util.h" #include "strv.h" #include "time-util.h" #include "varlink-io.systemd.AskPassword.h" #include "varlink-util.h" +#include "verbs.h" static const char *arg_icon = NULL; static const char *arg_id = NULL; /* identifier for 'ask-password' protocol */ @@ -38,35 +36,14 @@ static bool arg_varlink = false; STATIC_DESTRUCTOR_REGISTER(arg_message, freep); -static int help(void) { - _cleanup_free_ char *link = NULL; - _cleanup_(table_unrefp) Table *options = NULL; - int r; - - r = terminal_urlify_man("systemd-ask-password", "1", &link); - if (r < 0) - return log_oom(); - - r = option_parser_get_help_table(&options); - if (r < 0) - return r; - - printf("%s [OPTIONS...] MESSAGE\n" - "\n%sQuery the user for a passphrase, via the TTY or a UI agent.%s\n" - "\nOptions:\n", - program_invocation_short_name, - 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; -} +COMMAND( + "systemd-ask-password\0", + "Query the user for a passphrase, via the TTY or a UI agent.", + .argspec = "MESSAGE\0", + .man_pages = "systemd-ask-password.1\0", +); static int parse_argv(int argc, char *argv[]) { - const char *emoji = NULL; int r; @@ -78,7 +55,7 @@ static int parse_argv(int argc, char *argv[]) { FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { OPTION_COMMON_HELP: - return help(); + return command_print_help("systemd-ask-password"); OPTION_COMMON_VERSION: return version(); From c9cd66ee3ac0c1be534b3f2fea2756a966cefd1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 28 Jul 2026 01:27:52 +0200 Subject: [PATCH 14/26] backlight: add parse_argv() --- src/backlight/backlight.c | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/src/backlight/backlight.c b/src/backlight/backlight.c index a84640d45d..2fa25bd26c 100644 --- a/src/backlight/backlight.c +++ b/src/backlight/backlight.c @@ -6,7 +6,7 @@ #include "sd-device.h" #include "alloc-util.h" -#include "argv-util.h" +#include "build.h" #include "device-private.h" #include "device-util.h" #include "escape.h" @@ -639,15 +639,42 @@ static int verb_save(int argc, char *argv[], uintptr_t _data, void *userdata) { return 0; } -static int run(int argc, char *argv[]) { - log_setup(); +VERB_COMMON_HELP_HIDDEN(help); - if (argv_looks_like_help(argc, argv)) - return help(); +static int parse_argv(int argc, char *argv[], char ***ret_args) { + assert(argc >= 0); + assert(argv); + assert(ret_args); + + OptionParser opts = { argc, argv }; + + FOREACH_OPTION_OR_RETURN(c, &opts) + switch (c) { + + OPTION_COMMON_HELP: + return help(); + + OPTION_COMMON_VERSION: + return version(); + } + + *ret_args = option_parser_get_args(&opts); + return 1; +} + +static int run(int argc, char *argv[]) { + int r; + + log_setup(); umask(0022); - return dispatch_verb(strv_skip(argv, 1), /* userdata= */ NULL); + char **args = NULL; + r = parse_argv(argc, argv, &args); + if (r <= 0) + return r; + + return dispatch_verb(args, /* userdata= */ NULL); } DEFINE_MAIN_FUNCTION(run); From a60c999d5b753ab48dc49580dd60485d77bfbc4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 24 Jul 2026 20:50:26 +0200 Subject: [PATCH 15/26] systemd-backlight: convert to VERB_COMMAND --- src/backlight/backlight.c | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/src/backlight/backlight.c b/src/backlight/backlight.c index 2fa25bd26c..d1fbce9033 100644 --- a/src/backlight/backlight.c +++ b/src/backlight/backlight.c @@ -4,6 +4,7 @@ #include #include "sd-device.h" +#include "sd-json.h" #include "alloc-util.h" #include "build.h" @@ -11,8 +12,6 @@ #include "device-util.h" #include "escape.h" #include "fileio.h" -#include "format-table.h" -#include "help-util.h" #include "main-func.h" #include "parse-util.h" #include "percent-util.h" @@ -23,25 +22,12 @@ #define PCI_CLASS_GRAPHICS_CARD 0x30000 -static int help(void) { - _cleanup_(table_unrefp) Table *verbs = NULL; - int r; - - r = verbs_get_help_table(&verbs); - if (r < 0) - return r; - - help_cmdline("COMMAND [backlight|leds]:DEVICE"); - help_abstract("Save and restore backlight brightness at shutdown and boot."); - - help_section("Commands"); - r = table_print_or_warn(verbs); - if (r < 0) - return r; - - help_man_page_reference("systemd-backlight", "8"); - return 0; -} +COMMAND( + "systemd-backlight\0", + "Save and restore backlight brightness at shutdown and boot.", + .argspec = "COMMAND [backlight|leds]:DEVICE\0", + .man_pages = "systemd-backlight.8\0", +); static int has_multiple_graphics_cards(void) { _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; @@ -639,7 +625,7 @@ static int verb_save(int argc, char *argv[], uintptr_t _data, void *userdata) { return 0; } -VERB_COMMON_HELP_HIDDEN(help); +VERB_COMMON_HELP_AUTO_HIDDEN("systemd-backlight"); static int parse_argv(int argc, char *argv[], char ***ret_args) { assert(argc >= 0); @@ -652,7 +638,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { switch (c) { OPTION_COMMON_HELP: - return help(); + return command_print_help("systemd-backlight"); OPTION_COMMON_VERSION: return version(); From 580489eeec04f98553ce1c85e2052270610f1d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 24 Jul 2026 21:15:24 +0200 Subject: [PATCH 16/26] resolvectl: drop --help for systemd-resolve The rename was done in v239. We generally don't document obsolete interfaces, so let's just drop the (very nice) --help that we have here. --- src/resolve/resolvectl.c | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/src/resolve/resolvectl.c b/src/resolve/resolvectl.c index 9af5626059..96ce7539fc 100644 --- a/src/resolve/resolvectl.c +++ b/src/resolve/resolvectl.c @@ -3211,32 +3211,6 @@ static void help_dns_classes(void) { DUMP_STRING_TABLE(dns_class, int, _DNS_CLASS_MAX); } -static int compat_help(void) { - _cleanup_(table_unrefp) Table *options = NULL; - int r; - - r = option_parser_get_help_table_ns("systemd-resolve", &options); - if (r < 0) - return r; - - pager_open(arg_pager_flags); - - help_cmdline("[OPTIONS…] HOSTNAME|ADDRESS…"); - help_cmdline("[OPTIONS…] --service [[NAME] TYPE] DOMAIN"); - help_cmdline("[OPTIONS…] --openpgp EMAIL@DOMAIN…"); - help_cmdline("[OPTIONS…] --statistics"); - help_cmdline("[OPTIONS…] --reset-statistics"); - help_abstract("Resolve domain names, IPv4 and IPv6 addresses, DNS records, and services."); - - help_section("Options"); - r = table_print_or_warn(options); - if (r < 0) - return r; - - help_man_page_reference("resolvectl", "1"); - return 0; -} - static int native_help(void) { _cleanup_(table_unrefp) Table *verbs = NULL, *options = NULL; int r; @@ -3288,7 +3262,8 @@ static int compat_parse_argv(int argc, char *argv[], char ***remaining_args) { OPTION_NAMESPACE("systemd-resolve"): {} OPTION_COMMON_HELP: - return compat_help(); + printf("systemd-resolve is deprecated. Call resolvectl instead.\n"); + return 0; OPTION_COMMON_VERSION: return version(); From df03655bc5dde87a8fa95fd08308ecb4b3d3eaae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 24 Jul 2026 23:05:06 +0200 Subject: [PATCH 17/26] resolvectl: convert to VERB_COMMAND --- src/resolve/resolvconf-compat.c | 41 ++++++++++------------------ src/resolve/resolvectl.c | 47 +++++++-------------------------- 2 files changed, 24 insertions(+), 64 deletions(-) diff --git a/src/resolve/resolvconf-compat.c b/src/resolve/resolvconf-compat.c index d3ddec755a..9b556469a7 100644 --- a/src/resolve/resolvconf-compat.c +++ b/src/resolve/resolvconf-compat.c @@ -2,18 +2,18 @@ #include +#include "sd-json.h" + #include "alloc-util.h" #include "build.h" #include "extract-word.h" #include "fileio.h" -#include "format-table.h" -#include "help-util.h" #include "log.h" -#include "options.h" #include "resolvconf-compat.h" #include "resolvectl.h" #include "string-util.h" #include "strv.h" +#include "verbs.h" typedef enum LookupType { LOOKUP_TYPE_REGULAR, @@ -21,35 +21,22 @@ typedef enum LookupType { LOOKUP_TYPE_EXCLUSIVE, /* -x */ } LookupType; -static int resolvconf_help(void) { - _cleanup_(table_unrefp) Table *options = NULL; - int r; - - r = option_parser_get_help_table_ns("resolvconf", &options); - if (r < 0) - return r; - - help_cmdline("-a INTERFACE Date: Sat, 25 Jul 2026 11:39:48 -0600 Subject: [PATCH 18/26] systemd-dissect: convert to VERB_COMMAND --- src/dissect/dissect.c | 82 ++++++++++++------------------------------- 1 file changed, 23 insertions(+), 59 deletions(-) diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 04701429ad..44d1fc8029 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -42,7 +42,6 @@ #include "mountpoint-util.h" #include "namespace-util.h" #include "nsresource.h" -#include "options.h" #include "parse-argument.h" #include "parse-util.h" #include "path-util.h" @@ -60,6 +59,7 @@ #include "tmpfile-util.h" #include "uid-classification.h" #include "user-util.h" +#include "verbs.h" #include "vpick.h" static enum { @@ -121,63 +121,27 @@ STATIC_DESTRUCTOR_REGISTER(arg_loop_ref, freep); STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); STATIC_DESTRUCTOR_REGISTER(arg_image_filter, image_filter_freep); -static int help(void) { - _cleanup_free_ char *link = NULL; - _cleanup_(table_unrefp) Table *options = NULL, *commands = NULL; - int r; - - pager_open(arg_pager_flags); - - r = terminal_urlify_man("systemd-dissect", "1", &link); - if (r < 0) - return log_oom(); - - r = option_parser_get_help_table_ns("systemd-dissect", &options); - if (r < 0) - return r; - - r = option_parser_get_help_table_full("systemd-dissect", "Commands", &commands); - if (r < 0) - return r; - - /* Make the 1st column same width in both tables */ - (void) table_sync_column_widths(0, options, commands); - - printf("%1$s [OPTIONS...] IMAGE\n" - "%1$s [OPTIONS...] --mount IMAGE PATH\n" - "%1$s [OPTIONS...] --umount PATH\n" - "%1$s [OPTIONS...] --attach IMAGE\n" - "%1$s [OPTIONS...] --detach PATH\n" - "%1$s [OPTIONS...] --list IMAGE\n" - "%1$s [OPTIONS...] --mtree IMAGE\n" - "%1$s [OPTIONS...] --with IMAGE [COMMAND…]\n" - "%1$s [OPTIONS...] --copy-from IMAGE PATH [TARGET]\n" - "%1$s [OPTIONS...] --copy-to IMAGE [SOURCE] PATH\n" - "%1$s [OPTIONS...] --make-archive IMAGE [TARGET]\n" - "%1$s [OPTIONS...] --discover\n" - "%1$s [OPTIONS...] --validate IMAGE\n" - "%1$s [OPTIONS...] --shift IMAGE UIDBASE\n" - "\n%2$sDissect a Discoverable Disk Image (DDI).%3$s\n" - "\n%4$sOptions:%5$s\n", - program_invocation_short_name, - ansi_highlight(), - ansi_normal(), - ansi_underline(), - ansi_normal()); - - r = table_print_or_warn(options); - if (r < 0) - return r; - - printf("\n%sCommands:%s\n", ansi_underline(), ansi_normal()); - - r = table_print_or_warn(commands); - if (r < 0) - return r; - - printf("\nSee the %s for details.\n", link); - return 0; -} +COMMAND( + "systemd-dissect\0", + "Dissect a Discoverable Disk Image (DDI).", + .argspec = + "IMAGE\0" + "--mount IMAGE PATH\0" + "--umount PATH\0" + "--attach IMAGE\0" + "--detach PATH\0" + "--list IMAGE\0" + "--mtree IMAGE\0" + "--with IMAGE [COMMAND…]\0" + "--copy-from IMAGE PATH [TARGET]\0" + "--copy-to IMAGE [SOURCE] PATH\0" + "--make-archive IMAGE [TARGET]\0" + "--discover\0" + "--validate IMAGE\0" + "--shift IMAGE UIDBASE\0", + .man_pages = "systemd-dissect.1\0", + .option_namespace = "systemd-dissect", +); static int parse_image_path_argument(const char *path, char **ret_root, char **ret_image) { _cleanup_free_ char *p = NULL; @@ -439,7 +403,7 @@ static int parse_argv(int argc, char *argv[]) { OPTION_GROUP("Commands"): {} OPTION_COMMON_HELP: - return help(); + return command_print_help("systemd-dissect"); OPTION_COMMON_VERSION: return version(); From 816ce4eeee842b83a22ee7103eec7ef432678095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sat, 25 Jul 2026 11:50:04 -0600 Subject: [PATCH 19/26] notify: do not print full help output on error Printing a wall of text is obnoxious. Just tell the user what the problem was. --- src/notify/notify.c | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/notify/notify.c b/src/notify/notify.c index 667a7a6447..b322c5fc7f 100644 --- a/src/notify/notify.c +++ b/src/notify/notify.c @@ -283,7 +283,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { case ACTION_NOTIFY: { if (arg_fdname && fdset_isempty(arg_fds)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No file descriptors passed, but --fdname= set, refusing."); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No file descriptors passed, but --fdname= used, refusing."); size_t n_arg_env; @@ -308,14 +308,10 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { n_arg_env = strv_length(args); have_env = have_env || n_arg_env > 0; - if (!have_env) { - if (do_exec) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No notify message specified while --exec, refusing."); - - /* No argument at all? */ - help(); - return -EINVAL; - } + if (!have_env) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + do_exec ? "No notify message specified with --exec, refusing." : + "Nothing to notify about was specified."); if (n_arg_env > 0) { arg_env = strv_copy_n(args, n_arg_env); From adbec875af8af917d39511fedb7e2f33b7290f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sat, 25 Jul 2026 11:51:53 -0600 Subject: [PATCH 20/26] systemd-notify: convert to VERB_COMMAND --- src/notify/notify.c | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/src/notify/notify.c b/src/notify/notify.c index b322c5fc7f..05c5577784 100644 --- a/src/notify/notify.c +++ b/src/notify/notify.c @@ -6,6 +6,7 @@ #include "sd-daemon.h" #include "sd-event.h" +#include "sd-json.h" #include "alloc-util.h" #include "build.h" @@ -15,13 +16,10 @@ #include "exit-status.h" #include "fd-util.h" #include "fdset.h" -#include "format-table.h" #include "format-util.h" -#include "help-util.h" #include "log.h" #include "main-func.h" #include "notify-recv.h" -#include "options.h" #include "parse-util.h" #include "pidref.h" #include "process-util.h" @@ -30,6 +28,7 @@ #include "strv.h" #include "time-util.h" #include "user-util.h" +#include "verbs.h" static enum { ACTION_NOTIFY, @@ -56,27 +55,15 @@ STATIC_DESTRUCTOR_REGISTER(arg_exec, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_fds, fdset_freep); STATIC_DESTRUCTOR_REGISTER(arg_fdname, freep); -static int help(void) { - int r; - - _cleanup_(table_unrefp) Table *options = NULL; - r = option_parser_get_help_table(&options); - if (r < 0) - return r; - - help_cmdline("[OPTIONS...] [VARIABLE=VALUE...]"); - help_cmdline("[OPTIONS...] --exec [VARIABLE=VALUE...] ; -- CMDLINE..."); - help_cmdline("[OPTIONS...] --fork -- CMDLINE..."); - help_abstract("Notify the service manager about service status updates."); - - help_section("Options"); - r = table_print_or_warn(options); - if (r < 0) - return r; - - help_man_page_reference("systemd-notify", "1"); - return 0; -} +COMMAND( + "systemd-notify\0", + "Notify the service manager about service status updates.", + .argspec = + "[VARIABLE=VALUE…]\0" + "--exec [VARIABLE=VALUE…] ; -- CMDLINE…\0" + "--fork -- CMDLINE…\0", + .man_pages = "systemd-notify.1\0", +); static int get_manager_pid(PidRef *ret) { int r; @@ -159,7 +146,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { switch (c) { OPTION_COMMON_HELP: - return help(); + return command_print_help("systemd-notify"); OPTION_COMMON_VERSION: return version(); @@ -413,7 +400,6 @@ static int on_child(sd_event_source *s, const siginfo_t *si, void *userdata) { } static int action_fork(char *const *_command) { - static const int forward_signals[] = { SIGHUP, SIGTERM, From 6c83ea9eda6870b22d8ee2b1b6201c907b9d108c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sun, 1 Mar 2026 14:42:33 +0100 Subject: [PATCH 21/26] Add infrastracture to introspect commandline binaries This adds the helpers to add a hidden --introspect-cli option which prints a machine-readable description of the program's interface and exits. Applicable to any programs that have been converted to VERB_COMMAND(). The output format follows the draft CLI-Introspection Specification (https://github.com/behrmann/uapi-specifications/blob/cli-introspection/specs/cli_introspection.md): a single JSON document with a fixed "mediaType" string and an array of command objects, one per VERB_COMMAND() defined in the binary, so multicall binaries are described in full. Options are taken from the command's option namespace, so e.g. systemd-dissect does not list the mount.ddi mount-helper options. $ build/systemd-id128 --json=pretty --introspect-cli { "mediaType" : "application/vnd.io.systemd.cli-introspection-0", "commands" : [ { "names" : [ "systemd-id128" ], "version" : "262~devel", "features" : [ "-PAM", ... ], "abstract" : [ "Generate and print 128-bit identifiers." ], "options" : [ { "names" : [ "-h", "--help" ], "argument" : "no_argument", "help" : "Show this help" }, ... ], "verbs" : [ { "names" : [ "new" ], "abstract" : [ "Generate a new ID" ], "maxArguments" : 1 }, ... ] } ] } $ build/systemd-id128 --introspect-cli | jq '.commands[0].options[].names[-1]' "--help" "--version" "--no-pager" "--no-legend" "--json" "-j" "--pretty" "--value" "--app-specific" "--uuid" "--introspect-cli" --- src/shared/options.c | 92 +++++++++++++++++++++++++ src/shared/options.h | 16 +++++ src/shared/verbs.c | 159 +++++++++++++++++++++++++++++++++++++++++++ src/shared/verbs.h | 15 ++++ 4 files changed, 282 insertions(+) diff --git a/src/shared/options.c b/src/shared/options.c index ec6c142af3..2bc160f4cd 100644 --- a/src/shared/options.c +++ b/src/shared/options.c @@ -1,5 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "sd-json.h" + #include "format-table.h" #include "log.h" #include "options.h" @@ -636,3 +638,93 @@ int options_get_help_table_group( assert(opt - options < INT_MAX); return opt - options; } + +static int option_build_json(const Option *opt, const char *group, sd_json_variant **ret) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *names = NULL; + int r; + + assert(opt); + assert(opt->short_code != 0 || opt->long_code); + assert(ret); + + if (opt->short_code != 0) { + char s[3] = { '-', opt->short_code }; + + r = sd_json_variant_append_arrayb(&names, SD_JSON_BUILD_STRING(s)); + if (r < 0) + return r; + } + + if (opt->long_code) { + _cleanup_free_ char *s = strjoin("--", opt->long_code); + if (!s) + return -ENOMEM; + + r = sd_json_variant_append_arrayb(&names, SD_JSON_BUILD_STRING(s)); + if (r < 0) + return r; + } + + const char *argtype = + option_arg_required(opt) ? "required_argument" : + option_arg_optional(opt) ? "optional_argument" : + "no_argument"; + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_VARIANT("names", names), + SD_JSON_BUILD_PAIR_STRING("argument", argtype), + SD_JSON_BUILD_PAIR_CONDITION( + !!opt->metavar, + "metavar", SD_JSON_BUILD_STRING(opt->metavar)), + SD_JSON_BUILD_PAIR_CONDITION(!!opt->help, "help", SD_JSON_BUILD_STRING(opt->help)), + SD_JSON_BUILD_PAIR_CONDITION( + !!group, + "group", SD_JSON_BUILD_STRV(STRV_MAKE(group)))); +} + +int options_build_json( + const Option options[], + const Option options_end[], + const char *namespace, + sd_json_variant **ret) { + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; + const char *group = NULL; + int r; + + assert(ret); + + const Option *start = options_find_namespace(options, options_end, namespace); + if (!start) + return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), + "Option namespace %s not found.", + namespace ?: "(unnamed)"); + + for (const Option *opt = start; opt < options_end; opt++) { + if (FLAGS_SET(opt->flags, OPTION_NAMESPACE_MARKER)) + break; /* End of our namespace */ + + if (FLAGS_SET(opt->flags, OPTION_GROUP_MARKER)) { + group = opt->long_code; + continue; + } + + if (option_is_metadata(opt)) + /* Positional and help entries are only used for display */ + continue; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *o = NULL; + r = option_build_json(opt, group, &o); + if (r < 0) + return log_error_errno(r, "Failed to build JSON object for option '%s': %m", + opt->long_code ?: CHAR_TO_STR(opt->short_code)); + + r = sd_json_variant_append_array(&array, o); + if (r < 0) + return log_error_errno(r, "Failed to append JSON object to array: %m"); + } + + *ret = TAKE_PTR(array); + return 0; +} diff --git a/src/shared/options.h b/src/shared/options.h index f4f62ff9b2..8fe0cae2e5 100644 --- a/src/shared/options.h +++ b/src/shared/options.h @@ -95,6 +95,14 @@ assert_cc(sizeof(Option) % sizeof(void*) == 0); #define OPTION_COMMON_VERSION \ OPTION_LONG("version", NULL, "Show package version") +#define OPTION_COMMON_INTROSPECT_CLI \ + /* This option is internal-only for now and not shown in --help */ \ + OPTION_LONG("introspect-cli", NULL, /* help= */ NULL) + +/* For programs which do not do option parsing, but check the argv array directly. */ +#define OPTION_COMMON_INTROSPECT_CLI_ENTRY \ + _OPTION_ENTRY(__COUNTER__, /* fl= */ 0, /* sc= */ 0, "introspect-cli", /* mv= */ NULL, /* d= */ 0u, /* h= */ NULL) + #define OPTION_COMMON_NO_PAGER \ OPTION_LONG("no-pager", NULL, "Do not start a pager") @@ -291,3 +299,11 @@ int options_get_help_table_group( const Option options_end[], Table **ret, const char **ret_group); + +/* Build a JSON array of CLI-introspection option objects for the options in the given namespace. + * Returns NULL in *ret if the namespace defines no options. */ +int options_build_json( + const Option options[], + const Option options_end[], + const char *namespace, + sd_json_variant **ret); diff --git a/src/shared/verbs.c b/src/shared/verbs.c index cc0b358fcd..d6804a9154 100644 --- a/src/shared/verbs.c +++ b/src/shared/verbs.c @@ -1,5 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "sd-json.h" + +#include "build.h" #include "env-util.h" #include "format-table.h" #include "help-util.h" @@ -469,3 +472,159 @@ int _command_print_help( return 0; } + +static int verb_build_json(const Verb *verb, sd_json_variant **ret) { + int r; + + assert(verb); + assert(verb->verb); + assert(ret); + + /* Verbs are represented as command objects, as described in the CLI-Introspection + * Specification (https://uapi-group.org/specifications/specs/cli_introspection/). + * + * The "minArguments", "maxArguments", "isDefault", and "isOnlineOnly" fields + * are extensions not (yet) covered by the specification. Note that the argument + * counts include the verb itself. */ + + r = sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_STRV("names", STRV_MAKE(verb->verb)), + SD_JSON_BUILD_PAIR_CONDITION( + !!verb->help, + "abstract", SD_JSON_BUILD_STRV(STRV_MAKE(verb->help))), + SD_JSON_BUILD_PAIR_CONDITION( + verb->min_args != VERB_ANY, + "minArguments", SD_JSON_BUILD_UNSIGNED(verb->min_args)), + SD_JSON_BUILD_PAIR_CONDITION( + verb->max_args != VERB_ANY, + "maxArguments", SD_JSON_BUILD_UNSIGNED(verb->max_args)), + SD_JSON_BUILD_PAIR_CONDITION( + FLAGS_SET(verb->flags, VERB_DEFAULT), + "isDefault", SD_JSON_BUILD_BOOLEAN(true)), + SD_JSON_BUILD_PAIR_CONDITION( + FLAGS_SET(verb->flags, VERB_ONLINE_ONLY), + "isOnlineOnly", SD_JSON_BUILD_BOOLEAN(true))); + if (r < 0) + return log_error_errno(r, "Failed to build JSON object: %m"); + + return 0; +} + +static int command_build_json( + const Verb *cmdverb, + const Verb verbs_end[], + const Option options[], + const Option options_end[], + sd_json_variant **ret) { + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *names = NULL, *opts = NULL, *cmds = NULL; + int r; + + assert(cmdverb); + assert(FLAGS_SET(cmdverb->flags, VERB_COMMAND_MARKER)); + assert(ret); + + const CommandDescription *cmd = (const CommandDescription*) ASSERT_PTR(cmdverb->data); + + NULSTR_FOREACH(name, cmd->names) { + r = sd_json_variant_append_arrayb(&names, SD_JSON_BUILD_STRING(name)); + if (r < 0) + return log_error_errno(r, "Failed to append JSON string to array: %m"); + } + assert(names); /* At least the primary name must be defined */ + + r = options_build_json(options, options_end, cmd->option_namespace, &opts); + if (r < 0) + return r; + + for (const Verb *verb = cmdverb + 1; verb < verbs_end; verb++) { + if (FLAGS_SET(verb->flags, VERB_COMMAND_MARKER)) + break; /* Start of entries for another command */ + + if (FLAGS_SET(verb->flags, VERB_GROUP_MARKER)) + continue; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *o = NULL; + r = verb_build_json(verb, &o); + if (r < 0) + return r; + + r = sd_json_variant_append_array(&cmds, o); + if (r < 0) + return log_error_errno(r, "Failed to append JSON object to array: %m"); + } + + _cleanup_strv_free_ char **features = strv_split(systemd_features, /* separators= */ NULL); + if (!features) + return log_oom(); + + r = sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_VARIANT("names", names), + SD_JSON_BUILD_PAIR_STRING("version", PROJECT_VERSION_FULL), + SD_JSON_BUILD_PAIR_STRV("features", features), + SD_JSON_BUILD_PAIR_CONDITION( + !!cmd->abstract, + "abstract", SD_JSON_BUILD_STRV(STRV_MAKE(cmd->abstract))), + SD_JSON_BUILD_PAIR_CONDITION( + !!cmd->footer, + "footer", SD_JSON_BUILD_STRV(STRV_MAKE(cmd->footer))), + SD_JSON_BUILD_PAIR_CONDITION(!!opts, "options", SD_JSON_BUILD_VARIANT(opts)), + SD_JSON_BUILD_PAIR_CONDITION(!!cmds, "verbs", SD_JSON_BUILD_VARIANT(cmds))); + if (r < 0) + return log_error_errno(r, "Failed to build JSON object: %m"); + + return 0; +} + +int _introspect_cli( + const Verb verbs[], + const Verb verbs_end[], + const Option options[], + const Option options_end[], + sd_json_format_flags_t flags) { + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *commands = NULL, *w = NULL; + int r; + + assert(verbs); + assert(verbs_end > verbs); + assert((uintptr_t) verbs % sizeof(void*) == 0); + + if (flags == SD_JSON_FORMAT_OFF) + flags = SD_JSON_FORMAT_PRETTY_AUTO; + + for (const Verb *verb = verbs; verb < verbs_end; verb++) { + if (!FLAGS_SET(verb->flags, VERB_COMMAND_MARKER)) + continue; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *c = NULL; + r = command_build_json(verb, verbs_end, options, options_end, &c); + if (r < 0) + return r; + + r = sd_json_variant_append_array(&commands, c); + if (r < 0) + return log_error_errno(r, "Failed to append JSON object to array: %m"); + } + + if (!commands) + return log_error_errno(SYNTHETIC_ERRNO(ENODATA), + "No command description defined, cannot introspect."); + + r = sd_json_buildo( + &w, + SD_JSON_BUILD_PAIR_STRING( + "mediaType", + "application/vnd.io.systemd.cli-introspection-0"), + SD_JSON_BUILD_PAIR_VARIANT("commands", commands)); + if (r < 0) + return log_error_errno(r, "Failed to build JSON object: %m"); + + r = sd_json_variant_dump(w, flags, stdout, /* prefix= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to print JSON object: %m"); + + return 0; +} diff --git a/src/shared/verbs.h b/src/shared/verbs.h index 52b3cfd9ba..7ae669dc8a 100644 --- a/src/shared/verbs.h +++ b/src/shared/verbs.h @@ -151,3 +151,18 @@ int _command_print_help( static inline int verb_help_auto(int argc, char **argv, uintptr_t data, void *userdata) { return command_print_help((const char*) ASSERT_PTR(data)); } + +/* Print a machine-readable description of the program's commands, verbs, and options in the format + * defined by the CLI-Introspection Specification. One command object is emitted for each + * VERB_COMMAND() in the verb table, so multicall binaries are described in full. */ +int _introspect_cli( + const Verb verbs[], + const Verb verbs_end[], + const Option options[], + const Option options_end[], + sd_json_format_flags_t flags); +#define introspect_cli(flags) \ + _introspect_cli( \ + __start_SYSTEMD_VERBS, __stop_SYSTEMD_VERBS, \ + __start_SYSTEMD_OPTIONS, __stop_SYSTEMD_OPTIONS, \ + flags) From 1a54ce154c9c5485b77c866327c530d9a1457094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sat, 25 Jul 2026 16:18:19 -0600 Subject: [PATCH 22/26] various: add --introspect-cli --- src/ac-power/ac-power.c | 3 ++ src/analyze/analyze.c | 3 ++ src/ask-password/ask-password.c | 3 ++ src/backlight/backlight.c | 3 ++ src/dissect/dissect.c | 3 ++ src/home/homectl.c | 3 ++ src/id128/id128.c | 3 ++ src/notify/notify.c | 3 ++ src/resolve/resolvconf-compat.c | 3 ++ src/resolve/resolvectl.c | 6 ++++ src/timedate/timedatectl.c | 3 ++ src/varlinkctl/varlinkctl.c | 3 ++ src/veritysetup/veritysetup.c | 3 ++ .../units/TEST-74-AUX-UTILS.introspect-cli.sh | 35 +++++++++++++++++++ 14 files changed, 77 insertions(+) create mode 100755 test/units/TEST-74-AUX-UTILS.introspect-cli.sh diff --git a/src/ac-power/ac-power.c b/src/ac-power/ac-power.c index f94908873b..e52074bcf5 100644 --- a/src/ac-power/ac-power.c +++ b/src/ac-power/ac-power.c @@ -44,6 +44,9 @@ static int parse_argv(int argc, char *argv[]) { OPTION_LONG("low", NULL, "Check if battery is discharging and low"): arg_action = ACTION_LOW; break; + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(SD_JSON_FORMAT_OFF); } if (option_parser_get_n_args(&opts) > 0) diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index ba2dc0a070..271f74ef6f 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -559,6 +559,9 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { strv_free_and_replace(arg_debugger_args, l); break; } + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(arg_json_format_flags); } _cleanup_strv_free_ char **args = strv_copy(option_parser_get_args(&opts)); /* args is [arg1, arg2, …] */ diff --git a/src/ask-password/ask-password.c b/src/ask-password/ask-password.c index 4f75a94c39..725e72ba44 100644 --- a/src/ask-password/ask-password.c +++ b/src/ask-password/ask-password.c @@ -136,6 +136,9 @@ static int parse_argv(int argc, char *argv[]) { OPTION_SHORT('n', NULL, "Do not suffix password written to standard output with newline"): arg_newline = false; break; + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(SD_JSON_FORMAT_OFF); } if (isempty(emoji) || streq(emoji, "auto")) diff --git a/src/backlight/backlight.c b/src/backlight/backlight.c index d1fbce9033..42b7936abe 100644 --- a/src/backlight/backlight.c +++ b/src/backlight/backlight.c @@ -642,6 +642,9 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { OPTION_COMMON_VERSION: return version(); + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(SD_JSON_FORMAT_OFF); } *ret_args = option_parser_get_args(&opts); diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 44d1fc8029..00a38b9367 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -476,6 +476,9 @@ static int parse_argv(int argc, char *argv[]) { OPTION_LONG("shift", NULL, "Shift UID range to selected base"): arg_action = ACTION_SHIFT; break; + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(arg_json_format_flags); } if (system_scope_requested || user_scope_requested) diff --git a/src/home/homectl.c b/src/home/homectl.c index 72abf01525..b2f2882c72 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -4852,6 +4852,9 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { OPTION_SHORT('N', NULL, /* help= */ NULL): match_identity = &arg_identity_extra_other_machines; break; + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(arg_json_format_flags); } if (!strv_isempty(arg_languages)) { diff --git a/src/id128/id128.c b/src/id128/id128.c index 20490ef5b9..a65ba3b66d 100644 --- a/src/id128/id128.c +++ b/src/id128/id128.c @@ -292,6 +292,9 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { OPTION('u', "uuid", NULL, "Output in UUID format"): arg_mode = ID128_PRINT_UUID; break; + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(arg_json_format_flags); } if (arg_mode == ID128_PRINT_PRETTY && sd_json_format_enabled(arg_json_format_flags)) diff --git a/src/notify/notify.c b/src/notify/notify.c index 05c5577784..46acaec6f1 100644 --- a/src/notify/notify.c +++ b/src/notify/notify.c @@ -260,6 +260,9 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { OPTION('q', "quiet", NULL, "Do not show PID of child when forking"): arg_quiet = true; break; + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(SD_JSON_FORMAT_OFF); } bool have_env = arg_ready || arg_stopping || arg_reloading || arg_status || pidref_is_set(&arg_pid) || !fdset_isempty(arg_fds); diff --git a/src/resolve/resolvconf-compat.c b/src/resolve/resolvconf-compat.c index 9b556469a7..51e226da84 100644 --- a/src/resolve/resolvconf-compat.c +++ b/src/resolve/resolvconf-compat.c @@ -236,6 +236,9 @@ int resolvconf_parse_argv(int argc, char *argv[]) { OPTION_LONG("updates-are-enabled", NULL, /* help= */ NULL): return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Switch --updates-are-enabled not supported."); + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(SD_JSON_FORMAT_OFF); } if (arg_mode == _MODE_INVALID) diff --git a/src/resolve/resolvectl.c b/src/resolve/resolvectl.c index 75cb9f9d0a..08e6958d76 100644 --- a/src/resolve/resolvectl.c +++ b/src/resolve/resolvectl.c @@ -3433,6 +3433,9 @@ static int compat_parse_argv(int argc, char *argv[], char ***remaining_args) { if (r < 0) return r; break; + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(arg_json_format_flags); } if (arg_type == 0 && arg_class != 0) @@ -3659,6 +3662,9 @@ static int native_parse_argv(int argc, char *argv[], char ***remaining_args) { OPTION_COMMON_LOWERCASE_J: arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; break; + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(arg_json_format_flags); } if (arg_raw != RAW_NONE && sd_json_format_enabled(arg_json_format_flags)) diff --git a/src/timedate/timedatectl.c b/src/timedate/timedatectl.c index 24432b82c2..06595792bd 100644 --- a/src/timedate/timedatectl.c +++ b/src/timedate/timedatectl.c @@ -963,6 +963,9 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { OPTION('a', "all", NULL, "Show all properties, including empty ones"): SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_SHOW_EMPTY, true); break; + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(SD_JSON_FORMAT_OFF); } *ret_args = option_parser_get_args(&opts); diff --git a/src/varlinkctl/varlinkctl.c b/src/varlinkctl/varlinkctl.c index 2e825aff7f..dafda83152 100644 --- a/src/varlinkctl/varlinkctl.c +++ b/src/varlinkctl/varlinkctl.c @@ -206,6 +206,9 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { arg_push_fds.fds[arg_push_fds.n_fds++] = TAKE_FD(add_fd); break; } + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(arg_json_format_flags); } /* If more than one reply is expected, imply JSON-SEQ output, and set SD_JSON_FORMAT_FLUSH */ diff --git a/src/veritysetup/veritysetup.c b/src/veritysetup/veritysetup.c index 50bea8d66b..afce00bbe7 100644 --- a/src/veritysetup/veritysetup.c +++ b/src/veritysetup/veritysetup.c @@ -488,6 +488,9 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { OPTION_COMMON_VERSION: return version(); + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(SD_JSON_FORMAT_OFF); } *ret_args = option_parser_get_args(&opts); diff --git a/test/units/TEST-74-AUX-UTILS.introspect-cli.sh b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh new file mode 100755 index 0000000000..1752bdc238 --- /dev/null +++ b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Just a smoke test for the introspection code +for i in systemd-ac-power systemd-analyze systemd-ask-password systemd-dissect \ + systemd-id128 systemd-notify homectl resolvectl timedatectl varlinkctl; do + command -v "$i" >/dev/null || continue + + $i --introspect-cli | jq -e \ + '.mediaType == "application/vnd.io.systemd.cli-introspection-0"' + $i --introspect-cli | jq -e --arg name "$i" \ + 'any(.commands[]; .names[0] == $name)' + $i --introspect-cli | jq -e \ + 'any(.commands[]; [.options[].names[-1]] | contains(["--help", "--version", "--introspect-cli"]))' + $i --intro | grep -e --help +done + +# Those are not in $PATH and do not do regular option parsing, so the option must +# be given verbatim +for i in /usr/lib/systemd/systemd-backlight /usr/lib/systemd/systemd-veritysetup; do + [[ -x "$i" ]] || continue + + $i --introspect-cli | jq -e \ + '.mediaType == "application/vnd.io.systemd.cli-introspection-0"' +done + +# systemd-id128 defines verbs, check that they are described +systemd-id128 --introspect-cli | jq -e \ + '.commands[0].verbs | map(.names[0]) | contains(["new", "machine-id", "show", "help"])' + +# resolvectl is a multicall binary, check that both commands are described +resolvectl --introspect-cli | jq -e \ + '[.commands[].names[0]] | sort == ["resolvconf", "resolvectl"]' From 8a14181f55fd62d1763df886342d8bfb1918af42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 29 Jul 2026 10:18:16 -0600 Subject: [PATCH 23/26] resolvectl: separate systemd-resolve in introspection data Previously, the systemd-resolve compat alias was described in the same CommandDescription structure and resolvectl. But they have different option sets, so this doesn't really work. The alias is deprecated, so describe it as such in the abstract, but give it a separate entry. $ build/resolvectl --introspect-cli { "mediaType": "application/vnd.io.systemd.cli-introspection-0", "commands": [ { "names": [ "resolvconf" ], "abstract": [ "Register DNS server and domain configuration with systemd-resolved." ], ... }, { "names": [ "resolvectl" ], "abstract": [ "Send control commands to the network name resolution manager, or\nresolve domain names, IPv4 and IPv6 addresses, DNS records, and services." ], ... }, { "names": [ "systemd-resolve" ], "abstract": [ "Send control commands to the network name resolution manager, or\nresolve domain names, IPv4 and IPv6 addresses, DNS records, and services.\n\nThis command is deprecated. Use resolvectl.1 instead." ], ... } ] } --- src/resolve/resolvectl.c | 9 ++++++++- test/units/TEST-74-AUX-UTILS.introspect-cli.sh | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/resolve/resolvectl.c b/src/resolve/resolvectl.c index 08e6958d76..c774d93211 100644 --- a/src/resolve/resolvectl.c +++ b/src/resolve/resolvectl.c @@ -96,7 +96,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_set_domain, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_set_nta, strv_freep); COMMAND( - "resolvectl\0systemd-resolve\0", + "resolvectl\0", "Send control commands to the network name resolution manager, or\n" "resolve domain names, IPv4 and IPv6 addresses, DNS records, and services.", .man_pages = "resolvectl.1\0", @@ -3220,6 +3220,13 @@ static void help_dns_classes(void) { VERB_COMMON_HELP_AUTO_HIDDEN("resolvectl"); +COMMAND( + "systemd-resolve\0", + "This command is deprecated. Use resolvectl.1 instead.", + .man_pages = "resolvectl.1\0", + .option_namespace = "systemd-resolve", +); + static int compat_parse_argv(int argc, char *argv[], char ***remaining_args) { int r; diff --git a/test/units/TEST-74-AUX-UTILS.introspect-cli.sh b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh index 1752bdc238..8e8789bd31 100755 --- a/test/units/TEST-74-AUX-UTILS.introspect-cli.sh +++ b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh @@ -32,4 +32,4 @@ systemd-id128 --introspect-cli | jq -e \ # resolvectl is a multicall binary, check that both commands are described resolvectl --introspect-cli | jq -e \ - '[.commands[].names[0]] | sort == ["resolvconf", "resolvectl"]' + '[.commands[].names[0]] | sort == ["resolvconf", "resolvectl", "systemd-resolve"]' From 7670c20c6e38fabdcbdb0753925de5ca4bb003e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 29 Jul 2026 18:49:09 +0200 Subject: [PATCH 24/26] verbs: add PagerFlags pointer to all CommandDescription's Before this series, many help() functions opened the pager. This got lost in the translation. Starting the pager requires access to arg_pager_flags. While we could pipe this through in case when command_print_help() is called directly, but this hard in the case when verb_help_auto() or VERB_COMMON_HELP_AUTO/VERB_COMMON_HELP_AUTO_HIDDEN are used. Let's instead extend the (const static) CommandDescription structure with a pointer to the arg_pager_flags variable. This restores the pager functionality in a uniform fashion with a one-line change in the individual binaries. --- src/analyze/analyze.c | 1 + src/dissect/dissect.c | 1 + src/home/homectl.c | 1 + src/id128/id128.c | 1 + src/resolve/resolvectl.c | 2 ++ src/shared/verbs.c | 4 ++++ src/shared/verbs.h | 1 + src/timedate/timedatectl.c | 1 + src/varlinkctl/varlinkctl.c | 1 + 9 files changed, 13 insertions(+) diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index 271f74ef6f..38c17606d7 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -200,6 +200,7 @@ COMMAND( "systemd-analyze\0", .abstract = "Profile systemd, show unit dependencies, check unit files.", .man_pages = "systemd-analyze.1\0", + .pager_flags = &arg_pager_flags, ); VERB_COMMON_HELP_AUTO_HIDDEN("systemd-analyze"); diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 00a38b9367..ed0182950d 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -141,6 +141,7 @@ COMMAND( "--shift IMAGE UIDBASE\0", .man_pages = "systemd-dissect.1\0", .option_namespace = "systemd-dissect", + .pager_flags = &arg_pager_flags, ); static int parse_image_path_argument(const char *path, char **ret_root, char **ret_image) { diff --git a/src/home/homectl.c b/src/home/homectl.c index b2f2882c72..e5c2857134 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -183,6 +183,7 @@ COMMAND( "homectl\0", "Create, manipulate or inspect home directories.", .man_pages = "homectl.1\0", + .pager_flags = &arg_pager_flags, .flags = COMMAND_HELP_SEPARATE, /* the option table is very wide */ ); diff --git a/src/id128/id128.c b/src/id128/id128.c index a65ba3b66d..54d41b24c9 100644 --- a/src/id128/id128.c +++ b/src/id128/id128.c @@ -29,6 +29,7 @@ COMMAND( "systemd-id128\0", "Generate and print 128-bit identifiers.", .man_pages = "systemd-id128.1\0", + .pager_flags = &arg_pager_flags, ); static int print_id(sd_id128_t id) { diff --git a/src/resolve/resolvectl.c b/src/resolve/resolvectl.c index c774d93211..4336c4f90b 100644 --- a/src/resolve/resolvectl.c +++ b/src/resolve/resolvectl.c @@ -101,6 +101,7 @@ COMMAND( "resolve domain names, IPv4 and IPv6 addresses, DNS records, and services.", .man_pages = "resolvectl.1\0", .option_namespace = "resolvectl", + .pager_flags = &arg_pager_flags, ); typedef enum StatusMode { @@ -3225,6 +3226,7 @@ COMMAND( "This command is deprecated. Use resolvectl.1 instead.", .man_pages = "resolvectl.1\0", .option_namespace = "systemd-resolve", + .pager_flags = &arg_pager_flags, ); static int compat_parse_argv(int argc, char *argv[], char ***remaining_args) { diff --git a/src/shared/verbs.c b/src/shared/verbs.c index d6804a9154..ec8bfaf518 100644 --- a/src/shared/verbs.c +++ b/src/shared/verbs.c @@ -9,6 +9,7 @@ #include "log.h" #include "nulstr-util.h" #include "options.h" +#include "pager.h" #include "pretty-print.h" #include "string-util.h" #include "strv.h" @@ -444,6 +445,9 @@ int _command_print_help( const CommandDescription *cmd = (const CommandDescription*) ASSERT_PTR(cmdverb->data); + if (cmd->pager_flags) + pager_open(*cmd->pager_flags); + if (cmd->argspec) NULSTR_FOREACH(spec, cmd->argspec) { _cleanup_free_ char *line = strjoin("[OPTION…] ", spec); diff --git a/src/shared/verbs.h b/src/shared/verbs.h index 7ae669dc8a..cbe5b4e9cb 100644 --- a/src/shared/verbs.h +++ b/src/shared/verbs.h @@ -17,6 +17,7 @@ typedef struct CommandDescription { const char *footer; /* optional footer to print right above man page links */ const char *man_pages; /* nulstr with man page names */ const char *option_namespace; /* to be used when the options are in a namespace */ + const PagerFlags *pager_flags; /* optional pointer to the runtime pager flags variable */ CommandFlags flags; } CommandDescription; diff --git a/src/timedate/timedatectl.c b/src/timedate/timedatectl.c index 06595792bd..9926748cbc 100644 --- a/src/timedate/timedatectl.c +++ b/src/timedate/timedatectl.c @@ -183,6 +183,7 @@ COMMAND( "timedatectl\0", .abstract = "Query or change system time and date settings.", .man_pages = "timedatectl.1\0", + .pager_flags = &arg_pager_flags, ); VERB_DEFAULT_NOARG(verb_status, "status", "Show current time settings"); diff --git a/src/varlinkctl/varlinkctl.c b/src/varlinkctl/varlinkctl.c index dafda83152..2a1c9a1ce2 100644 --- a/src/varlinkctl/varlinkctl.c +++ b/src/varlinkctl/varlinkctl.c @@ -292,6 +292,7 @@ COMMAND( .man_pages = "varlinkctl.1\0", .footer = "With --exec, specify the command to invoke:\n" " varlinkctl --exec call ADDRESS METHOD PARAMS -- CMDLINE…", + .pager_flags = &arg_pager_flags, ); VERB(verb_info, "info", "ADDRESS", 2, 2, 0, "Show service information"); From 7ea090f41b389cb9448b4f1f81fe866a3cd27cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 13 Aug 2026 13:22:24 +0200 Subject: [PATCH 25/26] verbs: wrap the help footer at print time The footer is now wrapped to min($COLUMNS, 80) when it is printed, so CommandDescription footers do not need to be hard-wrapped in the source, and make use of the available terminal width. Newlines embedded in the text are preserved and each line is wrapped independently, so explicit line breaks can still be used where needed, e.g. before the example command line in varlinkctl. The text is wrapped at max of 80 columns to avoid very wrong lines on wide terminals. Such lines are hard to read. A new helper is added for this. We already have format_strv_width in format-table, but it requires a strv, so we'd need to conver the data, and returns a formatted string, which we'd need to print ourselves. So overall, it seems better to add a dedicated helper. --- src/resolve/resolvconf-compat.c | 12 +++++------ src/shared/verbs.c | 37 +++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/resolve/resolvconf-compat.c b/src/resolve/resolvconf-compat.c index 51e226da84..8e4835b4f8 100644 --- a/src/resolve/resolvconf-compat.c +++ b/src/resolve/resolvconf-compat.c @@ -28,12 +28,12 @@ COMMAND( .argspec = "-a INTERFACE footer) - printf("\n%s\n", cmd->footer); + r = print_wrapped(cmd->footer); + if (r < 0) + return r; r = print_man_links(cmd->man_pages); if (r < 0) From 94af9a8b4065edf47e80dcd78c2ce13cd1ef2e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 13 Aug 2026 13:52:35 +0200 Subject: [PATCH 26/26] verbs: wrap abstract too So far there's only one multi-line abstract. But as pointed out in the review, it's silly to handle footer but not the abstract. help_abstract() is not reused because it only works with one-line abstracts. (It always prints an empty line to separate paragraphs.) So the formatting is done anew in print_wrapped(). I think this is OK, since it's all very simple. After everything has been converted to COMMAND() and its helpers, help_abstract() and the sister functions will become unused and can be dropped then. --- src/resolve/resolvectl.c | 2 +- src/shared/verbs.c | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/resolve/resolvectl.c b/src/resolve/resolvectl.c index 4336c4f90b..db69e1bbdf 100644 --- a/src/resolve/resolvectl.c +++ b/src/resolve/resolvectl.c @@ -97,7 +97,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_set_nta, strv_freep); COMMAND( "resolvectl\0", - "Send control commands to the network name resolution manager, or\n" + "Send control commands to the network name resolution manager, or " "resolve domain names, IPv4 and IPv6 addresses, DNS records, and services.", .man_pages = "resolvectl.1\0", .option_namespace = "resolvectl", diff --git a/src/shared/verbs.c b/src/shared/verbs.c index 66dc3e4994..0b27f4850c 100644 --- a/src/shared/verbs.c +++ b/src/shared/verbs.c @@ -431,7 +431,7 @@ static bool verbs_array_has_verbs(const Verb verbs[], const Verb verbs_end[]) { return false; } -static int print_wrapped(const char *text) { +static int print_wrapped(const char *text, bool abstract) { /* Print the text wrapped at spaces to the specified width. Newlines embedded in the text * are preserved and each line is wrapped independently, so explicit line breaks (e.g. @@ -457,7 +457,14 @@ static int print_wrapped(const char *text) { putchar('\n'); STRV_FOREACH(line, lines2) - puts(*line); + if (abstract) + printf("%s%s%s%s\n", + ansi_highlight(), + ansi_add_italics(), + *line, + ansi_normal()); + else + puts(*line); return 0; } @@ -492,14 +499,15 @@ int _command_print_help( help_cmdline(have_verbs ? "[OPTION…] COMMAND …" : "[OPTION…]"); } - if (cmd->abstract) - help_abstract(cmd->abstract); + r = print_wrapped(cmd->abstract, /* abstract= */ true); + if (r < 0) + return r; r = print_verb_option_help(cmd, cmdverb + 1, verbs_end, options, options_end); if (r < 0) return log_error_errno(r, "Failed to print verb&option help: %m"); - r = print_wrapped(cmd->footer); + r = print_wrapped(cmd->footer, /* abstract= */ false); if (r < 0) return r;