From 3694241e3fa2ae1865cc78f6cb0c23f55ac93798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 25 Aug 2026 14:24:04 +0200 Subject: [PATCH 01/10] shared/verbs: add helper to call command_print_help_full with name only I didn't do this previously because I thought that there wouldn't be too many callers. But it turns out that there're quite a few. --- src/kernel-install/kernel-install.c | 2 +- src/mount/mount-tool.c | 2 +- src/resolve/resolvconf-compat.c | 2 +- src/resolve/resolvectl.c | 2 +- src/run/run.c | 4 ++-- src/shared/verbs.h | 5 +++-- src/sysext/sysext.c | 9 +++------ src/systemctl/systemctl.c | 2 +- 8 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/kernel-install/kernel-install.c b/src/kernel-install/kernel-install.c index d35ff931b2..943640bd41 100644 --- a/src/kernel-install/kernel-install.c +++ b/src/kernel-install/kernel-install.c @@ -1624,7 +1624,7 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { switch (c) { OPTION_COMMON_HELP: - return command_print_help_full("kernel-install", /* footer_ansi_seq= */ NULL); + return command_print_help_name("kernel-install"); OPTION_COMMON_VERSION: return version(); diff --git a/src/mount/mount-tool.c b/src/mount/mount-tool.c index 5ecd7c097a..5d989188c8 100644 --- a/src/mount/mount-tool.c +++ b/src/mount/mount-tool.c @@ -163,7 +163,7 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { OPTION_GROUP("Common options"): {} OPTION_COMMON_HELP: - return command_print_help_full(cmd->names, /* footer_ansi_seq= */ NULL); + return command_print_help_name(cmd->names); OPTION_COMMON_VERSION: return version(); diff --git a/src/resolve/resolvconf-compat.c b/src/resolve/resolvconf-compat.c index 8a07124573..018908bcc0 100644 --- a/src/resolve/resolvconf-compat.c +++ b/src/resolve/resolvconf-compat.c @@ -181,7 +181,7 @@ int resolvconf_parse_argv(int argc, char *argv[]) { OPTION_NAMESPACE("resolvconf"): {} OPTION_COMMON_HELP: - return command_print_help_full("resolvconf", /* footer_ansi_seq= */ NULL); + return command_print_help_name("resolvconf"); OPTION_COMMON_VERSION: return version(); diff --git a/src/resolve/resolvectl.c b/src/resolve/resolvectl.c index f2d5946ae6..c4f2c5a2d5 100644 --- a/src/resolve/resolvectl.c +++ b/src/resolve/resolvectl.c @@ -3498,7 +3498,7 @@ static int native_parse_argv(int argc, char *argv[], char ***remaining_args) { OPTION_NAMESPACE("resolvectl"): {} OPTION_COMMON_HELP: - return command_print_help_full("resolvectl", /* footer_ansi_seq= */ NULL); + return command_print_help_name("resolvectl"); OPTION_COMMON_VERSION: return version(); diff --git a/src/run/run.c b/src/run/run.c index 6aa58f8b5c..cdf15fb41f 100644 --- a/src/run/run.c +++ b/src/run/run.c @@ -208,7 +208,7 @@ static int parse_argv(int argc, char *argv[]) { OPTION_NAMESPACE("systemd-run"): {} OPTION_COMMON_HELP: - return command_print_help_full("systemd-run", /* footer_ansi_seq= */ NULL); + return command_print_help_name("systemd-run"); OPTION_COMMON_VERSION: return version(); @@ -784,7 +784,7 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) { OPTION_NAMESPACE("run0"): {} OPTION_COMMON_HELP: - return command_print_help_full("run0", /* footer_ansi_seq= */ NULL); + return command_print_help_name("run0"); OPTION('V', "version", NULL, "Show package version"): return version(); diff --git a/src/shared/verbs.h b/src/shared/verbs.h index 673ef5870b..72b6d70f4f 100644 --- a/src/shared/verbs.h +++ b/src/shared/verbs.h @@ -162,10 +162,11 @@ int _command_print_help_full( __start_SYSTEMD_VERBS, __stop_SYSTEMD_VERBS, \ __start_SYSTEMD_OPTIONS, __stop_SYSTEMD_OPTIONS, \ name, footer_ansi_seq) -#define command_print_help() command_print_help_full(/* name= */ NULL, /* footer_ansi_seq= */ NULL) +#define command_print_help_name(name) command_print_help_full(name, /* footer_ansi_seq= */ NULL) +#define command_print_help() command_print_help_name(/* name= */ NULL) static inline int verb_help_auto(int argc, char **argv, uintptr_t data, void *userdata) { - return command_print_help_full((const char*) data, /* footer_ansi_seq= */ NULL); + return command_print_help_name((const char*) data); } /* Print a machine-readable description of the program's commands, verbs, and options in the format diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c index b0e668a38a..7722c458cb 100644 --- a/src/sysext/sysext.c +++ b/src/sysext/sysext.c @@ -3138,9 +3138,8 @@ static int vl_method_list(sd_varlink *link, sd_json_variant *parameters, sd_varl VERB_FULL(verb_help, "help", NULL, VERB_ANY, VERB_ANY, 0, /* dat= */ 0u, /* help= */ NULL); static int verb_help(int argc, char **argv, uintptr_t data, void *userdata) { - return command_print_help_full( - arg_image_class == IMAGE_SYSEXT ? "systemd-sysext" : "systemd-confext", - /* footer_ansi_seq= */ NULL); + return command_print_help_name( + arg_image_class == IMAGE_SYSEXT ? "systemd-sysext" : "systemd-confext"); } static int parse_argv(int argc, char *argv[], char ***ret_args) { @@ -3156,9 +3155,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { switch (c) { OPTION_COMMON_HELP: - return command_print_help_full( - arg_image_class == IMAGE_SYSEXT ? "systemd-sysext" : "systemd-confext", - /* footer_ansi_seq= */ NULL); + return verb_help(argc, argv, /* data= */ 0u, /* userdata= */ NULL); OPTION_COMMON_VERSION: return version(); diff --git a/src/systemctl/systemctl.c b/src/systemctl/systemctl.c index 6a36afc6d3..63fdfbc064 100644 --- a/src/systemctl/systemctl.c +++ b/src/systemctl/systemctl.c @@ -351,7 +351,7 @@ static int systemctl_parse_argv(int argc, char *argv[], int log_level_shift, cha OPTION_NAMESPACE("systemctl"): {} OPTION_COMMON_HELP: - return command_print_help_full("systemctl", /* footer_ansi_seq= */ NULL); + return command_print_help_name("systemctl"); OPTION_COMMON_VERSION: return version(); From 477d08b3a97fa5b18b61da61d300a81ea9adb3b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 25 Aug 2026 11:00:37 +0200 Subject: [PATCH 02/10] integritysetup: convert to VERB_COMMAND and add --introspect-cli MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standard parse_argv() func is added. --version is now accepted (previously only the help forms were recognized, via argv_looks_like_help()). The synopsis changes from "systemd-integritysetup COMMAND ..." to the standard "systemd-integritysetup [OPTION…] COMMAND …", and the help output gains an Options section. "help" as the first argument keeps working via the hidden common help verb. Previously, we'd just print help and exit with success if no arguments were given. This seems unexpected and wrong. The program now prints the usual error message: Command verb required (one of attach, detach, help). --- src/integritysetup/integritysetup.c | 70 +++++++++++-------- .../units/TEST-74-AUX-UTILS.introspect-cli.sh | 1 + 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/integritysetup/integritysetup.c b/src/integritysetup/integritysetup.c index bf2f40b0d4..461be60905 100644 --- a/src/integritysetup/integritysetup.c +++ b/src/integritysetup/integritysetup.c @@ -1,22 +1,20 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include +#include "sd-json.h" + #include "alloc-util.h" -#include "argv-util.h" +#include "build.h" #include "cryptsetup-util.h" #include "dlopen-note.h" #include "fileio.h" -#include "format-table.h" -#include "help-util.h" #include "integrity-util.h" #include "log.h" #include "main-func.h" #include "path-util.h" #include "string-table.h" #include "string-util.h" -#include "strv.h" #include "time-util.h" #include "verbs.h" @@ -43,25 +41,11 @@ static const char* const dm_integrity_algorithm_table[_INTEGRITY_ALGORITHM_MAX] DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(dm_integrity_algorithm, IntegrityAlgorithm); -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 an integrity protected block device."); - - help_section("Commands"); - r = table_print_or_warn(verbs); - if (r < 0) - return r; - - help_man_page_reference("systemd-integritysetup@.service", "8"); - return 0; -} +COMMAND( + "systemd-integritysetup\0", + "Attach or detach an integrity protected block device.", + .man_pages = "systemd-integritysetup@.service.8\0", +); static int load_key_file( const char *key_file, @@ -197,23 +181,51 @@ static int verb_detach(int argc, char *argv[], uintptr_t _data, void *userdata) return 0; } +VERB_COMMON_HELP_AUTO_HIDDEN(); + +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 command_print_help(); + + OPTION_COMMON_VERSION: + return version(); + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(SD_JSON_FORMAT_OFF); + } + + *ret_args = option_parser_get_args(&opts); + return 1; +} + static int run(int argc, char *argv[]) { int r; - LIBCRYPTSETUP_NOTE(required); - - 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(); + LIBCRYPTSETUP_NOTE(required); + r = dlopen_cryptsetup(LOG_ERR); if (r < 0) return r; umask(0022); - return dispatch_verb(strv_skip(argv, 1), /* userdata= */ NULL); + return dispatch_verb(args, /* userdata= */ NULL); } DEFINE_MAIN_FUNCTION(run); diff --git a/test/units/TEST-74-AUX-UTILS.introspect-cli.sh b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh index ce9124696b..8cc37d3559 100755 --- a/test/units/TEST-74-AUX-UTILS.introspect-cli.sh +++ b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh @@ -73,6 +73,7 @@ INTROSPECTABLE=( systemd-import systemd-import-fs systemd-inhibit + systemd-integritysetup systemd-journal-gatewayd systemd-journal-remote systemd-journal-upload From 9d2178b4a8e3243a1240a112d36ff1d2dc75946c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 25 Aug 2026 12:46:02 +0200 Subject: [PATCH 03/10] integritysetup: mark as public in meson This means that the program is tested for --help and --version, but doesn't mean that it's more or less supported. --- src/integritysetup/meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/src/integritysetup/meson.build b/src/integritysetup/meson.build index 7f29c031b8..b8d23eeafa 100644 --- a/src/integritysetup/meson.build +++ b/src/integritysetup/meson.build @@ -7,6 +7,7 @@ endif executables += [ libexec_template + { 'name' : 'systemd-integritysetup', + 'public' : true, 'sources' : files('integritysetup.c'), 'export' : files('integrity-util.c'), 'dependencies' : libcryptsetup_cflags, From ed595e6b6761d73435d887de35c95cc74b4fd6af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 25 Aug 2026 12:46:17 +0200 Subject: [PATCH 04/10] basic/argv-util: drop unused code --- src/basic/argv-util.c | 27 --------------------------- src/basic/argv-util.h | 1 - src/test/test-argv-util.c | 13 ------------- 3 files changed, 41 deletions(-) diff --git a/src/basic/argv-util.c b/src/basic/argv-util.c index 20587f66ad..b0381a6096 100644 --- a/src/basic/argv-util.c +++ b/src/basic/argv-util.c @@ -13,7 +13,6 @@ #include "path-util.h" #include "process-util.h" #include "string-util.h" -#include "strv.h" int saved_argc = 0; char **saved_argv = NULL; @@ -69,32 +68,6 @@ bool invoked_by_systemd(void) { return cached; } -bool argv_looks_like_help(int argc, char **argv) { - char **l; - - /* Scans the command line for indications the user asks for help. This is supposed to be called by - * tools that do not implement getopt()-style command line parsing because they are not primarily - * user-facing. Detects four ways of asking for help: - * - * 1. Passing zero arguments - * 2. Passing "help" as first argument - * 3. Passing --help as any argument - * 4. Passing -h as any argument - */ - assert(argc == 0 || argv); - - if (argc <= 1) - return true; - - if (streq_ptr(argv[1], "help")) - return true; - - l = strv_skip(argv, 1); - - return strv_contains(l, "--help") || - strv_contains(l, "-h"); -} - static int update_argv(const char name[], size_t l) { static int can_do = -1; int r; diff --git a/src/basic/argv-util.h b/src/basic/argv-util.h index a9526ef936..9d598929e7 100644 --- a/src/basic/argv-util.h +++ b/src/basic/argv-util.h @@ -10,7 +10,6 @@ void save_argc_argv(int argc, char **argv); bool invoked_as(char *argv[], const char *token); bool invoked_by_systemd(void) _const_; -bool argv_looks_like_help(int argc, char **argv); int rename_process_full(const char *comm, const char *invocation); static inline int rename_process(const char *name) { diff --git a/src/test/test-argv-util.c b/src/test/test-argv-util.c index df844db30a..dfe813a1eb 100644 --- a/src/test/test-argv-util.c +++ b/src/test/test-argv-util.c @@ -99,19 +99,6 @@ TEST(rename_process) { test_rename_process_one("1234567", 1); /* should always fit */ } -TEST(argv_help) { - assert_se(argv_looks_like_help(1, STRV_MAKE("program"))); - assert_se(argv_looks_like_help(2, STRV_MAKE("program", "help"))); - assert_se(argv_looks_like_help(3, STRV_MAKE("program", "arg1", "--help"))); - assert_se(argv_looks_like_help(4, STRV_MAKE("program", "arg1", "arg2", "-h"))); - assert_se(!argv_looks_like_help(2, STRV_MAKE("program", "arg1"))); - assert_se(!argv_looks_like_help(4, STRV_MAKE("program", "arg1", "arg2", "--h"))); - assert_se(!argv_looks_like_help(3, STRV_MAKE("program", "Help", "arg2"))); - assert_se(argv_looks_like_help(5, STRV_MAKE("program", "--help", "arg1", "-h", "--help"))); - assert_se(!argv_looks_like_help(4, STRV_MAKE("program","arg1", "arg2", "-H"))); - assert_se(!argv_looks_like_help(3, STRV_MAKE("program", "--Help", "arg2"))); -} - static int intro(void) { log_show_color(true); return EXIT_SUCCESS; From 65510d4cbad88e4c0c39f22510cedce5a4ddf030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 25 Aug 2026 11:07:11 +0200 Subject: [PATCH 05/10] services: convert to VERB_COMMAND and add --introspect-cli MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This converts systemd-homed, -hostnamed, -importd, -localed, -logind, -machined, -networkd, -portebled, -resolved, -sysupdated, -timedated, and -timesyncd. The service name and description move from service_parse_argv() arguments into a COMMAND() description that each daemon now defines. The service_parse_argv() macro captures the calling binary's verb section, where that COMMAND is found, while the options are still defined and parsed in libshared, in the new "service" option namespace that each COMMAND references. (The named namespace makes sure that options defined by other translation units in libshared can never leak into the services' parsers.) The supported features are declared via option group filtering: each COMMAND lists "Options" plus "Bus introspection" and/or "Runtime scope". Unsupported options are now rejected by the parser as unknown options (and are not shown in --help), replacing the runtime "This service does not support …" errors. The help output changes slightly: the "Bus introspection" and "Runtime scope" option groups now get their own headings, and some descriptions were reworded to the imperative form used elsewhere. --- src/home/homed.c | 15 ++- src/hostname/hostnamed.c | 15 ++- src/import/importd.c | 16 ++- src/locale/localed.c | 15 ++- src/login/logind.c | 15 ++- src/machine/machined.c | 16 ++- src/network/networkd.c | 15 ++- src/portable/portabled.c | 16 ++- src/resolve/resolved.c | 15 ++- src/shared/service-util.c | 102 +++++++----------- src/shared/service-util.h | 17 ++- src/sysupdate/sysupdated.c | 15 ++- src/timedate/timedated.c | 15 ++- src/timesync/timesyncd.c | 15 ++- .../units/TEST-74-AUX-UTILS.introspect-cli.sh | 20 ++++ 15 files changed, 220 insertions(+), 102 deletions(-) diff --git a/src/home/homed.c b/src/home/homed.c index f36e61d04e..a1b0d991de 100644 --- a/src/home/homed.c +++ b/src/home/homed.c @@ -13,6 +13,17 @@ #include "log.h" #include "main-func.h" #include "service-util.h" +#include "verbs.h" + +COMMAND( + "systemd-homed\0", + "Create, remove, change or inspect home areas.", + .man_pages = "systemd-homed.service.8\0", + .option_namespace = "service", + .option_groups = + "Options\0" + "Bus introspection\0", +); static int run(int argc, char *argv[]) { _cleanup_(manager_freep) Manager *m = NULL; @@ -25,9 +36,7 @@ static int run(int argc, char *argv[]) { log_setup(); - r = service_parse_argv("systemd-homed.service", - "A service to create, remove, change or inspect home areas.", - BUS_IMPLEMENTATIONS(&manager_object, + r = service_parse_argv(BUS_IMPLEMENTATIONS(&manager_object, &log_control_object), /* runtime_scope= */ NULL, argc, argv); diff --git a/src/hostname/hostnamed.c b/src/hostname/hostnamed.c index 98ade098a7..ffb98fab05 100644 --- a/src/hostname/hostnamed.c +++ b/src/hostname/hostnamed.c @@ -46,6 +46,7 @@ #include "varlink-io.systemd.Hostname.h" #include "varlink-io.systemd.service.h" #include "varlink-util.h" +#include "verbs.h" #include "virt.h" #include "vsock-util.h" @@ -2654,6 +2655,16 @@ static bool context_check_idle(void *userdata) { hashmap_isempty(c->polkit_registry); } +COMMAND( + "systemd-hostnamed\0", + "Manage the system hostname and related metadata.", + .man_pages = "systemd-hostnamed.service.8\0", + .option_namespace = "service", + .option_groups = + "Options\0" + "Bus introspection\0", +); + static int run(int argc, char *argv[]) { _cleanup_(context_destroy) Context context = { .hostname_source = _HOSTNAME_INVALID, /* appropriate value will be set later */ @@ -2664,9 +2675,7 @@ static int run(int argc, char *argv[]) { log_setup(); - r = service_parse_argv("systemd-hostnamed.service", - "Manage the system hostname and related metadata.", - BUS_IMPLEMENTATIONS(&manager_object, + r = service_parse_argv(BUS_IMPLEMENTATIONS(&manager_object, &log_control_object), /* runtime_scope= */ NULL, argc, argv); diff --git a/src/import/importd.c b/src/import/importd.c index 3a1ad8b502..1f72d6b5fe 100644 --- a/src/import/importd.c +++ b/src/import/importd.c @@ -48,6 +48,7 @@ #include "varlink-io.systemd.Import.h" #include "varlink-io.systemd.service.h" #include "varlink-util.h" +#include "verbs.h" #include "web-util.h" typedef struct Manager Manager; @@ -2079,6 +2080,17 @@ static void manager_parse_env(Manager *m) { log_warning_errno(r, "Failed to parse $SYSTEMD_IMPORT_BTRFS_QUOTA: %m"); } +COMMAND( + "systemd-importd\0", + "Import and export VM and container images.", + .man_pages = "systemd-importd.service.8\0", + .option_namespace = "service", + .option_groups = + "Options\0" + "Bus introspection\0" + "Runtime scope\0", +); + static int run(int argc, char *argv[]) { _cleanup_(manager_unrefp) Manager *m = NULL; RuntimeScope scope = RUNTIME_SCOPE_SYSTEM; @@ -2088,9 +2100,7 @@ static int run(int argc, char *argv[]) { log_setup(); - r = service_parse_argv("systemd-importd.service", - "VM and container image import and export service.", - BUS_IMPLEMENTATIONS(&manager_object, + r = service_parse_argv(BUS_IMPLEMENTATIONS(&manager_object, &log_control_object), &scope, argc, argv); diff --git a/src/locale/localed.c b/src/locale/localed.c index a6ff82ffbc..a4c01a3d41 100644 --- a/src/locale/localed.c +++ b/src/locale/localed.c @@ -24,6 +24,7 @@ #include "service-util.h" #include "string-util.h" #include "strv.h" +#include "verbs.h" static int vconsole_reload(sd_bus *bus) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; @@ -627,6 +628,16 @@ static bool context_check_idle(void *userdata) { return hashmap_isempty(c->polkit_registry); } +COMMAND( + "systemd-localed\0", + "Manage system locale settings and key mappings.", + .man_pages = "systemd-localed.service.8\0", + .option_namespace = "service", + .option_groups = + "Options\0" + "Bus introspection\0", +); + static int run(int argc, char *argv[]) { _cleanup_(context_clear) Context context = {}; _cleanup_(sd_event_unrefp) sd_event *event = NULL; @@ -637,9 +648,7 @@ static int run(int argc, char *argv[]) { log_setup(); - r = service_parse_argv("systemd-localed.service", - "Manage system locale settings and key mappings.", - BUS_IMPLEMENTATIONS(&manager_object, + r = service_parse_argv(BUS_IMPLEMENTATIONS(&manager_object, &log_control_object), /* runtime_scope= */ NULL, argc, argv); diff --git a/src/login/logind.c b/src/login/logind.c index 9fcf57ab91..3552ed2eed 100644 --- a/src/login/logind.c +++ b/src/login/logind.c @@ -46,6 +46,7 @@ #include "terminal-util.h" #include "udev-util.h" #include "user-util.h" +#include "verbs.h" static Manager* manager_free(Manager *m); DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); @@ -1347,6 +1348,16 @@ static int manager_run(Manager *m) { } } +COMMAND( + "systemd-logind\0", + "Manage user logins and devices and privileged operations.", + .man_pages = "systemd-logind.service.8\0", + .option_namespace = "service", + .option_groups = + "Options\0" + "Bus introspection\0", +); + static int run(int argc, char *argv[]) { _cleanup_(manager_freep) Manager *m = NULL; _unused_ _cleanup_(notify_on_cleanup) const char *notify_message = NULL; @@ -1359,9 +1370,7 @@ static int run(int argc, char *argv[]) { log_set_facility(LOG_AUTH); log_setup(); - r = service_parse_argv("systemd-logind.service", - "Manager for user logins and devices and privileged operations.", - BUS_IMPLEMENTATIONS(&manager_object, + r = service_parse_argv(BUS_IMPLEMENTATIONS(&manager_object, &log_control_object), /* runtime_scope= */ NULL, argc, argv); diff --git a/src/machine/machined.c b/src/machine/machined.c index 47b7650caf..58edc628a6 100644 --- a/src/machine/machined.c +++ b/src/machine/machined.c @@ -35,6 +35,7 @@ #include "signal-util.h" #include "special.h" #include "string-util.h" +#include "verbs.h" #include "vsock-util.h" static Manager* manager_unref(Manager *m); @@ -343,6 +344,17 @@ static bool check_idle(void *userdata) { return hashmap_isempty(m->machines); } +COMMAND( + "systemd-machined\0", + "Manage registrations of local VMs and containers.", + .man_pages = "systemd-machined.service.8\0", + .option_namespace = "service", + .option_groups = + "Options\0" + "Bus introspection\0" + "Runtime scope\0", +); + static int run(int argc, char *argv[]) { _cleanup_(manager_unrefp) Manager *m = NULL; RuntimeScope scope = RUNTIME_SCOPE_SYSTEM; @@ -357,9 +369,7 @@ static int run(int argc, char *argv[]) { log_set_facility(LOG_AUTH); log_setup(); - r = service_parse_argv("systemd-machined.service", - "Manage registrations of local VMs and containers.", - BUS_IMPLEMENTATIONS(&manager_object, + r = service_parse_argv(BUS_IMPLEMENTATIONS(&manager_object, &log_control_object), &scope, argc, argv); diff --git a/src/network/networkd.c b/src/network/networkd.c index cbdf3896b2..e6b2d3e227 100644 --- a/src/network/networkd.c +++ b/src/network/networkd.c @@ -19,6 +19,17 @@ #include "service-util.h" #include "strv.h" #include "user-util.h" +#include "verbs.h" + +COMMAND( + "systemd-networkd\0", + "Manage and configure network devices, create virtual network devices.", + .man_pages = "systemd-networkd.service.8\0", + .option_namespace = "service", + .option_groups = + "Options\0" + "Bus introspection\0", +); static int run(int argc, char *argv[]) { _cleanup_(manager_freep) Manager *m = NULL; @@ -31,9 +42,7 @@ static int run(int argc, char *argv[]) { log_setup(); - r = service_parse_argv("systemd-networkd.service", - "Manage and configure network devices, create virtual network devices", - BUS_IMPLEMENTATIONS(&manager_object, &log_control_object), + r = service_parse_argv(BUS_IMPLEMENTATIONS(&manager_object, &log_control_object), /* runtime_scope= */ NULL, argc, argv); if (r <= 0) diff --git a/src/portable/portabled.c b/src/portable/portabled.c index b79491e2d8..09197aa651 100644 --- a/src/portable/portabled.c +++ b/src/portable/portabled.c @@ -20,6 +20,7 @@ #include "portabled.h" #include "service-util.h" #include "signal-util.h" +#include "verbs.h" static Manager* manager_unref(Manager *m); DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_unref); @@ -139,6 +140,17 @@ static bool check_idle(void *userdata) { hashmap_isempty(m->polkit_registry); } +COMMAND( + "systemd-portabled\0", + "Manage registrations of portable images.", + .man_pages = "systemd-portabled.service.8\0", + .option_namespace = "service", + .option_groups = + "Options\0" + "Bus introspection\0" + "Runtime scope\0", +); + static int run(int argc, char *argv[]) { _cleanup_(manager_unrefp) Manager *m = NULL; RuntimeScope scope = RUNTIME_SCOPE_SYSTEM; @@ -152,9 +164,7 @@ static int run(int argc, char *argv[]) { log_setup(); - r = service_parse_argv("systemd-portabled.service", - "Manage registrations of portable images.", - BUS_IMPLEMENTATIONS(&manager_object, + r = service_parse_argv(BUS_IMPLEMENTATIONS(&manager_object, &log_control_object), &scope, argc, argv); diff --git a/src/resolve/resolved.c b/src/resolve/resolved.c index 0b6e3e983d..c2d3248a34 100644 --- a/src/resolve/resolved.c +++ b/src/resolve/resolved.c @@ -19,6 +19,17 @@ #include "resolved-resolv-conf.h" #include "service-util.h" #include "user-util.h" +#include "verbs.h" + +COMMAND( + "systemd-resolved\0", + "Provide name resolution with caching using DNS, mDNS, LLMNR.", + .man_pages = "systemd-resolved.service.8\0", + .option_namespace = "service", + .option_groups = + "Options\0" + "Bus introspection\0", +); static int run(int argc, char *argv[]) { _cleanup_(manager_freep) Manager *m = NULL; @@ -32,9 +43,7 @@ static int run(int argc, char *argv[]) { log_setup(); - r = service_parse_argv("systemd-resolved.service", - "Provide name resolution with caching using DNS, mDNS, LLMNR.", - BUS_IMPLEMENTATIONS(&manager_object, + r = service_parse_argv(BUS_IMPLEMENTATIONS(&manager_object, &log_control_object), /* runtime_scope= */ NULL, argc, argv); diff --git a/src/shared/service-util.c b/src/shared/service-util.c index 7c1df8e73a..9726bbf701 100644 --- a/src/shared/service-util.c +++ b/src/shared/service-util.c @@ -1,59 +1,19 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include +#include "sd-json.h" #include "build.h" #include "bus-object.h" -#include "format-table.h" -#include "help-util.h" #include "log.h" +#include "nulstr-util.h" #include "options.h" #include "runtime-scope.h" #include "service-util.h" +#include "string-util.h" -static int help(const char *service, - const char *description, - bool with_bus_introspect, - bool with_runtime_scope) { - - static const char* const groups[] = { - NULL, - "Bus introspection", - "Runtime scope", - }; - - bool conds[ELEMENTSOF(groups)] = { true, with_bus_introspect, with_runtime_scope }; - Table* tables[ELEMENTSOF(groups)] = {}; - CLEANUP_ELEMENTS(tables, table_unref_array_clear); - int r; - - for (size_t i = 0; i < ELEMENTSOF(groups); i++) - if (conds[i]) { - r = option_parser_get_help_table_group(groups[i], &tables[i]); - if (r < 0) - return r; - } - - (void) table_sync_column_widths(0, tables[0], tables[1] ?: tables[2], tables[1] ? tables[2] : NULL); - - help_cmdline("[OPTIONS...]"); - help_abstract(description); - - help_section("Options"); - for (size_t i = 0; i < ELEMENTSOF(groups); i++) - if (conds[i]) { - r = table_print_or_warn(tables[i]); - if (r < 0) - return r; - } - - help_man_page_reference(service, "8"); - return 0; /* No further action */ -} - -int service_parse_argv( - const char *service, - const char *description, +int _service_parse_argv( + const Verb *verbs, + const Verb *verbs_end, const BusObjectImplementation* const* bus_objects, RuntimeScope *runtime_scope, int argc, char *argv[]) { @@ -61,30 +21,50 @@ int service_parse_argv( assert(argc >= 0); assert(argv); - OptionParser opts = { argc, argv }; + const CommandDescription *cmd = NULL; + assert_se(_verbs_find_command(verbs, verbs_end, /* name= */ NULL, &cmd)); + + /* The COMMAND description must reference the option namespace defined below, and list + * exactly the option groups matching the features the service supports. Options outside of + * the listed groups are treated as unknown. */ + assert(streq_ptr(cmd->option_namespace, "service")); + assert(nulstr_contains(cmd->option_groups, "Options")); + assert(!!bus_objects == nulstr_contains(cmd->option_groups, "Bus introspection")); + assert(!!runtime_scope == nulstr_contains(cmd->option_groups, "Runtime scope")); + + OptionParser opts = { + argc, argv, + .namespace = "service", + .option_groups = cmd->option_groups, + }; FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { + OPTION_NAMESPACE("service"): {} + + OPTION_GROUP("Options"): {} + OPTION_COMMON_HELP: - return help(service, - description, - /* with_bus_introspect= */ bus_objects, - /* with_runtime_scope= */ runtime_scope); + return _command_print_help_full( + verbs, verbs_end, + __start_SYSTEMD_OPTIONS, __stop_SYSTEMD_OPTIONS, + cmd->names, + /* footer_ansi_seq= */ NULL); OPTION_COMMON_VERSION: return version(); + OPTION_COMMON_INTROSPECT_CLI: + return _introspect_cli( + verbs, verbs_end, + __start_SYSTEMD_OPTIONS, __stop_SYSTEMD_OPTIONS, + SD_JSON_FORMAT_OFF); + OPTION_GROUP("Bus introspection"): {} OPTION_LONG("bus-introspect", "PATH", "Write D-Bus XML introspection data"): - /* The option is defined in the shared option table, but it's not supported in this binary, - * so we pretend it doesn't exist. */ - if (!bus_objects) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "This service does not support the --bus-introspect= option."); - - return bus_introspect_implementations(stdout, opts.arg, bus_objects); + return bus_introspect_implementations(stdout, opts.arg, ASSERT_PTR(bus_objects)); OPTION_GROUP("Runtime scope"): {} @@ -92,11 +72,7 @@ int service_parse_argv( "Start service in system mode"): {} OPTION_LONG_DATA("user", NULL, /* data= */ RUNTIME_SCOPE_USER, "Start service in user mode"): - if (!runtime_scope) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "This service does not support the --system/--user options."); - - *runtime_scope = opts.opt->data; + *ASSERT_PTR(runtime_scope) = opts.opt->data; break; } diff --git a/src/shared/service-util.h b/src/shared/service-util.h index 462ba2b3fa..e63f8fe9b0 100644 --- a/src/shared/service-util.h +++ b/src/shared/service-util.h @@ -2,10 +2,21 @@ #pragma once #include "forward.h" +#include "verbs.h" -int service_parse_argv( - const char *service, - const char *description, +int _service_parse_argv( + const Verb *verbs, + const Verb *verbs_end, const BusObjectImplementation* const* bus_objects, RuntimeScope *runtime_scope, int argc, char *argv[]); + +/* The service is expected to define a COMMAND() with .option_namespace = "service" and the option + * groups matching the features it supports: "Options" always, "Bus introspection" iff bus_objects + * is passed, "Runtime scope" iff runtime_scope is passed. The macro captures the calling binary's + * verb section, where that COMMAND is found, while the options are defined in + * service_parse_argv_full() itself. */ +#define service_parse_argv(bus_objects, runtime_scope, argc, argv) \ + _service_parse_argv( \ + __start_SYSTEMD_VERBS, __stop_SYSTEMD_VERBS, \ + bus_objects, runtime_scope, argc, argv) diff --git a/src/sysupdate/sysupdated.c b/src/sysupdate/sysupdated.c index d6f77edb81..ef4c1b579a 100644 --- a/src/sysupdate/sysupdated.c +++ b/src/sysupdate/sysupdated.c @@ -41,6 +41,7 @@ #include "strv.h" #include "sysupdate-target.h" #include "sysupdate-util.h" +#include "verbs.h" #define FEATURES_DROPIN_NAME "systemd-sysupdate-enabled" @@ -2160,15 +2161,23 @@ static int manager_run(Manager *m) { m); } +COMMAND( + "systemd-sysupdated\0", + "Manage system updates.", + .man_pages = "systemd-sysupdated.service.8\0", + .option_namespace = "service", + .option_groups = + "Options\0" + "Bus introspection\0", +); + static int run(int argc, char *argv[]) { _cleanup_(manager_freep) Manager *m = NULL; int r; log_setup(); - r = service_parse_argv("systemd-sysupdated.service", - "System update management service.", - BUS_IMPLEMENTATIONS(&manager_object, + r = service_parse_argv(BUS_IMPLEMENTATIONS(&manager_object, &log_control_object), /* runtime_scope= */ NULL, argc, argv); diff --git a/src/timedate/timedated.c b/src/timedate/timedated.c index 25e6ddccdf..667e4262b3 100644 --- a/src/timedate/timedated.c +++ b/src/timedate/timedated.c @@ -46,6 +46,7 @@ #include "time-util.h" #include "unit-def.h" #include "unit-name.h" +#include "verbs.h" #define NULL_ADJTIME_UTC "0.0 0 0\n0\nUTC\n" #define NULL_ADJTIME_LOCAL "0.0 0 0\n0\nLOCAL\n" @@ -1167,6 +1168,16 @@ static bool context_check_idle(void *userdata) { return hashmap_isempty(c->polkit_registry); } +COMMAND( + "systemd-timedated\0", + "Manage the system clock and timezone and NTP enablement.", + .man_pages = "systemd-timedated.service.8\0", + .option_namespace = "service", + .option_groups = + "Options\0" + "Bus introspection\0", +); + static int run(int argc, char *argv[]) { _cleanup_(context_clear) Context context = {}; _cleanup_(sd_event_unrefp) sd_event *event = NULL; @@ -1177,9 +1188,7 @@ static int run(int argc, char *argv[]) { log_setup(); - r = service_parse_argv("systemd-timedated.service", - "Manage the system clock and timezone and NTP enablement.", - BUS_IMPLEMENTATIONS(&manager_object, + r = service_parse_argv(BUS_IMPLEMENTATIONS(&manager_object, &log_control_object), /* runtime_scope= */ NULL, argc, argv); diff --git a/src/timesync/timesyncd.c b/src/timesync/timesyncd.c index 5e0d13023a..b5a4455a64 100644 --- a/src/timesync/timesyncd.c +++ b/src/timesync/timesyncd.c @@ -22,6 +22,7 @@ #include "timesyncd-bus.h" #include "timesyncd-conf.h" #include "timesyncd-manager.h" +#include "verbs.h" static int advance_tstamp(int fd, usec_t epoch) { assert(fd >= 0); @@ -122,6 +123,16 @@ static int load_clock_timestamp(void) { return 0; } +COMMAND( + "systemd-timesyncd\0", + "Synchronize the system clock across the network.", + .man_pages = "systemd-timesyncd.service.8\0", + .option_namespace = "service", + .option_groups = + "Options\0" + "Bus introspection\0", +); + static int run(int argc, char *argv[]) { _cleanup_(manager_freep) Manager *m = NULL; _unused_ _cleanup_(notify_on_cleanup) const char *notify_message = NULL; @@ -130,9 +141,7 @@ static int run(int argc, char *argv[]) { log_set_facility(LOG_CRON); log_setup(); - r = service_parse_argv("systemd-timesyncd.service", - "Network time synchronization", - BUS_IMPLEMENTATIONS(&manager_object, &log_control_object), + r = service_parse_argv(BUS_IMPLEMENTATIONS(&manager_object, &log_control_object), /* runtime_scope= */ NULL, argc, argv); if (r <= 0) diff --git a/test/units/TEST-74-AUX-UTILS.introspect-cli.sh b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh index 8cc37d3559..8d85dc9412 100755 --- a/test/units/TEST-74-AUX-UTILS.introspect-cli.sh +++ b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh @@ -66,25 +66,32 @@ INTROSPECTABLE=( systemd-firstboot systemd-growfs systemd-hibernate-resume + systemd-homed + systemd-hostnamed systemd-hwdb systemd-id128 systemd-imds systemd-imdsd systemd-import systemd-import-fs + systemd-importd systemd-inhibit systemd-integritysetup systemd-journal-gatewayd systemd-journal-remote systemd-journal-upload systemd-keyutil + systemd-localed + systemd-logind systemd-machine-id-setup + systemd-machined systemd-measure systemd-modules-load systemd-mount systemd-mstack systemd-mute-console systemd-network-generator + systemd-networkd systemd-networkd-wait-online systemd-notify systemd-nspawn @@ -92,6 +99,7 @@ INTROSPECTABLE=( systemd-path systemd-pcrextend systemd-pcrlock + systemd-portabled systemd-pty-forward systemd-pull systemd-random-seed @@ -102,6 +110,7 @@ INTROSPECTABLE=( systemd-report-files systemd-report-sign-plain systemd-report-sign-tsm + systemd-resolved systemd-run systemd-sbsign systemd-sleep @@ -116,7 +125,10 @@ INTROSPECTABLE=( systemd-sysext systemd-sysinstall systemd-sysupdate + systemd-sysupdated systemd-sysusers + systemd-timedated + systemd-timesyncd systemd-tmpfiles systemd-tpm2-clear systemd-tpm2-setup @@ -181,6 +193,14 @@ systemd-clonesetup --introspect-cli | jq -e \ systemd-dissect --introspect-cli | jq -e \ '[.commands[].names[0]] | sort == ["mount.ddi", "systemd-dissect"]' +# systemd-hostnamed does not support --system/--user: options from groups not listed by the +# command must be rejected as unknown and hidden from the introspection (option group filtering) +if command -v systemd-hostnamed >/dev/null; then + (! systemd-hostnamed --system 2>&1) | grep "unrecognized option" >/dev/null + systemd-hostnamed --introspect-cli | jq -e \ + 'all(.commands[]; all(.options[]; .names[-1] != "--system"))' +fi + systemd-id128 --introspect-cli | jq -e \ '.commands[0].verbs | map(.names[0]) | contains(["new", "machine-id", "show", "help"])' From 044ebd7e5491ff4df346e311e8ed7c9208ee2853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 25 Aug 2026 11:08:24 +0200 Subject: [PATCH 06/10] executor: convert to VERB_COMMAND and add --introspect-cli The help is looked up by explicit name: in the multicall configuration the systemd and systemd-executor commands share one binary. --- src/core/executor.c | 42 ++++++------------- .../units/TEST-74-AUX-UTILS.introspect-cli.sh | 1 + 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/src/core/executor.c b/src/core/executor.c index 0c104e5c31..6b96ea3f9c 100644 --- a/src/core/executor.c +++ b/src/core/executor.c @@ -2,9 +2,9 @@ #include +#include "sd-json.h" #include "sd-messages.h" -#include "alloc-util.h" #include "argv-util.h" #include "build.h" #include "capability-util.h" @@ -18,45 +18,24 @@ #include "exit-status.h" #include "fd-util.h" #include "fdset.h" -#include "format-table.h" #include "label-util.h" #include "log.h" #include "options.h" #include "parse-util.h" -#include "pretty-print.h" #include "selinux-util.h" #include "static-destruct.h" +#include "verbs.h" static FILE *arg_serialization = NULL; STATIC_DESTRUCTOR_REGISTER(arg_serialization, fclosep); -static int help(void) { - _cleanup_free_ char *link = NULL; - _cleanup_(table_unrefp) Table *options = NULL; - int r; - - r = terminal_urlify_man("systemd", "1", &link); - if (r < 0) - return log_oom(); - - r = option_parser_get_help_table_ns("systemd-executor", &options); - if (r < 0) - return r; - - printf("%s [OPTIONS...]\n\n" - "%sSandbox and execute processes.%s\n\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-executor\0", + "Sandbox and execute processes.", + .man_pages = "systemd.1\0", + .option_namespace = "systemd-executor", +); static int parse_argv(int argc, char *argv[]) { int r; @@ -72,7 +51,7 @@ static int parse_argv(int argc, char *argv[]) { OPTION_NAMESPACE("systemd-executor"): {} OPTION_COMMON_HELP: - return help(); + return command_print_help_name("systemd-executor"); OPTION_COMMON_VERSION: return version(); @@ -128,6 +107,9 @@ static int parse_argv(int argc, char *argv[]) { arg_serialization = f; break; } + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(SD_JSON_FORMAT_OFF); } if (!arg_serialization) diff --git a/test/units/TEST-74-AUX-UTILS.introspect-cli.sh b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh index 8d85dc9412..8283d1d2da 100755 --- a/test/units/TEST-74-AUX-UTILS.introspect-cli.sh +++ b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh @@ -61,6 +61,7 @@ INTROSPECTABLE=( systemd-detect-virt systemd-dissect systemd-escape + systemd-executor systemd-export systemd-factory-reset systemd-firstboot From 4f2b0e977c260bf351d86d707003ba9b796ac8d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 25 Aug 2026 11:11:00 +0200 Subject: [PATCH 07/10] core: convert to VERB_COMMAND and add --introspect-cli MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --introspect-cli follows the same deferred pattern as --help: parse_argv() only records the action, and it is executed in the same place where ACTION_HELP and ACTION_VERSION are handled. The help is looked up by explicit name: in the multicall configuration the systemd and systemd-executor commands share one binary, so the "first COMMAND" default would depend on link order. The "This program takes no positional arguments." line is dropped from the help output — the default "[OPTION…]" synopsis already says so. (And also, this is not quite true. It does take positional args when called as init.) --- src/basic/proc-cmdline.c | 1 + src/core/main.c | 48 +++++++++---------- src/test/test-proc-cmdline.c | 3 +- .../units/TEST-74-AUX-UTILS.introspect-cli.sh | 7 +++ 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/basic/proc-cmdline.c b/src/basic/proc-cmdline.c index 4de3480ff0..5b8f3c954b 100644 --- a/src/basic/proc-cmdline.c +++ b/src/basic/proc-cmdline.c @@ -37,6 +37,7 @@ int proc_cmdline_filter_pid1_args(char **argv, char ***ret) { { "no-pager", no_argument, }, { "help", no_argument, }, { "version", no_argument, }, + { "introspect-cli", no_argument, }, { "dump-configuration-items", no_argument, }, { "dump-bus-properties", no_argument, }, { "bus-introspect", required_argument }, diff --git a/src/core/main.c b/src/core/main.c index d27156ec3f..fe7e5d8498 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -51,13 +51,11 @@ #include "fd-util.h" #include "fdset.h" #include "fileio.h" -#include "format-table.h" #include "format-util.h" #include "fs-util.h" #include "glyph-util.h" #include "hash-funcs.h" #include "hashmap.h" -#include "help-util.h" #include "hexdecoct.h" #include "hostname-setup.h" #include "id128-util.h" @@ -119,6 +117,7 @@ #include "unit-name.h" #include "user-util.h" #include "utf8.h" +#include "verbs.h" #include "version.h" #include "virt.h" #include "watchdog.h" @@ -131,6 +130,7 @@ static enum { ACTION_RUN, ACTION_HELP, ACTION_VERSION, + ACTION_INTROSPECT_CLI, ACTION_TEST, ACTION_DUMP_CONFIGURATION_ITEMS, ACTION_DUMP_BUS_PROPERTIES, @@ -1039,6 +1039,14 @@ static int redirect_telinit(char *argv[], char **args) { strnull(a), strnull(b), strnull(c)); } +COMMAND( + "systemd\0", + "Start and monitor system and user services.", + .man_pages = "systemd.1\0", + .option_namespace = "systemd", + .pager_flags = &arg_pager_flags, +); + static int parse_argv(int argc, char *argv[]) { bool user_arg_seen = false; int r; @@ -1068,6 +1076,10 @@ static int parse_argv(int argc, char *argv[]) { arg_action = ACTION_VERSION; break; + OPTION_COMMON_INTROSPECT_CLI: + arg_action = ACTION_INTROSPECT_CLI; + break; + OPTION_LONG("test", NULL, "Determine initial transaction, dump it and exit"): arg_action = ACTION_TEST; break; @@ -1268,6 +1280,7 @@ static int parse_argv(int argc, char *argv[]) { "Switch -%c is only supported when running as PID 1.", opts.opt->short_code); /* In PID1, do nothing and continue with option parsing. */ break; + OPTION_ERROR: if (getpid_cached() != 1) return -EINVAL; @@ -1290,28 +1303,6 @@ static int parse_argv(int argc, char *argv[]) { return 0; } -static int help(void) { - _cleanup_(table_unrefp) Table *options = NULL; - int r; - - r = option_parser_get_help_table_ns("systemd", &options); - if (r < 0) - return r; - - help_cmdline("[OPTIONS...]"); - help_abstract("Starts and monitors system and user services."); - - printf("\nThis program takes no positional arguments.\n"); - - help_section("Options"); - r = table_print_or_warn(options); - if (r < 0) - return r; - - help_man_page_reference("systemd", "1"); - return 0; -} - static int prepare_reexecute( Manager *m, FILE **ret_f, @@ -3745,18 +3736,23 @@ static int run_systemd(int argc, char *argv[]) { if (r < 0) goto finish; - if (IN_SET(arg_action, ACTION_TEST, ACTION_HELP, ACTION_DUMP_CONFIGURATION_ITEMS, ACTION_DUMP_BUS_PROPERTIES, ACTION_BUS_INTROSPECT)) + if (IN_SET(arg_action, ACTION_TEST, ACTION_DUMP_CONFIGURATION_ITEMS, ACTION_DUMP_BUS_PROPERTIES, ACTION_BUS_INTROSPECT)) pager_open(arg_pager_flags); if (arg_action != ACTION_RUN) skip_setup = true; if (arg_action == ACTION_HELP) { - retval = help() < 0 ? EXIT_FAILURE : EXIT_SUCCESS; + r = command_print_help_name("systemd"); + retval = r < 0 ? EXIT_FAILURE : EXIT_SUCCESS; goto finish; } else if (arg_action == ACTION_VERSION) { retval = version(); goto finish; + } else if (arg_action == ACTION_INTROSPECT_CLI) { + r = introspect_cli(SD_JSON_FORMAT_OFF); + retval = r < 0 ? EXIT_FAILURE : EXIT_SUCCESS; + goto finish; } else if (arg_action == ACTION_DUMP_CONFIGURATION_ITEMS) { unit_dump_config_items(stdout); retval = EXIT_SUCCESS; diff --git a/src/test/test-proc-cmdline.c b/src/test/test-proc-cmdline.c index 69ad4e212f..c43e7fab1d 100644 --- a/src/test/test-proc-cmdline.c +++ b/src/test/test-proc-cmdline.c @@ -271,9 +271,10 @@ TEST(proc_cmdline_filter_pid1_args) { "--log-color\0--log-level\0drop3\0" /* optional argument ("--log-level" is handled as another option) */ "--log-color\0accept5\0" /* optional argument (separated with space) */ "--log-color=drop4\0accept6\0" /* optional argument (concatenated with '=') */ + "--introspect-cli\0accept7\0" /* no argument */ "--log-color\0--\0" /* "--" is _not_ handled as argument, and remaining strings are accepted */ "remaining\0-x\0--foo\0", - STRV_MAKE("accept1", "accept2", "accept3", "accept4", "accept5", "accept6", "remaining", "-x", "--foo")); + STRV_MAKE("accept1", "accept2", "accept3", "accept4", "accept5", "accept6", "accept7", "remaining", "-x", "--foo")); /* test for "--" */ test_proc_cmdline_filter_pid1_args_one("systemd\0" diff --git a/test/units/TEST-74-AUX-UTILS.introspect-cli.sh b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh index 8283d1d2da..78d329bfc5 100755 --- a/test/units/TEST-74-AUX-UTILS.introspect-cli.sh +++ b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh @@ -40,6 +40,7 @@ INTROSPECTABLE=( shutdown storagectl systemctl + systemd systemd-ac-power systemd-analyze systemd-ask-password @@ -205,6 +206,12 @@ fi systemd-id128 --introspect-cli | jq -e \ '.commands[0].verbs | map(.names[0]) | contains(["new", "machine-id", "show", "help"])' +# systemd is optionally a multicall binary that also provides systemd-executor +if [[ "$(readlink "$(command -v systemd-executor)" 2>/dev/null)" == systemd ]]; then + systemd --introspect-cli | jq -e \ + '[.commands[].names[0]] | sort == ["systemd", "systemd-executor"]' +fi + if command -v systemd-mount >/dev/null; then systemd-mount --introspect-cli | jq -e \ '[.commands[].names[0]] | sort == ["systemd-mount", "systemd-umount"]' From 4927588993725147ae089eba4d3c5d74d19f6356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 25 Aug 2026 13:51:54 +0200 Subject: [PATCH 08/10] shutdown: convert to run() function --- src/shutdown/shutdown.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/shutdown/shutdown.c b/src/shutdown/shutdown.c index ebf011f7a7..7130a996a4 100644 --- a/src/shutdown/shutdown.c +++ b/src/shutdown/shutdown.c @@ -34,6 +34,7 @@ #include "killall.h" #include "log.h" #include "luo-util.h" +#include "main-func.h" #include "options.h" #include "parse-util.h" #include "pidref.h" @@ -366,7 +367,7 @@ static void sleep_until_minimum_uptime(void) { } } -int main(int argc, char *argv[]) { +static int run(int argc, char *argv[]) { static const char* const dirs[] = { SYSTEM_SHUTDOWN_PATH, NULL @@ -402,10 +403,9 @@ int main(int argc, char *argv[]) { log_set_prohibit_ipc(true); log_parse_environment(); - if (getpid_cached() != 1) { - log_error("Not executed by init (PID 1). Refusing to operate."); - return EXIT_FAILURE; - } + if (getpid_cached() != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Not executed by init (PID 1). Refusing to operate."); log_set_always_reopen_console(true); @@ -668,7 +668,6 @@ int main(int argc, char *argv[]) { switch (cmd) { case LINUX_REBOOT_CMD_KEXEC: - if (!in_container) { /* Preserve fd stores via the kernel Live Update Orchestrator before kexec. * The session fd must stay open until the kexec syscall. */ @@ -706,7 +705,7 @@ int main(int argc, char *argv[]) { /* If we are in a container, and we lacked CAP_SYS_BOOT just exit, this will kill our * container for good. */ log_info("Exiting container."); - return EXIT_SUCCESS; + return 0; } r = log_error_errno(errno, "Failed to invoke reboot(): %m"); @@ -717,3 +716,5 @@ error: LOG_MESSAGE_ID(SD_MESSAGE_SHUTDOWN_ERROR_STR)); freeze(); } + +DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); From a4145b8ca3f2e0b3c9f67e9863a1e7c1d4ea4129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 25 Aug 2026 11:13:56 +0200 Subject: [PATCH 09/10] shutdown: convert to VERB_COMMAND and add --introspect-cli systemd-shutdown parsed a bunch of options, but had no way to print them. Add the standard --help and --version options and the CLI introspection. parse_argv() is moved before the PID 1 check, so that the new options also work when the binary is invoked manually. As PID 1 we can never return from main() (the kernel would panic), so in that case parse errors still freeze(), and the help output is followed by a freeze() too. Ctrl-Alt-Delete and console reopening are re-enabled before freezing, so that the freeze can be escaped through the kernel and the error is visible, as before. The --help/--version/introspect-cli option names are already listed in the kernel command line exclusion list in proc-cmdline.c. --- src/shutdown/shutdown.c | 44 ++++++++++++++++--- .../units/TEST-74-AUX-UTILS.introspect-cli.sh | 1 + 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/shutdown/shutdown.c b/src/shutdown/shutdown.c index 7130a996a4..44fddbde7b 100644 --- a/src/shutdown/shutdown.c +++ b/src/shutdown/shutdown.c @@ -16,6 +16,7 @@ #include "alloc-util.h" #include "async.h" #include "binfmt-util.h" +#include "build.h" #include "cgroup-setup.h" #include "cgroup-util.h" #include "constants.h" @@ -49,6 +50,7 @@ #include "terminal-util.h" #include "time-util.h" #include "umount.h" +#include "verbs.h" #include "virt.h" #include "watchdog.h" @@ -60,6 +62,13 @@ static const char *arg_verb = NULL; static uint8_t arg_exit_code = 0; static usec_t arg_timeout = DEFAULT_TIMEOUT_USEC; +COMMAND( + "systemd-shutdown\0", + "Execute the final phase of system shutdown.", + .argspec = "reboot|poweroff|halt|kexec|exit\0", + .man_pages = "systemd-shutdown.8\0", +); + static int parse_argv(int argc, char *argv[]) { assert(argc >= 1); assert(argv); @@ -75,6 +84,15 @@ static int parse_argv(int argc, char *argv[]) { FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { + OPTION_COMMON_HELP: + return command_print_help(); + + OPTION_COMMON_VERSION: + return version(); + + OPTION_COMMON_INTROSPECT_CLI: + return introspect_cli(SD_JSON_FORMAT_OFF); + OPTION_COMMON_LOG_LEVEL: r = log_set_max_level_from_string(opts.arg); if (r < 0) @@ -149,7 +167,7 @@ static int parse_argv(int argc, char *argv[]) { if (!arg_verb) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Verb argument missing."); - return 0; + return 1; /* Further action */ } static int switch_root_initramfs(void) { @@ -403,9 +421,17 @@ static int run(int argc, char *argv[]) { log_set_prohibit_ipc(true); log_parse_environment(); - if (getpid_cached() != 1) + r = parse_argv(argc, argv); + if (getpid_cached() != 1) { + /* If we're a normal user space process, our work is done. Either we hit an error, or were + * called with --help, --version, or another option that was handled by parse_argv(), or it + * looks like we're being asked to do some real work, but we should do that only as part of + * the real shutdown sequence. */ + if (r <= 0) + return r; return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not executed by init (PID 1). Refusing to operate."); + } log_set_always_reopen_console(true); @@ -414,8 +440,7 @@ static int run(int argc, char *argv[]) { * through kernel. */ (void) reboot(RB_ENABLE_CAD); - r = parse_argv(argc, argv); - if (r < 0) + if (r <= 0) goto error; log_open(); @@ -711,9 +736,14 @@ static int run(int argc, char *argv[]) { r = log_error_errno(errno, "Failed to invoke reboot(): %m"); error: - log_struct_errno(LOG_EMERG, r, - LOG_MESSAGE("Critical error while doing system shutdown: %m"), - LOG_MESSAGE_ID(SD_MESSAGE_SHUTDOWN_ERROR_STR)); + if (r == 0) + log_struct(LOG_EMERG, + LOG_MESSAGE("Non-shutdown operation requested, cannot operate."), + LOG_MESSAGE_ID(SD_MESSAGE_SHUTDOWN_ERROR_STR)); + else + log_struct_errno(LOG_EMERG, r, + LOG_MESSAGE("Critical error while doing system shutdown: %m"), + LOG_MESSAGE_ID(SD_MESSAGE_SHUTDOWN_ERROR_STR)); freeze(); } diff --git a/test/units/TEST-74-AUX-UTILS.introspect-cli.sh b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh index 78d329bfc5..cfdbe5c5ef 100755 --- a/test/units/TEST-74-AUX-UTILS.introspect-cli.sh +++ b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh @@ -115,6 +115,7 @@ INTROSPECTABLE=( systemd-resolved systemd-run systemd-sbsign + systemd-shutdown systemd-sleep systemd-socket-activate systemd-socket-proxyd From 04b996d0bbb1c9620c9a247205378bd8ad2c3f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 25 Aug 2026 13:43:19 +0200 Subject: [PATCH 10/10] shutdown: mark as public --- src/shutdown/meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shutdown/meson.build b/src/shutdown/meson.build index 1545db92e5..397c67098f 100644 --- a/src/shutdown/meson.build +++ b/src/shutdown/meson.build @@ -12,6 +12,7 @@ shutdown_detach_sources = files( executables += [ libexec_template + { 'name' : 'systemd-shutdown', + 'public' : true, 'export' : files( 'detach-dm.c', 'detach-loopback.c',