From 979ecbb14ee03150a277e021cdec902f7c40f481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 13 May 2026 17:00:00 +0200 Subject: [PATCH 01/12] systemctl: split out helper for --state= and allow resetting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So far we'd reject --state=, but it seems nicer to make it reset the setting as we generally do. The output variable is modified in place… Option parsing isn't atomic anyway, so I think it's fine to to that. --- man/systemctl.xml | 7 +++-- src/systemctl/systemctl.c | 59 ++++++++++++++++++++++++--------------- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/man/systemctl.xml b/man/systemctl.xml index b8ba0f945e..355956c8ad 100644 --- a/man/systemctl.xml +++ b/man/systemctl.xml @@ -2000,9 +2000,10 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err The argument is a comma-separated list of unit LOAD, SUB, or ACTIVE states. When listing - units with list-units, list-dependencies, show - or status, show only those in the specified states. Use - or to show only failed units. + units with list-units, list-dependencies, + show or status, show only those in the specified states. Use + or to show only failed units. Use + to reset the filter. As a special case, if one of the arguments is , a list of allowed values will be printed and the program will exit. diff --git a/src/systemctl/systemctl.c b/src/systemctl/systemctl.c index 775188b519..47f34e1ad3 100644 --- a/src/systemctl/systemctl.c +++ b/src/systemctl/systemctl.c @@ -334,7 +334,7 @@ static void help_types(void) { DUMP_STRING_TABLE(unit_type, UnitType, _UNIT_TYPE_MAX); } -static void help_states(void) { +static int help_states(void) { if (arg_legend != 0) puts("Available unit load states:"); DUMP_STRING_TABLE(unit_load_state, UnitLoadState, _UNIT_LOAD_STATE_MAX); @@ -389,7 +389,38 @@ static void help_states(void) { if (arg_legend != 0) puts("\nAvailable timer unit substates:"); - DUMP_STRING_TABLE(timer_state, TimerState, _TIMER_STATE_MAX); + return DUMP_STRING_TABLE(timer_state, TimerState, _TIMER_STATE_MAX); +} + +static int parse_states_argument(const char *value, char ***states) { + int r; + + assert(value); + assert(states); + + if (isempty(value)) { + /* reset the setting */ + *states = strv_free(*states); + return 1; + } + + for (const char *p = value;;) { + _cleanup_free_ char *s = NULL; + + r = extract_first_word(&p, &s, ",", /* flags= */ 0); + if (r < 0) + return log_error_errno(r, "Failed to parse --state=: %m"); + if (r == 0) + break; + + if (streq(s, "help")) + return help_states(); + + if (strv_consume(states, TAKE_PTR(s)) < 0) + return log_oom(); + } + + return 1; } static int systemctl_parse_argv(int argc, char *argv[]) { @@ -869,27 +900,9 @@ static int systemctl_parse_argv(int argc, char *argv[]) { break; case ARG_STATE: - if (isempty(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "--state= requires arguments."); - - for (const char *p = optarg;;) { - _cleanup_free_ char *s = NULL; - - r = extract_first_word(&p, &s, ",", 0); - if (r < 0) - return log_error_errno(r, "Failed to parse state: %s", optarg); - if (r == 0) - break; - - if (streq(s, "help")) { - help_states(); - return 0; - } - - if (strv_consume(&arg_states, TAKE_PTR(s)) < 0) - return log_oom(); - } + r = parse_states_argument(optarg, &arg_states); + if (r <= 0) + return r; break; case 'r': From ae83ce85b9eda97ed4a8301558f7f34e8569c84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 13 May 2026 17:10:00 +0200 Subject: [PATCH 02/12] systemctl: split out helper for --property= We explicitly handled --property= in a specific way, so preserve that behaviour. --- src/systemctl/systemctl.c | 50 ++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/systemctl/systemctl.c b/src/systemctl/systemctl.c index 47f34e1ad3..00da6d39b4 100644 --- a/src/systemctl/systemctl.c +++ b/src/systemctl/systemctl.c @@ -327,6 +327,34 @@ static int systemctl_help(void) { return 0; } +static int parse_property_argument(const char *value, char ***properties) { + int r; + + assert(value); + assert(properties); + + if (isempty(value) && !*properties) { + /* Make sure that if the empty property list was specified, we won't show any properties. */ + *properties = strv_new(NULL); + if (!*properties) + return log_oom(); + } else + for (const char *p = value;;) { + _cleanup_free_ char *prop = NULL; + + r = extract_first_word(&p, &prop, ",", /* flags= */ 0); + if (r < 0) + return log_error_errno(r, "Failed to parse property: '%s'", value); + if (r == 0) + break; + + if (strv_consume(properties, TAKE_PTR(prop)) < 0) + return log_oom(); + } + + return 0; +} + static void help_types(void) { if (arg_legend != 0) puts("Available unit types:"); @@ -613,25 +641,9 @@ static int systemctl_parse_argv(int argc, char *argv[]) { _fallthrough_; case 'p': - /* Make sure that if the empty property list was specified, we won't show any - properties. */ - if (isempty(optarg) && !arg_properties) { - arg_properties = new0(char*, 1); - if (!arg_properties) - return log_oom(); - } else - for (const char *p = optarg;;) { - _cleanup_free_ char *prop = NULL; - - r = extract_first_word(&p, &prop, ",", 0); - if (r < 0) - return log_error_errno(r, "Failed to parse property: %s", optarg); - if (r == 0) - break; - - if (strv_consume(&arg_properties, TAKE_PTR(prop)) < 0) - return log_oom(); - } + r = parse_property_argument(optarg, &arg_properties); + if (r < 0) + return r; /* If the user asked for a particular property, show it, even if it is empty. */ SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_SHOW_EMPTY, true); From 04bf62b01270e8aeac0ee203b8be908b9814e5f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 13 May 2026 17:18:30 +0200 Subject: [PATCH 03/12] systemctl: split out helper for --type= and allow resetting Analogous to grandparent commit. --- man/systemctl.xml | 3 +- src/systemctl/systemctl.c | 101 +++++++++++++++++++++----------------- 2 files changed, 59 insertions(+), 45 deletions(-) diff --git a/man/systemctl.xml b/man/systemctl.xml index 355956c8ad..a6d295927a 100644 --- a/man/systemctl.xml +++ b/man/systemctl.xml @@ -1988,7 +1988,8 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err The argument is a comma-separated list of unit types such as and . When units are listed with list-units, list-dependencies, show, or status, - only units of the specified types will be shown. By default, units of all types are shown. + only units of the specified types will be shown. By default, units of all types are shown. + Use to reset the filter. As a special case, if one of the arguments is , a list of allowed values will be printed and the program will exit. diff --git a/src/systemctl/systemctl.c b/src/systemctl/systemctl.c index 00da6d39b4..5c90092f2a 100644 --- a/src/systemctl/systemctl.c +++ b/src/systemctl/systemctl.c @@ -355,13 +355,6 @@ static int parse_property_argument(const char *value, char ***properties) { return 0; } -static void help_types(void) { - if (arg_legend != 0) - puts("Available unit types:"); - - DUMP_STRING_TABLE(unit_type, UnitType, _UNIT_TYPE_MAX); -} - static int help_states(void) { if (arg_legend != 0) puts("Available unit load states:"); @@ -451,6 +444,60 @@ static int parse_states_argument(const char *value, char ***states) { return 1; } +static int help_types(void) { + if (arg_legend != 0) + puts("Available unit types:"); + + return DUMP_STRING_TABLE(unit_type, UnitType, _UNIT_TYPE_MAX); +} + +static int parse_types_argument(const char *value, char ***types, char ***states) { + int r; + + assert(value); + assert(types); + assert(states); + + if (isempty(value)) { + /* reset the setting */ + *types = strv_free(*types); + return 1; + } + + for (const char *p = value;;) { + _cleanup_free_ char *type = NULL; + + r = extract_first_word(&p, &type, ",", /* flags= */ 0); + if (r < 0) + return log_error_errno(r, "Failed to parse -t/--type=: %m"); + if (r == 0) + break; + + if (streq(type, "help")) + return help_types(); + + if (unit_type_from_string(type) >= 0) { + if (strv_consume(types, TAKE_PTR(type)) < 0) + return log_oom(); + continue; + } + + /* It's much nicer to use --state= for load states, but let's support this in + * --types= too for compatibility with old versions */ + if (unit_load_state_from_string(type) >= 0) { + if (strv_consume(states, TAKE_PTR(type)) < 0) + return log_oom(); + continue; + } + + log_error("Unknown unit type or load state '%s'.", type); + return log_info_errno(SYNTHETIC_ERRNO(EINVAL), + "Use -t help to see a list of allowed values."); + } + + return 1; +} + static int systemctl_parse_argv(int argc, char *argv[]) { enum { ARG_FAIL = 0x100, /* compatibility only */ @@ -597,43 +644,9 @@ static int systemctl_parse_argv(int argc, char *argv[]) { return version(); case 't': - if (isempty(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "--type= requires arguments."); - - for (const char *p = optarg;;) { - _cleanup_free_ char *type = NULL; - - r = extract_first_word(&p, &type, ",", 0); - if (r < 0) - return log_error_errno(r, "Failed to parse type: %s", optarg); - if (r == 0) - break; - - if (streq(type, "help")) { - help_types(); - return 0; - } - - if (unit_type_from_string(type) >= 0) { - if (strv_consume(&arg_types, TAKE_PTR(type)) < 0) - return log_oom(); - continue; - } - - /* It's much nicer to use --state= for load states, but let's support this in - * --types= too for compatibility with old versions */ - if (unit_load_state_from_string(type) >= 0) { - if (strv_consume(&arg_states, TAKE_PTR(type)) < 0) - return log_oom(); - continue; - } - - log_error("Unknown unit type or load state '%s'.", type); - return log_info_errno(SYNTHETIC_ERRNO(EINVAL), - "Use -t help to see a list of allowed values."); - } - + r = parse_types_argument(optarg, &arg_types, &arg_states); + if (r <= 0) + return r; break; case 'P': From f01b3122e33f56b0e5d871d6e81a0a6711b9312b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 13 May 2026 17:32:44 +0200 Subject: [PATCH 04/12] systemctl: split out helper for --what and allow resetting Analogous to parent commit. --- man/systemctl.xml | 10 +++--- src/systemctl/systemctl.c | 71 ++++++++++++++++++++++++--------------- 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/man/systemctl.xml b/man/systemctl.xml index a6d295927a..ce485aee14 100644 --- a/man/systemctl.xml +++ b/man/systemctl.xml @@ -2578,10 +2578,12 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err all as a shortcut for specifying all six resource types. If this option is not specified defaults to the combination of cache, runtime and fdstore, i.e. the three kinds of resources that are generally considered - to be redundant and can be reconstructed on next invocation. Note that the explicit removal of the - fdstore resource type is only useful if the - FileDescriptorStorePreserve= option is enabled, since the file descriptor store - is otherwise cleaned automatically when the unit is stopped. + to be redundant and can be reconstructed on next invocation. The empty argument can be used to + reset to this default. + + Note that the explicit removal of the fdstore resource type is only + useful if the FileDescriptorStorePreserve= option is enabled, since the file + descriptor store is otherwise cleaned automatically when the unit is stopped. diff --git a/src/systemctl/systemctl.c b/src/systemctl/systemctl.c index 5c90092f2a..632e54256c 100644 --- a/src/systemctl/systemctl.c +++ b/src/systemctl/systemctl.c @@ -498,6 +498,46 @@ static int parse_types_argument(const char *value, char ***types, char ***states return 1; } +static int parse_what_argument(const char *value, char ***clean_what) { + int r; + + assert(value); + assert(clean_what); + + if (isempty(value)) { + /* reset the setting */ + *clean_what = strv_free(*clean_what); + return 1; + } + + for (const char *p = value;;) { + _cleanup_free_ char *k = NULL; + + r = extract_first_word(&p, &k, ",", /* flags= */ 0); + if (r < 0) + return log_error_errno(r, "Failed to parse --what=: %m"); + if (r == 0) + break; + + if (streq(k, "help")) { + puts("runtime\n" + "state\n" + "cache\n" + "logs\n" + "configuration\n" + "fdstore\n" + "all"); + return 0; + } + + r = strv_consume(clean_what, TAKE_PTR(k)); + if (r < 0) + return log_oom(); + } + + return 1; +} + static int systemctl_parse_argv(int argc, char *argv[]) { enum { ARG_FAIL = 0x100, /* compatibility only */ @@ -967,34 +1007,9 @@ static int systemctl_parse_argv(int argc, char *argv[]) { break; case ARG_WHAT: - if (isempty(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--what= requires arguments (see --what=help)."); - - for (const char *p = optarg;;) { - _cleanup_free_ char *k = NULL; - - r = extract_first_word(&p, &k, ",", 0); - if (r < 0) - return log_error_errno(r, "Failed to parse directory type: %s", optarg); - if (r == 0) - break; - - if (streq(k, "help")) { - puts("runtime\n" - "state\n" - "cache\n" - "logs\n" - "configuration\n" - "fdstore\n" - "all"); - return 0; - } - - r = strv_consume(&arg_clean_what, TAKE_PTR(k)); - if (r < 0) - return log_oom(); - } - + r = parse_what_argument(optarg, &arg_clean_what); + if (r <= 0) + return r; break; case ARG_REBOOT_ARG: From 137736dd645ea245e9a8f5fd6db3141bc4acb5dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 13 May 2026 17:39:26 +0200 Subject: [PATCH 05/12] systemctl: reorder cases in parse_argv() to match order in --help MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compatibility-only options (--fail, --irreversible, --ignore-dependencies, --no-legend) are grouped at the end alongside the '.' / '?' error handlers. The case 'P': … _fallthrough_; case 'p': pair is kept intact and placed at -p's slot in --help, so -P sits immediately before -p in the source. Co-developed-by: Claude Opus 4.7 --- src/systemctl/systemctl.c | 409 +++++++++++++++++++------------------- 1 file changed, 205 insertions(+), 204 deletions(-) diff --git a/src/systemctl/systemctl.c b/src/systemctl/systemctl.c index 632e54256c..799249fe42 100644 --- a/src/systemctl/systemctl.c +++ b/src/systemctl/systemctl.c @@ -683,12 +683,55 @@ static int systemctl_parse_argv(int argc, char *argv[]) { case ARG_VERSION: return version(); + case ARG_SYSTEM: + arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; + break; + + case ARG_USER: + arg_runtime_scope = RUNTIME_SCOPE_USER; + break; + + case 'C': + r = capsule_name_is_valid(optarg); + if (r < 0) + return log_error_errno(r, "Unable to validate capsule name '%s': %m", optarg); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid capsule name: %s", optarg); + + arg_host = optarg; + arg_transport = BUS_TRANSPORT_CAPSULE; + arg_runtime_scope = RUNTIME_SCOPE_USER; + break; + + case 'H': + arg_transport = BUS_TRANSPORT_REMOTE; + arg_host = optarg; + break; + + case 'M': + r = parse_machine_argument(optarg, &arg_host, &arg_transport); + if (r < 0) + return r; + break; + case 't': r = parse_types_argument(optarg, &arg_types, &arg_states); if (r <= 0) return r; break; + case ARG_STATE: + r = parse_states_argument(optarg, &arg_states); + if (r <= 0) + return r; + break; + + case ARG_FAILED: + if (strv_extend(&arg_states, "failed") < 0) + return log_oom(); + + break; + case 'P': SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_ONLY_VALUE, true); _fallthrough_; @@ -708,18 +751,42 @@ static int systemctl_parse_argv(int argc, char *argv[]) { arg_all = true; break; + case 'l': + arg_full = true; + break; + + case 'r': + if (geteuid() != 0) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), + "--recursive requires root privileges."); + + arg_recursive = true; + break; + case ARG_REVERSE: arg_dependency = DEPENDENCY_REVERSE; break; + case ARG_BEFORE: + arg_dependency = DEPENDENCY_BEFORE; + arg_jobs_before = true; + break; + case ARG_AFTER: arg_dependency = DEPENDENCY_AFTER; arg_jobs_after = true; break; - case ARG_BEFORE: - arg_dependency = DEPENDENCY_BEFORE; - arg_jobs_before = true; + case ARG_WITH_DEPENDENCIES: + arg_with_dependencies = true; + break; + + case ARG_JOB_MODE: + _arg_job_mode = optarg; + break; + + case 'T': + arg_show_transaction = true; break; case ARG_SHOW_TYPES: @@ -730,125 +797,22 @@ static int systemctl_parse_argv(int argc, char *argv[]) { SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_ONLY_VALUE, true); break; - case ARG_JOB_MODE: - _arg_job_mode = optarg; - break; - - case ARG_FAIL: - _arg_job_mode = "fail"; - break; - - case ARG_IRREVERSIBLE: - _arg_job_mode = "replace-irreversibly"; - break; - - case ARG_IGNORE_DEPENDENCIES: - _arg_job_mode = "ignore-dependencies"; - break; - - case ARG_USER: - arg_runtime_scope = RUNTIME_SCOPE_USER; - break; - - case ARG_SYSTEM: - arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; - break; - - case ARG_GLOBAL: - arg_runtime_scope = RUNTIME_SCOPE_GLOBAL; - break; - - case 'C': - r = capsule_name_is_valid(optarg); - if (r < 0) - return log_error_errno(r, "Unable to validate capsule name '%s': %m", optarg); - if (r == 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid capsule name: %s", optarg); - - arg_host = optarg; - arg_transport = BUS_TRANSPORT_CAPSULE; - arg_runtime_scope = RUNTIME_SCOPE_USER; - break; - - case ARG_WAIT: - arg_wait = true; - break; - - case ARG_NO_BLOCK: - arg_no_block = true; - break; - - case ARG_NO_LEGEND: - arg_legend = false; - break; - - case ARG_LEGEND: - r = parse_boolean_argument("--legend", optarg, NULL); - if (r < 0) - return r; - arg_legend = r; - break; - - case ARG_NO_PAGER: - arg_pager_flags |= PAGER_DISABLE; - break; - - case ARG_NO_WALL: - arg_no_wall = true; - break; - - case ARG_ROOT: - r = parse_path_argument(optarg, false, &arg_root); + case ARG_CHECK_INHIBITORS: + r = parse_tristate_argument_with_auto("--check-inhibitors=", optarg, &arg_check_inhibitors); if (r < 0) return r; break; - case ARG_IMAGE: - r = parse_path_argument(optarg, false, &arg_image); - if (r < 0) + case 'i': + arg_check_inhibitors = 0; + break; + + case 's': + r = parse_signal_argument(optarg, &arg_signal); + if (r <= 0) return r; break; - case ARG_IMAGE_POLICY: - r = parse_image_policy_argument(optarg, &arg_image_policy); - if (r < 0) - return r; - break; - - case 'l': - arg_full = true; - break; - - case ARG_FAILED: - if (strv_extend(&arg_states, "failed") < 0) - return log_oom(); - - break; - - case ARG_DRY_RUN: - arg_dry_run = true; - break; - - case 'q': - arg_quiet = true; - - if (arg_legend < 0) - arg_legend = false; - - break; - - case 'v': - arg_verbose = true; - break; - - case 'f': - arg_force++; - break; - - case ARG_NO_RELOAD: - arg_no_reload = true; - break; - case ARG_KILL_WHOM: arg_kill_whom = optarg; break; @@ -877,31 +841,130 @@ static int systemctl_parse_argv(int argc, char *argv[]) { break; } - case 's': - r = parse_signal_argument(optarg, &arg_signal); + case ARG_KILL_SUBGROUP: { + if (empty_or_root(optarg)) { + arg_kill_subgroup = mfree(arg_kill_subgroup); + break; + } + + _cleanup_free_ char *p = NULL; + if (path_simplify_alloc(optarg, &p) < 0) + return log_oom(); + + if (!path_is_safe(p)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Control group sub-path '%s' is not valid.", p); + + free_and_replace(arg_kill_subgroup, p); + break; + } + + case ARG_WHAT: + r = parse_what_argument(optarg, &arg_clean_what); if (r <= 0) return r; break; + case ARG_NOW: + arg_now = true; + break; + + case ARG_DRY_RUN: + arg_dry_run = true; + break; + + case 'q': + arg_quiet = true; + + if (arg_legend < 0) + arg_legend = false; + + break; + + case 'v': + arg_verbose = true; + break; + + case ARG_NO_WARN: + arg_no_warn = true; + break; + + case ARG_WAIT: + arg_wait = true; + break; + + case ARG_NO_BLOCK: + arg_no_block = true; + break; + + case ARG_NO_WALL: + arg_no_wall = true; + break; + + case ARG_MESSAGE: + if (strv_extend(&arg_wall, optarg) < 0) + return log_oom(); + break; + + case ARG_NO_RELOAD: + arg_no_reload = true; + break; + + case ARG_LEGEND: + r = parse_boolean_argument("--legend", optarg, NULL); + if (r < 0) + return r; + arg_legend = r; + break; + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + case ARG_NO_ASK_PASSWORD: arg_ask_password = false; break; - case 'H': - arg_transport = BUS_TRANSPORT_REMOTE; - arg_host = optarg; - break; - - case 'M': - r = parse_machine_argument(optarg, &arg_host, &arg_transport); - if (r < 0) - return r; + case ARG_GLOBAL: + arg_runtime_scope = RUNTIME_SCOPE_GLOBAL; break; case ARG_RUNTIME: arg_runtime = true; break; + case 'f': + arg_force++; + break; + + case ARG_PRESET_MODE: + if (streq(optarg, "help")) + return DUMP_STRING_TABLE(unit_file_preset_mode, UnitFilePresetMode, _UNIT_FILE_PRESET_MODE_MAX); + + arg_preset_mode = unit_file_preset_mode_from_string(optarg); + if (arg_preset_mode < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Failed to parse preset mode: %s.", optarg); + + break; + + case ARG_ROOT: + r = parse_path_argument(optarg, false, &arg_root); + if (r < 0) + return r; + break; + + case ARG_IMAGE: + r = parse_path_argument(optarg, false, &arg_image); + if (r < 0) + return r; + break; + + case ARG_IMAGE_POLICY: + r = parse_image_policy_argument(optarg, &arg_image_policy); + if (r < 0) + return r; + break; + case 'n': if (safe_atou(optarg, &arg_lines) < 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), @@ -925,20 +988,6 @@ static int systemctl_parse_argv(int argc, char *argv[]) { } break; - case 'i': - arg_check_inhibitors = 0; - break; - - case ARG_CHECK_INHIBITORS: - r = parse_tristate_argument_with_auto("--check-inhibitors=", optarg, &arg_check_inhibitors); - if (r < 0) - return r; - break; - - case ARG_PLAIN: - arg_plain = true; - break; - case ARG_FIRMWARE_SETUP: arg_firmware_setup = true; break; @@ -964,54 +1013,6 @@ static int systemctl_parse_argv(int argc, char *argv[]) { arg_boot_loader_entry = empty_to_null(optarg); break; - case ARG_STATE: - r = parse_states_argument(optarg, &arg_states); - if (r <= 0) - return r; - break; - - case 'r': - if (geteuid() != 0) - return log_error_errno(SYNTHETIC_ERRNO(EPERM), - "--recursive requires root privileges."); - - arg_recursive = true; - break; - - case ARG_PRESET_MODE: - if (streq(optarg, "help")) - return DUMP_STRING_TABLE(unit_file_preset_mode, UnitFilePresetMode, _UNIT_FILE_PRESET_MODE_MAX); - - arg_preset_mode = unit_file_preset_mode_from_string(optarg); - if (arg_preset_mode < 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Failed to parse preset mode: %s.", optarg); - - break; - - case ARG_NOW: - arg_now = true; - break; - - case ARG_MESSAGE: - if (strv_extend(&arg_wall, optarg) < 0) - return log_oom(); - break; - - case 'T': - arg_show_transaction = true; - break; - - case ARG_WITH_DEPENDENCIES: - arg_with_dependencies = true; - break; - - case ARG_WHAT: - r = parse_what_argument(optarg, &arg_clean_what); - if (r <= 0) - return r; - break; - case ARG_REBOOT_ARG: arg_reboot_argument = optarg; break; @@ -1031,6 +1032,10 @@ static int systemctl_parse_argv(int argc, char *argv[]) { return r; break; + case ARG_PLAIN: + arg_plain = true; + break; + case ARG_TIMESTAMP_STYLE: if (streq(optarg, "help")) return DUMP_STRING_TABLE(timestamp_style, TimestampStyle, _TIMESTAMP_STYLE_MAX); @@ -1054,10 +1059,6 @@ static int systemctl_parse_argv(int argc, char *argv[]) { arg_marked = true; break; - case ARG_NO_WARN: - arg_no_warn = true; - break; - case ARG_DROP_IN: arg_drop_in = optarg; break; @@ -1092,22 +1093,22 @@ static int systemctl_parse_argv(int argc, char *argv[]) { arg_stdin = true; break; - case ARG_KILL_SUBGROUP: { - if (empty_or_root(optarg)) { - arg_kill_subgroup = mfree(arg_kill_subgroup); - break; - } + /* Compatibility-only options, not shown in --help. */ + case ARG_FAIL: + _arg_job_mode = "fail"; + break; - _cleanup_free_ char *p = NULL; - if (path_simplify_alloc(optarg, &p) < 0) - return log_oom(); + case ARG_IRREVERSIBLE: + _arg_job_mode = "replace-irreversibly"; + break; - if (!path_is_safe(p)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Control group sub-path '%s' is not valid.", p); + case ARG_IGNORE_DEPENDENCIES: + _arg_job_mode = "ignore-dependencies"; + break; - free_and_replace(arg_kill_subgroup, p); + case ARG_NO_LEGEND: + arg_legend = false; break; - } case '.': /* Output an error mimicking getopt, and print a hint afterwards */ From 91249f8004479d59a196151de631bf1d8e50187b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 13 May 2026 17:50:24 +0200 Subject: [PATCH 06/12] systemctl: convert parse_argv to OPTION macros MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The verbs[] table still lives in systemctl-main.c — only the option parsing side is migrated. systemctl_dispatch_parse_argv() gains a remaining_args out-param so run() can pass the parsed positional args to systemctl_main(), which dispatches via _dispatch_verb_with_args() instead of dispatch_verb(). The Options section of --help now renders from the OPTION declarations; the verb sections still use raw printfs and will be converted alongside the verbs[] migration. Co-developed-by: Claude Opus 4.7 --- src/systemctl/fuzz-systemctl-parse-argv.c | 2 +- src/systemctl/systemctl-main.c | 15 +- src/systemctl/systemctl.c | 553 ++++++++-------------- src/systemctl/systemctl.h | 2 +- 4 files changed, 195 insertions(+), 377 deletions(-) diff --git a/src/systemctl/fuzz-systemctl-parse-argv.c b/src/systemctl/fuzz-systemctl-parse-argv.c index d30b9d8fed..38c0decd94 100644 --- a/src/systemctl/fuzz-systemctl-parse-argv.c +++ b/src/systemctl/fuzz-systemctl-parse-argv.c @@ -56,7 +56,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { optind = 0; /* this tells the getopt machinery to reinitialize */ arg_transport = BUS_TRANSPORT_LOCAL; - r = systemctl_dispatch_parse_argv(strv_length(argv), argv); + r = systemctl_dispatch_parse_argv(strv_length(argv), argv, /* remaining_args= */ NULL); if (r < 0) log_error_errno(r, "Failed to parse args: %m"); else diff --git a/src/systemctl/systemctl-main.c b/src/systemctl/systemctl-main.c index 4d1830071f..27f7451911 100644 --- a/src/systemctl/systemctl-main.c +++ b/src/systemctl/systemctl-main.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include "dissect-image.h" @@ -11,6 +10,7 @@ #include "main-func.h" #include "mount-util.h" #include "stat-util.h" +#include "strv.h" #include "systemctl.h" #include "systemctl-add-dependency.h" #include "systemctl-cancel-job.h" @@ -47,7 +47,7 @@ #include "verbs.h" #include "virt.h" -static int systemctl_main(int argc, char *argv[]) { +static int systemctl_main(char **args) { static const Verb verbs[] = { { "list-units", VERB_ANY, VERB_ANY, VERB_DEFAULT|VERB_ONLINE_ONLY, verb_list_units }, { "list-unit-files", VERB_ANY, VERB_ANY, 0, verb_list_unit_files }, @@ -135,24 +135,25 @@ static int systemctl_main(int argc, char *argv[]) { {} }; - const Verb *verb = verbs_find_verb(argv[optind], verbs, verbs + ELEMENTSOF(verbs) - 1); + const Verb *verb = verbs_find_verb(args[0], verbs, verbs + ELEMENTSOF(verbs) - 1); if (verb && (verb->flags & VERB_ONLINE_ONLY) && arg_root) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Verb '%s' cannot be used with --root= or --image=.", - argv[optind] ?: verb->verb); + args[0] ?: verb->verb); - return dispatch_verb(argc, argv, verbs, NULL); + return _dispatch_verb_with_args(args, verbs, verbs + ELEMENTSOF(verbs) - 1, NULL); } static int run(int argc, char *argv[]) { _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; _cleanup_(umount_and_freep) char *mounted_dir = NULL; + char **args = STRV_EMPTY; int r; setlocale(LC_ALL, ""); log_setup(); - r = systemctl_dispatch_parse_argv(argc, argv); + r = systemctl_dispatch_parse_argv(argc, argv, &args); if (r <= 0) goto finish; @@ -200,7 +201,7 @@ static int run(int argc, char *argv[]) { switch (arg_action) { case ACTION_SYSTEMCTL: - r = systemctl_main(argc, argv); + r = systemctl_main(args); break; /* Legacy command aliases set arg_action. They provide some fallbacks, e.g. to tell sysvinit to diff --git a/src/systemctl/systemctl.c b/src/systemctl/systemctl.c index 799249fe42..8a241495c0 100644 --- a/src/systemctl/systemctl.c +++ b/src/systemctl/systemctl.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include @@ -10,9 +9,11 @@ #include "bus-util.h" #include "capsule-util.h" #include "extract-word.h" +#include "format-table.h" #include "help-util.h" #include "image-policy.h" #include "install.h" +#include "options.h" #include "output-mode.h" #include "pager.h" #include "parse-argument.h" @@ -108,9 +109,16 @@ STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); STATIC_DESTRUCTOR_REGISTER(arg_kill_subgroup, freep); static int systemctl_help(void) { + _cleanup_(table_unrefp) Table *options_table = NULL; + int r; + + r = option_parser_get_help_table_ns("systemctl", &options_table); + if (r < 0) + return r; + pager_open(arg_pager_flags); - help_cmdline("[OPTIONS...] COMMAND ..."); + help_cmdline("[OPTIONS…] COMMAND …"); help_abstract("Query or send control commands to the system manager."); help_section("Unit Commands"); @@ -227,100 +235,9 @@ static int systemctl_help(void) { " time, and hibernate\n"); help_section("Options"); - printf(" -h --help Show this help\n" - " --version Show package version\n" - " --system Connect to system manager\n" - " --user Connect to user service manager\n" - " -C --capsule=NAME Connect to service manager of specified capsule\n" - " -H --host=[USER@]HOST Operate on remote host\n" - " -M --machine=CONTAINER Operate on a local container\n" - " -t --type=TYPE List units of a particular type\n" - " --state=STATE List units with particular LOAD or SUB or ACTIVE state\n" - " --failed Shortcut for --state=failed\n" - " -p --property=NAME Show only properties by this name\n" - " -P NAME Equivalent to --value --property=NAME\n" - " -a --all Show all properties/all units currently in memory,\n" - " including dead/empty ones. To list all units installed\n" - " on the system, use 'list-unit-files' instead.\n" - " -l --full Don't ellipsize unit names on output\n" - " -r --recursive Show unit list of host and local containers\n" - " --reverse Show reverse dependencies with 'list-dependencies'\n" - " --before Show units ordered before with 'list-dependencies'\n" - " --after Show units ordered after with 'list-dependencies'\n" - " --with-dependencies Show unit dependencies with 'status', 'cat',\n" - " 'list-units', and 'list-unit-files'.\n" - " --job-mode=MODE Specify how to deal with already queued jobs, when\n" - " queueing a new job\n" - " -T --show-transaction When enqueuing a unit job, show full transaction\n" - " --show-types When showing sockets, explicitly show their type\n" - " --value When showing properties, only print the value\n" - " --check-inhibitors=MODE\n" - " Whether to check inhibitors before shutting down,\n" - " sleeping, or hibernating\n" - " -i Shortcut for --check-inhibitors=no\n" - " -s --signal=SIGNAL Which signal to send\n" - " --kill-whom=WHOM Whom to send signal to\n" - " --kill-value=INT Signal value to enqueue\n" - " --kill-subgroup=PATH\n" - " Send signal to sub-control group only\n" - " --what=RESOURCES Which types of resources to remove\n" - " --now Start or stop unit after enabling or disabling it\n" - " --dry-run Only print what would be done\n" - " Currently supported by verbs: halt, poweroff, reboot,\n" - " kexec, soft-reboot, suspend, hibernate, \n" - " suspend-then-hibernate, hybrid-sleep, default,\n" - " rescue, emergency, and exit.\n" - " -q --quiet Suppress output\n" - " -v --verbose Show unit logs while executing operation\n" - " --no-warn Suppress several warnings shown by default\n" - " --wait For (re)start, wait until service stopped again\n" - " For is-system-running, wait until startup is completed\n" - " For kill, wait until service stopped\n" - " --no-block Do not wait until operation finished\n" - " --no-wall Don't send wall message before halt/power-off/reboot\n" - " --message=MESSAGE Specify human-readable reason for system shutdown\n" - " --no-reload Don't reload daemon after en-/dis-abling unit files\n" - " --legend=BOOL Enable/disable the legend (column headers and hints)\n" - " --no-pager Do not pipe output into a pager\n" - " --no-ask-password Do not ask for system passwords\n" - " --global Edit/enable/disable/mask default user unit files\n" - " globally\n" - " --runtime Edit/enable/disable/mask unit files temporarily until\n" - " next reboot\n" - " -f --force When enabling unit files, override existing symlinks\n" - " When shutting down, execute action immediately\n" - " --preset-mode= Apply only enable, only disable, or all presets\n" - " --root=PATH Edit/enable/disable/mask unit files in the specified\n" - " root directory\n" - " --image=PATH Edit/enable/disable/mask unit files in the specified\n" - " disk image\n" - " --image-policy=POLICY\n" - " Specify disk image dissection policy\n" - " -n --lines=INTEGER Number of journal entries to show\n" - " -o --output=STRING Change journal output mode (short, short-precise,\n" - " short-iso, short-iso-precise, short-full,\n" - " short-monotonic, short-unix, short-delta,\n" - " verbose, export, json, json-pretty, json-sse, cat)\n" - " --firmware-setup Tell the firmware to show the setup menu on next boot\n" - " --boot-loader-menu=TIME\n" - " Boot into boot loader menu on next boot\n" - " --boot-loader-entry=NAME\n" - " Boot into a specific boot loader entry on next boot\n" - " --reboot-argument=ARG\n" - " Specify argument string to pass to reboot()\n" - " --kernel-cmdline=CMDLINE\n" - " Append to the kernel command line when loading the\n" - " kernel from the booted boot loader entry\n" - " --plain Print unit dependencies as a list instead of a tree\n" - " --timestamp=FORMAT Change format of printed timestamps (pretty, unix,\n" - " us, utc, us+utc)\n" - " --read-only Create read-only bind mount\n" - " --mkdir Create directory before mounting, if missing\n" - " --marked Restart/reload previously marked units\n" - " --drop-in=NAME Edit unit files using the specified drop-in file name\n" - " --when=TIME Schedule halt/power-off/reboot/kexec action after\n" - " a certain timestamp\n" - " --stdin Read new contents of edited file from stdin\n"); + r = table_print_or_warn(options_table); + if (r < 0) + return r; help_man_page_reference("systemctl", "1"); @@ -538,134 +455,8 @@ static int parse_what_argument(const char *value, char ***clean_what) { return 1; } -static int systemctl_parse_argv(int argc, char *argv[]) { - enum { - ARG_FAIL = 0x100, /* compatibility only */ - ARG_REVERSE, - ARG_AFTER, - ARG_BEFORE, - ARG_CHECK_INHIBITORS, - ARG_DRY_RUN, - ARG_SHOW_TYPES, - ARG_IRREVERSIBLE, /* compatibility only */ - ARG_IGNORE_DEPENDENCIES, /* compatibility only */ - ARG_VALUE, - ARG_VERSION, - ARG_USER, - ARG_SYSTEM, - ARG_GLOBAL, - ARG_NO_BLOCK, - ARG_LEGEND, - ARG_NO_LEGEND, /* compatibility only */ - ARG_NO_PAGER, - ARG_NO_WALL, - ARG_ROOT, - ARG_IMAGE, - ARG_IMAGE_POLICY, - ARG_NO_RELOAD, - ARG_KILL_WHOM, - ARG_KILL_VALUE, - ARG_NO_ASK_PASSWORD, - ARG_FAILED, - ARG_RUNTIME, - ARG_PLAIN, - ARG_STATE, - ARG_JOB_MODE, - ARG_PRESET_MODE, - ARG_FIRMWARE_SETUP, - ARG_BOOT_LOADER_MENU, - ARG_BOOT_LOADER_ENTRY, - ARG_NOW, - ARG_MESSAGE, - ARG_WITH_DEPENDENCIES, - ARG_WAIT, - ARG_WHAT, - ARG_REBOOT_ARG, - ARG_KERNEL_CMDLINE, - ARG_TIMESTAMP_STYLE, - ARG_READ_ONLY, - ARG_MKDIR, - ARG_MARKED, - ARG_NO_WARN, - ARG_DROP_IN, - ARG_WHEN, - ARG_STDIN, - ARG_KILL_SUBGROUP, - }; - - static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "type", required_argument, NULL, 't' }, - { "property", required_argument, NULL, 'p' }, - { "all", no_argument, NULL, 'a' }, - { "reverse", no_argument, NULL, ARG_REVERSE }, - { "after", no_argument, NULL, ARG_AFTER }, - { "before", no_argument, NULL, ARG_BEFORE }, - { "show-types", no_argument, NULL, ARG_SHOW_TYPES }, - { "failed", no_argument, NULL, ARG_FAILED }, - { "full", no_argument, NULL, 'l' }, - { "job-mode", required_argument, NULL, ARG_JOB_MODE }, - { "fail", no_argument, NULL, ARG_FAIL }, /* compatibility only */ - { "irreversible", no_argument, NULL, ARG_IRREVERSIBLE }, /* compatibility only */ - { "ignore-dependencies", no_argument, NULL, ARG_IGNORE_DEPENDENCIES }, /* compatibility only */ - { "ignore-inhibitors", no_argument, NULL, 'i' }, /* compatibility only */ - { "check-inhibitors", required_argument, NULL, ARG_CHECK_INHIBITORS }, - { "value", no_argument, NULL, ARG_VALUE }, - { "user", no_argument, NULL, ARG_USER }, - { "system", no_argument, NULL, ARG_SYSTEM }, - { "global", no_argument, NULL, ARG_GLOBAL }, - { "capsule", required_argument, NULL, 'C' }, - { "wait", no_argument, NULL, ARG_WAIT }, - { "no-block", no_argument, NULL, ARG_NO_BLOCK }, - { "legend", required_argument, NULL, ARG_LEGEND }, - { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, /* compatibility only */ - { "no-pager", no_argument, NULL, ARG_NO_PAGER }, - { "no-wall", no_argument, NULL, ARG_NO_WALL }, - { "dry-run", no_argument, NULL, ARG_DRY_RUN }, - { "quiet", no_argument, NULL, 'q' }, - { "verbose", no_argument, NULL, 'v' }, - { "no-warn", no_argument, NULL, ARG_NO_WARN }, - { "root", required_argument, NULL, ARG_ROOT }, - { "image", required_argument, NULL, ARG_IMAGE }, - { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, - { "force", no_argument, NULL, 'f' }, - { "no-reload", no_argument, NULL, ARG_NO_RELOAD }, - { "kill-whom", required_argument, NULL, ARG_KILL_WHOM }, - { "kill-value", required_argument, NULL, ARG_KILL_VALUE }, - { "signal", required_argument, NULL, 's' }, - { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD }, - { "host", required_argument, NULL, 'H' }, - { "machine", required_argument, NULL, 'M' }, - { "runtime", no_argument, NULL, ARG_RUNTIME }, - { "lines", required_argument, NULL, 'n' }, - { "output", required_argument, NULL, 'o' }, - { "plain", no_argument, NULL, ARG_PLAIN }, - { "state", required_argument, NULL, ARG_STATE }, - { "recursive", no_argument, NULL, 'r' }, - { "with-dependencies", no_argument, NULL, ARG_WITH_DEPENDENCIES }, - { "preset-mode", required_argument, NULL, ARG_PRESET_MODE }, - { "firmware-setup", no_argument, NULL, ARG_FIRMWARE_SETUP }, - { "boot-loader-menu", required_argument, NULL, ARG_BOOT_LOADER_MENU }, - { "boot-loader-entry", required_argument, NULL, ARG_BOOT_LOADER_ENTRY }, - { "now", no_argument, NULL, ARG_NOW }, - { "message", required_argument, NULL, ARG_MESSAGE }, - { "show-transaction", no_argument, NULL, 'T' }, - { "what", required_argument, NULL, ARG_WHAT }, - { "reboot-argument", required_argument, NULL, ARG_REBOOT_ARG }, - { "kernel-cmdline", required_argument, NULL, ARG_KERNEL_CMDLINE }, - { "timestamp", required_argument, NULL, ARG_TIMESTAMP_STYLE }, - { "read-only", no_argument, NULL, ARG_READ_ONLY }, - { "mkdir", no_argument, NULL, ARG_MKDIR }, - { "marked", no_argument, NULL, ARG_MARKED }, - { "drop-in", required_argument, NULL, ARG_DROP_IN }, - { "when", required_argument, NULL, ARG_WHEN }, - { "stdin", no_argument, NULL, ARG_STDIN }, - { "kill-subgroup", required_argument, NULL, ARG_KILL_SUBGROUP }, - {} - }; - - int c, r; +static int systemctl_parse_argv(int argc, char *argv[], char ***remaining_args) { + int r; assert(argc >= 0); assert(argv); @@ -673,182 +464,191 @@ static int systemctl_parse_argv(int argc, char *argv[]) { /* We default to allowing interactive authorization only in systemctl (not in the legacy commands) */ arg_ask_password = true; - while ((c = getopt_long(argc, argv, "hC:t:p:P:alqvfs:H:M:n:o:iTr.::", options, NULL)) >= 0) + OptionParser opts = { argc, argv, .namespace = "systemctl" }; + FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { - case 'h': + OPTION_NAMESPACE("systemctl"): {} + + OPTION_COMMON_HELP: return systemctl_help(); - case ARG_VERSION: + OPTION_COMMON_VERSION: return version(); - case ARG_SYSTEM: + OPTION_LONG("system", NULL, "Connect to the system service manager"): arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; break; - case ARG_USER: + OPTION_LONG("user", NULL, "Connect to the user service manager"): arg_runtime_scope = RUNTIME_SCOPE_USER; break; - case 'C': - r = capsule_name_is_valid(optarg); + OPTION('C', "capsule", "NAME", "Connect to service manager of specified capsule"): + r = capsule_name_is_valid(opts.arg); if (r < 0) - return log_error_errno(r, "Unable to validate capsule name '%s': %m", optarg); + return log_error_errno(r, "Unable to validate capsule name '%s': %m", opts.arg); if (r == 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid capsule name: %s", optarg); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid capsule name: %s", opts.arg); - arg_host = optarg; + arg_host = opts.arg; arg_transport = BUS_TRANSPORT_CAPSULE; arg_runtime_scope = RUNTIME_SCOPE_USER; break; - case 'H': + OPTION_COMMON_HOST: arg_transport = BUS_TRANSPORT_REMOTE; - arg_host = optarg; + arg_host = opts.arg; break; - case 'M': - r = parse_machine_argument(optarg, &arg_host, &arg_transport); + OPTION_COMMON_MACHINE: + r = parse_machine_argument(opts.arg, &arg_host, &arg_transport); if (r < 0) return r; break; - case 't': - r = parse_types_argument(optarg, &arg_types, &arg_states); + OPTION('t', "type", "TYPE", "List units of a particular type"): + r = parse_types_argument(opts.arg, &arg_types, &arg_states); if (r <= 0) return r; break; - case ARG_STATE: - r = parse_states_argument(optarg, &arg_states); + OPTION_LONG("state", "STATE", "List units with particular LOAD or SUB or ACTIVE state"): + r = parse_states_argument(opts.arg, &arg_states); if (r <= 0) return r; break; - case ARG_FAILED: + OPTION_LONG("failed", NULL, "Shortcut for --state=failed"): if (strv_extend(&arg_states, "failed") < 0) return log_oom(); - break; - case 'P': - SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_ONLY_VALUE, true); - _fallthrough_; - - case 'p': - r = parse_property_argument(optarg, &arg_properties); + OPTION('p', "property", "NAME", "Show only properties by this name"): {} + OPTION_SHORT('P', "NAME", "Equivalent to --value --property=NAME"): + r = parse_property_argument(opts.arg, &arg_properties); if (r < 0) return r; /* If the user asked for a particular property, show it, even if it is empty. */ SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_SHOW_EMPTY, true); + if (opts.opt->short_code == 'P') + SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_ONLY_VALUE, true); + break; - case 'a': + OPTION('a', "all", NULL, + "Show all properties/all units currently in memory, including dead/empty ones. " + "To list all units installed on the system, use 'list-unit-files' instead"): SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_SHOW_EMPTY, true); arg_all = true; break; - case 'l': + OPTION('l', "full", NULL, "Don't ellipsize unit names on output"): arg_full = true; break; - case 'r': + OPTION('r', "recursive", NULL, "Show unit list of host and local containers"): if (geteuid() != 0) return log_error_errno(SYNTHETIC_ERRNO(EPERM), - "--recursive requires root privileges."); + "--recursive requires root privileges"); arg_recursive = true; break; - case ARG_REVERSE: + OPTION_LONG("reverse", NULL, "Show reverse dependencies with 'list-dependencies'"): arg_dependency = DEPENDENCY_REVERSE; break; - case ARG_BEFORE: + OPTION_LONG("before", NULL, "Show units ordered before with 'list-dependencies'"): arg_dependency = DEPENDENCY_BEFORE; arg_jobs_before = true; break; - case ARG_AFTER: + OPTION_LONG("after", NULL, "Show units ordered after with 'list-dependencies'"): arg_dependency = DEPENDENCY_AFTER; arg_jobs_after = true; break; - case ARG_WITH_DEPENDENCIES: + OPTION_LONG("with-dependencies", NULL, + "Show unit dependencies with 'status', 'cat', 'list-units', and 'list-unit-files'"): arg_with_dependencies = true; break; - case ARG_JOB_MODE: - _arg_job_mode = optarg; + OPTION_LONG("job-mode", "MODE", + "Specify how to deal with already queued jobs, when queueing a new job"): + _arg_job_mode = opts.arg; break; - case 'T': + OPTION('T', "show-transaction", NULL, "When enqueuing a unit job, show full transaction"): arg_show_transaction = true; break; - case ARG_SHOW_TYPES: + OPTION_LONG("show-types", NULL, "When showing sockets, explicitly show their type"): arg_show_types = true; break; - case ARG_VALUE: + OPTION_LONG("value", NULL, "When showing properties, only print the value"): SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_ONLY_VALUE, true); break; - case ARG_CHECK_INHIBITORS: - r = parse_tristate_argument_with_auto("--check-inhibitors=", optarg, &arg_check_inhibitors); + OPTION_LONG("check-inhibitors", "MODE", + "Whether to check inhibitors before shutting down, sleeping, or hibernating"): + r = parse_tristate_argument_with_auto("--check-inhibitors=", opts.arg, &arg_check_inhibitors); if (r < 0) return r; break; - case 'i': + OPTION_LONG("ignore-inhibitors", NULL, /* help= */ NULL): {} + + OPTION_SHORT('i', NULL, "Shortcut for --check-inhibitors=no"): arg_check_inhibitors = 0; break; - case 's': - r = parse_signal_argument(optarg, &arg_signal); + OPTION('s', "signal", "SIGNAL", "Which signal to send"): + r = parse_signal_argument(opts.arg, &arg_signal); if (r <= 0) return r; break; - case ARG_KILL_WHOM: - arg_kill_whom = optarg; + OPTION_LONG("kill-whom", "WHOM", "Whom to send signal to"): + arg_kill_whom = opts.arg; break; - case ARG_KILL_VALUE: { + OPTION_LONG("kill-value", "INT", "Signal value to enqueue"): { unsigned u; - if (isempty(optarg)) { + if (isempty(opts.arg)) { arg_kill_value_set = false; return 0; } /* First, try to parse unsigned, so that we can support the prefixes 0x, 0o, 0b */ - r = safe_atou_full(optarg, 0, &u); + r = safe_atou_full(opts.arg, 0, &u); if (r < 0) /* If this didn't work, try as signed integer, without those prefixes */ - r = safe_atoi(optarg, &arg_kill_value); + r = safe_atoi(opts.arg, &arg_kill_value); else if (u > INT_MAX) r = -ERANGE; else arg_kill_value = (int) u; if (r < 0) - return log_error_errno(r, "Unable to parse signal queue value: %s", optarg); + return log_error_errno(r, "Unable to parse signal queue value: %s", opts.arg); arg_kill_value_set = true; break; } - case ARG_KILL_SUBGROUP: { - if (empty_or_root(optarg)) { + OPTION_LONG("kill-subgroup", "PATH", "Send signal to sub-control group only"): { + if (empty_or_root(opts.arg)) { arg_kill_subgroup = mfree(arg_kill_subgroup); break; } _cleanup_free_ char *p = NULL; - if (path_simplify_alloc(optarg, &p) < 0) + if (path_simplify_alloc(opts.arg, &p) < 0) return log_oom(); if (!path_is_safe(p)) @@ -858,21 +658,24 @@ static int systemctl_parse_argv(int argc, char *argv[]) { break; } - case ARG_WHAT: - r = parse_what_argument(optarg, &arg_clean_what); + OPTION_LONG("what", "RESOURCES", "Which types of resources to remove"): + r = parse_what_argument(opts.arg, &arg_clean_what); if (r <= 0) return r; break; - case ARG_NOW: + OPTION_LONG("now", NULL, "Start or stop unit after enabling or disabling it"): arg_now = true; break; - case ARG_DRY_RUN: + OPTION_LONG("dry-run", NULL, + "Only print what would be done. Currently supported by verbs: halt, poweroff, " + "reboot, kexec, soft-reboot, suspend, hibernate, suspend-then-hibernate, " + "hybrid-sleep, default, rescue, emergency, and exit."): arg_dry_run = true; break; - case 'q': + OPTION('q', "quiet", NULL, "Suppress output"): arg_quiet = true; if (arg_legend < 0) @@ -880,107 +683,118 @@ static int systemctl_parse_argv(int argc, char *argv[]) { break; - case 'v': + OPTION('v', "verbose", NULL, "Show unit logs while executing operation"): arg_verbose = true; break; - case ARG_NO_WARN: + OPTION_LONG("no-warn", NULL, "Suppress several warnings shown by default"): arg_no_warn = true; break; - case ARG_WAIT: + OPTION_LONG("wait", NULL, + "For (re)start, wait until service stopped again. " + "For is-system-running, wait until startup is completed. " + "For kill, wait until service stopped."): arg_wait = true; break; - case ARG_NO_BLOCK: + OPTION_LONG("no-block", NULL, "Do not wait until operation finished"): arg_no_block = true; break; - case ARG_NO_WALL: + OPTION_LONG("no-wall", NULL, "Don't send wall message before halt/power-off/reboot"): arg_no_wall = true; break; - case ARG_MESSAGE: - if (strv_extend(&arg_wall, optarg) < 0) + OPTION_LONG("message", "MESSAGE", "Specify human-readable reason for system shutdown"): + if (strv_extend(&arg_wall, opts.arg) < 0) return log_oom(); break; - case ARG_NO_RELOAD: + OPTION_LONG("no-reload", NULL, "Don't reload daemon after en-/dis-abling unit files"): arg_no_reload = true; break; - case ARG_LEGEND: - r = parse_boolean_argument("--legend", optarg, NULL); + OPTION_LONG("legend", "BOOL", "Enable/disable the legend (column headers and hints)"): + r = parse_boolean_argument("--legend", opts.arg, NULL); if (r < 0) return r; arg_legend = r; break; - case ARG_NO_PAGER: + OPTION_COMMON_NO_PAGER: arg_pager_flags |= PAGER_DISABLE; break; - case ARG_NO_ASK_PASSWORD: + OPTION_COMMON_NO_ASK_PASSWORD: arg_ask_password = false; break; - case ARG_GLOBAL: + OPTION_LONG("global", NULL, "Edit/enable/disable/mask default user unit files globally"): arg_runtime_scope = RUNTIME_SCOPE_GLOBAL; break; - case ARG_RUNTIME: + OPTION_LONG("runtime", NULL, + "Edit/enable/disable/mask unit files temporarily until next reboot"): arg_runtime = true; break; - case 'f': + OPTION('f', "force", NULL, + "When enabling unit files, override existing symlinks. " + "When shutting down, execute action immediately."): arg_force++; break; - case ARG_PRESET_MODE: - if (streq(optarg, "help")) + OPTION_LONG("preset-mode", "MODE", "Apply only enable, only disable, or all presets"): + if (streq(opts.arg, "help")) return DUMP_STRING_TABLE(unit_file_preset_mode, UnitFilePresetMode, _UNIT_FILE_PRESET_MODE_MAX); - arg_preset_mode = unit_file_preset_mode_from_string(optarg); + arg_preset_mode = unit_file_preset_mode_from_string(opts.arg); if (arg_preset_mode < 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Failed to parse preset mode: %s.", optarg); + "Failed to parse preset mode: %s.", opts.arg); break; - case ARG_ROOT: - r = parse_path_argument(optarg, false, &arg_root); + OPTION_LONG("root", "PATH", + "Edit/enable/disable/mask unit files in the specified root directory"): + r = parse_path_argument(opts.arg, false, &arg_root); if (r < 0) return r; break; - case ARG_IMAGE: - r = parse_path_argument(optarg, false, &arg_image); + OPTION_LONG("image", "PATH", + "Edit/enable/disable/mask unit files in the specified disk image"): + r = parse_path_argument(opts.arg, false, &arg_image); if (r < 0) return r; break; - case ARG_IMAGE_POLICY: - r = parse_image_policy_argument(optarg, &arg_image_policy); + OPTION_LONG("image-policy", "POLICY", "Specify disk image dissection policy"): + r = parse_image_policy_argument(opts.arg, &arg_image_policy); if (r < 0) return r; break; - case 'n': - if (safe_atou(optarg, &arg_lines) < 0) + OPTION('n', "lines", "INTEGER", "Number of journal entries to show"): + if (safe_atou(opts.arg, &arg_lines) < 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse lines '%s'", - optarg); + opts.arg); break; - case 'o': - if (streq(optarg, "help")) + OPTION('o', "output", "STRING", + "Change journal output mode (short, short-precise, short-iso, short-iso-precise, " + "short-full, short-monotonic, short-unix, short-delta, verbose, export, json, " + "json-pretty, json-sse, cat)"): + if (streq(opts.arg, "help")) return DUMP_STRING_TABLE(output_mode, OutputMode, _OUTPUT_MODE_MAX); - arg_output = output_mode_from_string(optarg); + arg_output = output_mode_from_string(opts.arg); if (arg_output < 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown output '%s'.", - optarg); + opts.arg); if (OUTPUT_MODE_IS_JSON(arg_output)) { arg_legend = false; @@ -988,21 +802,20 @@ static int systemctl_parse_argv(int argc, char *argv[]) { } break; - case ARG_FIRMWARE_SETUP: + OPTION_LONG("firmware-setup", NULL, "Tell the firmware to show the setup menu on next boot"): arg_firmware_setup = true; break; - case ARG_BOOT_LOADER_MENU: - - r = parse_sec(optarg, &arg_boot_loader_menu); + OPTION_LONG("boot-loader-menu", "TIME", "Boot into boot loader menu on next boot"): + r = parse_sec(opts.arg, &arg_boot_loader_menu); if (r < 0) - return log_error_errno(r, "Failed to parse --boot-loader-menu= argument '%s': %m", optarg); + return log_error_errno(r, "Failed to parse --boot-loader-menu= argument '%s': %m", opts.arg); break; - case ARG_BOOT_LOADER_ENTRY: - - if (streq(optarg, "help")) { /* Yes, this means, "help" is not a valid boot loader entry name we can deal with */ + OPTION_LONG("boot-loader-entry", "NAME", + "Boot into a specific boot loader entry on next boot"): + if (streq(opts.arg, "help")) { /* Yes, this means, "help" is not a valid boot loader entry name we can deal with */ r = help_boot_loader_entry(); if (r < 0) return r; @@ -1010,119 +823,117 @@ static int systemctl_parse_argv(int argc, char *argv[]) { return 0; } - arg_boot_loader_entry = empty_to_null(optarg); + arg_boot_loader_entry = empty_to_null(opts.arg); break; - case ARG_REBOOT_ARG: - arg_reboot_argument = optarg; + OPTION_LONG("reboot-argument", "ARG", "Specify argument string to pass to reboot()"): + arg_reboot_argument = opts.arg; break; - case ARG_KERNEL_CMDLINE: - if (isempty(optarg)) { + OPTION_LONG("kernel-cmdline", "CMDLINE", + "Append to the kernel command line when loading the kernel " + "from the booted boot loader entry"): + if (isempty(opts.arg)) { arg_kernel_cmdline = mfree(arg_kernel_cmdline); break; } - if (!string_is_safe(optarg, STRING_ALLOW_GLOBS|STRING_ALLOW_BACKSLASHES|STRING_ALLOW_QUOTES)) + if (!string_is_safe(opts.arg, STRING_ALLOW_GLOBS|STRING_ALLOW_BACKSLASHES|STRING_ALLOW_QUOTES)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "--kernel-cmdline= argument contains invalid characters: %s", optarg); + "--kernel-cmdline= argument contains invalid characters: %s", opts.arg); - r = free_and_strdup_warn(&arg_kernel_cmdline, optarg); + r = free_and_strdup_warn(&arg_kernel_cmdline, opts.arg); if (r < 0) return r; break; - case ARG_PLAIN: + OPTION_LONG("plain", NULL, "Print unit dependencies as a list instead of a tree"): arg_plain = true; break; - case ARG_TIMESTAMP_STYLE: - if (streq(optarg, "help")) + OPTION_LONG("timestamp", "FORMAT", + "Change format of printed timestamps (pretty, unix, us, utc, us+utc)"): + if (streq(opts.arg, "help")) return DUMP_STRING_TABLE(timestamp_style, TimestampStyle, _TIMESTAMP_STYLE_MAX); - arg_timestamp_style = timestamp_style_from_string(optarg); + arg_timestamp_style = timestamp_style_from_string(opts.arg); if (arg_timestamp_style < 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Invalid value: %s.", optarg); + "Invalid value: %s.", opts.arg); break; - case ARG_READ_ONLY: + OPTION_LONG("read-only", NULL, "Create read-only bind mount"): arg_read_only = true; break; - case ARG_MKDIR: + OPTION_LONG("mkdir", NULL, "Create directory before mounting, if missing"): arg_mkdir = true; break; - case ARG_MARKED: + OPTION_LONG("marked", NULL, "Restart/reload previously marked units"): arg_marked = true; break; - case ARG_DROP_IN: - arg_drop_in = optarg; + OPTION_LONG("drop-in", "NAME", "Edit unit files using the specified drop-in file name"): + arg_drop_in = opts.arg; break; - case ARG_WHEN: - if (streq(optarg, "show")) { + OPTION_LONG("when", "TIME", + "Schedule halt/power-off/reboot/kexec action after a certain timestamp"): + if (streq(opts.arg, "show")) { arg_action = ACTION_SYSTEMCTL_SHOW_SHUTDOWN; return 1; } - if (STR_IN_SET(optarg, "", "cancel")) { + if (STR_IN_SET(opts.arg, "", "cancel")) { arg_action = ACTION_CANCEL_SHUTDOWN; return 1; } - if (streq(optarg, "auto")) { + if (streq(opts.arg, "auto")) { arg_when = USEC_INFINITY; /* logind chooses on server side */ break; } - r = parse_timestamp(optarg, &arg_when); + r = parse_timestamp(opts.arg, &arg_when); if (r < 0) - return log_error_errno(r, "Failed to parse --when= argument '%s': %m", optarg); + return log_error_errno(r, "Failed to parse --when= argument '%s': %m", opts.arg); if (!timestamp_is_set(arg_when)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Invalid timestamp '%s' specified for --when=.", optarg); + "Invalid timestamp '%s' specified for --when=.", opts.arg); break; - case ARG_STDIN: + OPTION_LONG("stdin", NULL, "Read new contents of edited file from stdin"): arg_stdin = true; break; /* Compatibility-only options, not shown in --help. */ - case ARG_FAIL: + OPTION_LONG("fail", NULL, /* help= */ NULL): _arg_job_mode = "fail"; break; - case ARG_IRREVERSIBLE: + OPTION_LONG("irreversible", NULL, /* help= */ NULL): _arg_job_mode = "replace-irreversibly"; break; - case ARG_IGNORE_DEPENDENCIES: + OPTION_LONG("ignore-dependencies", NULL, /* help= */ NULL): _arg_job_mode = "ignore-dependencies"; break; - case ARG_NO_LEGEND: + OPTION_LONG("no-legend", NULL, /* help= */ NULL): arg_legend = false; break; - case '.': + OPTION_SHORT_FLAGS(OPTION_OPTIONAL_ARG, '.', "ARG", /* help= */ NULL): /* Output an error mimicking getopt, and print a hint afterwards */ log_error("%s: invalid option -- '.'", program_invocation_name); log_notice("Hint: to specify units starting with a dash, use \"--\":\n" - " %s [OPTIONS...] COMMAND -- -.%s ...", - program_invocation_name, optarg ?: "mount"); - _fallthrough_; - - case '?': + " %s [OPTIONS…] COMMAND -- -.%s …", + program_invocation_name, opts.arg ?: "mount"); return -EINVAL; - - default: - assert_not_reached(); } /* If we are in --user mode, there's no point in talking to PolicyKit or the infra to query system @@ -1142,17 +953,20 @@ static int systemctl_parse_argv(int argc, char *argv[]) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--wait may not be combined with --no-block."); - bool do_reload_or_restart = streq_ptr(argv[optind], "reload-or-restart"); + char **args = option_parser_get_args(&opts); + size_t n_args = option_parser_get_n_args(&opts); + + bool do_reload_or_restart = streq_ptr(args[0], "reload-or-restart"); if (arg_marked) { if (!do_reload_or_restart) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--marked may only be used with 'reload-or-restart'."); - if (optind + 1 < argc) + if (n_args > 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No additional arguments allowed with 'reload-or-restart --marked'."); } else if (do_reload_or_restart) { - if (optind + 1 >= argc) + if (n_args <= 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "List of units to restart/reload is required."); } @@ -1161,10 +975,12 @@ static int systemctl_parse_argv(int argc, char *argv[]) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported."); + if (remaining_args) + *remaining_args = args; return 1; } -int systemctl_dispatch_parse_argv(int argc, char *argv[]) { +int systemctl_dispatch_parse_argv(int argc, char *argv[], char ***remaining_args) { assert(argc >= 0); assert(argv); @@ -1183,8 +999,9 @@ int systemctl_dispatch_parse_argv(int argc, char *argv[]) { } else if (invoked_as(argv, "shutdown")) { arg_action = ACTION_POWEROFF; return shutdown_parse_argv(argc, argv); + } else { + arg_action = ACTION_SYSTEMCTL; + return systemctl_parse_argv(argc, argv, remaining_args); } - arg_action = ACTION_SYSTEMCTL; - return systemctl_parse_argv(argc, argv); } diff --git a/src/systemctl/systemctl.h b/src/systemctl/systemctl.h index bc52e96fe3..3789fbb5ea 100644 --- a/src/systemctl/systemctl.h +++ b/src/systemctl/systemctl.h @@ -102,4 +102,4 @@ static inline const char* arg_job_mode(void) { return _arg_job_mode ?: "replace"; } -int systemctl_dispatch_parse_argv(int argc, char *argv[]); +int systemctl_dispatch_parse_argv(int argc, char *argv[], char ***remaining_args); From 52185521f06e35b62f5df1eed452afbc387c4d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 13 May 2026 22:24:14 +0200 Subject: [PATCH 07/12] systemctl: convert verbs to VERB macros systemctl_main() is moved to systemctl.c to allow fuzz-systemctl-parse-argv to compile. It needs systemctl_help(), which needs the verb table, with the expected groups. Once we provide that, the linker needs all the verb_* functions. So add dummy implementations in fuzz-systemctl-parse-argv to allow the link to happen. The alternative would be to provide an empty option table, but that seems to be more complicated, and also can simulate parsing of the whole command line with the full verb set, so it seems better to test with the real verb table. $ nm build/fuzz-systemctl-parse-argv | rg 0000000000418885 0000000000418885 T verb_add_dependency 0000000000418885 T verb_bind 0000000000418885 T verb_cancel 0000000000418885 T verb_cat 0000000000418885 T verb_clean_or_freeze 0000000000418885 T verb_edit 0000000000418885 T verb_enable 0000000000418885 T verb_get_default 0000000000418885 T verb_import_environment 0000000000418885 T verb_is_active 0000000000418885 T verb_is_enabled 0000000000418885 T verb_is_failed 0000000000418885 T verb_is_system_running 0000000000418885 T verb_kill 0000000000418885 T verb_list_automounts 0000000000418885 T verb_list_dependencies 0000000000418885 T verb_list_jobs 0000000000418885 T verb_list_machines 0000000000418885 T verb_list_paths 0000000000418885 T verb_list_sockets 0000000000418885 T verb_list_timers 0000000000418885 T verb_list_unit_files 0000000000418885 T verb_list_units 0000000000418885 T verb_log_setting 0000000000418885 T verb_mount_image 0000000000418885 t verb_noop 0000000000418885 T verb_preset_all 0000000000418885 T verb_reset_failed 0000000000418885 T verb_service_log_setting 0000000000418885 T verb_service_watchdogs 0000000000418885 T verb_set_default 0000000000418885 T verb_set_environment 0000000000418885 T verb_set_property 0000000000418885 T verb_show 0000000000418885 T verb_show_environment 0000000000418885 T verb_start_special 0000000000418885 T verb_start_system_special 0000000000418885 T verb_switch_root 0000000000418885 T verb_trivial_method 0000000000418885 T verb_whoami --- src/fundamental/macro-fundamental.h | 1 + src/systemctl/fuzz-systemctl-parse-argv.c | 42 +++ src/systemctl/systemctl-main.c | 129 +-------- src/systemctl/systemctl.c | 336 ++++++++++++++-------- src/systemctl/systemctl.h | 1 + 5 files changed, 269 insertions(+), 240 deletions(-) diff --git a/src/fundamental/macro-fundamental.h b/src/fundamental/macro-fundamental.h index 6ed6cf2f8a..29b2533884 100644 --- a/src/fundamental/macro-fundamental.h +++ b/src/fundamental/macro-fundamental.h @@ -72,6 +72,7 @@ #define REENABLE_WARNING \ _Pragma("GCC diagnostic pop") +#define _alias_(x) typeof(x) __attribute__((alias(#x))) #define _align_(x) __attribute__((__aligned__(x))) #define _alignas_(x) __attribute__((__aligned__(alignof(x)))) #define _alignptr_ __attribute__((__aligned__(sizeof(void *)))) diff --git a/src/systemctl/fuzz-systemctl-parse-argv.c b/src/systemctl/fuzz-systemctl-parse-argv.c index 38c0decd94..c57faa6771 100644 --- a/src/systemctl/fuzz-systemctl-parse-argv.c +++ b/src/systemctl/fuzz-systemctl-parse-argv.c @@ -15,6 +15,48 @@ #include "systemctl.h" #include "systemctl-util.h" +static int verb_noop(int argc, char *argv[], uintptr_t _data, void *userdata) { return 0; } +_alias_(verb_noop) + verb_add_dependency, + verb_bind, + verb_cancel, + verb_cat, + verb_clean_or_freeze, + verb_edit, + verb_enable, + verb_get_default, + verb_import_environment, + verb_is_active, + verb_is_enabled, + verb_is_failed, + verb_is_system_running, + verb_kill, + verb_list_automounts, + verb_list_dependencies, + verb_list_jobs, + verb_list_machines, + verb_list_paths, + verb_list_sockets, + verb_list_timers, + verb_list_unit_files, + verb_list_units, + verb_log_setting, + verb_mount_image, + verb_preset_all, + verb_reset_failed, + verb_service_log_setting, + verb_service_watchdogs, + verb_set_default, + verb_set_environment, + verb_set_property, + verb_show, + verb_show_environment, + verb_start_special, + verb_start_system_special, + verb_switch_root, + verb_trivial_method, + verb_whoami; + int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { _cleanup_strv_free_ char **argv = NULL; _cleanup_close_ int orig_stdout_fd = -EBADF; diff --git a/src/systemctl/systemctl-main.c b/src/systemctl/systemctl-main.c index 27f7451911..395874d563 100644 --- a/src/systemctl/systemctl-main.c +++ b/src/systemctl/systemctl-main.c @@ -11,139 +11,12 @@ #include "mount-util.h" #include "stat-util.h" #include "strv.h" -#include "systemctl.h" -#include "systemctl-add-dependency.h" -#include "systemctl-cancel-job.h" -#include "systemctl-clean-or-freeze.h" #include "systemctl-compat-halt.h" -#include "systemctl-daemon-reload.h" -#include "systemctl-edit.h" -#include "systemctl-enable.h" -#include "systemctl-is-active.h" -#include "systemctl-is-enabled.h" -#include "systemctl-is-system-running.h" -#include "systemctl-kill.h" -#include "systemctl-list-dependencies.h" -#include "systemctl-list-jobs.h" -#include "systemctl-list-machines.h" -#include "systemctl-list-unit-files.h" -#include "systemctl-list-units.h" -#include "systemctl-log-setting.h" #include "systemctl-logind.h" -#include "systemctl-mount.h" -#include "systemctl-preset-all.h" -#include "systemctl-reset-failed.h" -#include "systemctl-service-watchdogs.h" -#include "systemctl-set-default.h" -#include "systemctl-set-environment.h" -#include "systemctl-set-property.h" -#include "systemctl-show.h" -#include "systemctl-start-special.h" -#include "systemctl-start-unit.h" -#include "systemctl-switch-root.h" -#include "systemctl-trivial-method.h" #include "systemctl-util.h" -#include "systemctl-whoami.h" -#include "verbs.h" +#include "systemctl.h" #include "virt.h" -static int systemctl_main(char **args) { - static const Verb verbs[] = { - { "list-units", VERB_ANY, VERB_ANY, VERB_DEFAULT|VERB_ONLINE_ONLY, verb_list_units }, - { "list-unit-files", VERB_ANY, VERB_ANY, 0, verb_list_unit_files }, - { "list-automounts", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_list_automounts }, - { "list-paths", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_list_paths }, - { "list-sockets", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_list_sockets }, - { "list-timers", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_list_timers }, - { "list-jobs", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_list_jobs }, - { "list-machines", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_list_machines }, - { "clear-jobs", VERB_ANY, 1, VERB_ONLINE_ONLY, verb_trivial_method }, - { "cancel", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_cancel }, - { "start", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_start }, - { "stop", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_start }, - { "condstop", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_start }, /* For compatibility with ALTLinux */ - { "reload", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_start }, - { "restart", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_start }, - { "try-restart", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_start }, - { "enqueue-marked", 1, 1, VERB_ONLINE_ONLY, verb_start }, - { "reload-or-restart", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_start }, - { "reload-or-try-restart", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_start }, /* For compatibility with systemctl <= 228 */ - { "try-reload-or-restart", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_start }, - { "force-reload", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_start }, /* For compatibility with SysV */ - { "condreload", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_start }, /* For compatibility with ALTLinux */ - { "condrestart", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_start }, /* For compatibility with RH */ - { "isolate", 2, 2, VERB_ONLINE_ONLY, verb_start }, - { "kill", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_kill }, - { "clean", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_clean_or_freeze }, - { "freeze", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_clean_or_freeze }, - { "thaw", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_clean_or_freeze }, - { "is-active", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_is_active }, - { "check", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_is_active }, /* deprecated alias of is-active */ - { "is-failed", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_is_failed }, - { "show", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_show }, - { "cat", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_cat }, - { "status", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_show }, - { "help", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_show }, - { "daemon-reload", 1, 1, VERB_ONLINE_ONLY, verb_daemon_reload }, - { "daemon-reexec", 1, 1, VERB_ONLINE_ONLY, verb_daemon_reload }, - { "log-level", VERB_ANY, 2, VERB_ONLINE_ONLY, verb_log_setting }, - { "log-target", VERB_ANY, 2, VERB_ONLINE_ONLY, verb_log_setting }, - { "service-log-level", 2, 3, VERB_ONLINE_ONLY, verb_service_log_setting }, - { "service-log-target", 2, 3, VERB_ONLINE_ONLY, verb_service_log_setting }, - { "service-watchdogs", VERB_ANY, 2, VERB_ONLINE_ONLY, verb_service_watchdogs }, - { "show-environment", VERB_ANY, 1, VERB_ONLINE_ONLY, verb_show_environment }, - { "set-environment", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_set_environment }, - { "unset-environment", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_set_environment }, - { "import-environment", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_import_environment }, - { "halt", VERB_ANY, 1, VERB_ONLINE_ONLY, verb_start_system_special }, - { "poweroff", VERB_ANY, 1, VERB_ONLINE_ONLY, verb_start_system_special }, - { "reboot", VERB_ANY, 1, VERB_ONLINE_ONLY, verb_start_system_special }, - { "kexec", VERB_ANY, 1, VERB_ONLINE_ONLY, verb_start_system_special }, - { "soft-reboot", VERB_ANY, 1, VERB_ONLINE_ONLY, verb_start_system_special }, - { "sleep", VERB_ANY, 1, VERB_ONLINE_ONLY, verb_start_system_special }, - { "suspend", VERB_ANY, 1, VERB_ONLINE_ONLY, verb_start_system_special }, - { "hibernate", VERB_ANY, 1, VERB_ONLINE_ONLY, verb_start_system_special }, - { "hybrid-sleep", VERB_ANY, 1, VERB_ONLINE_ONLY, verb_start_system_special }, - { "suspend-then-hibernate",VERB_ANY, 1, VERB_ONLINE_ONLY, verb_start_system_special }, - { "default", VERB_ANY, 1, VERB_ONLINE_ONLY, verb_start_special }, - { "rescue", VERB_ANY, 1, VERB_ONLINE_ONLY, verb_start_system_special }, - { "emergency", VERB_ANY, 1, VERB_ONLINE_ONLY, verb_start_system_special }, - { "exit", VERB_ANY, 2, VERB_ONLINE_ONLY, verb_start_special }, - { "reset-failed", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_reset_failed }, - { "enable", 2, VERB_ANY, 0, verb_enable }, - { "disable", 2, VERB_ANY, 0, verb_enable }, - { "is-enabled", 2, VERB_ANY, 0, verb_is_enabled }, - { "reenable", 2, VERB_ANY, 0, verb_enable }, - { "preset", 2, VERB_ANY, 0, verb_enable }, - { "preset-all", VERB_ANY, 1, 0, verb_preset_all }, - { "mask", 2, VERB_ANY, 0, verb_enable }, - { "unmask", 2, VERB_ANY, 0, verb_enable }, - { "link", 2, VERB_ANY, 0, verb_enable }, - { "revert", 2, VERB_ANY, 0, verb_enable }, - { "switch-root", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_switch_root }, - { "list-dependencies", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_list_dependencies }, - { "set-default", 2, 2, 0, verb_set_default }, - { "get-default", VERB_ANY, 1, 0, verb_get_default }, - { "set-property", 3, VERB_ANY, VERB_ONLINE_ONLY, verb_set_property }, - { "is-system-running", VERB_ANY, 1, 0, verb_is_system_running }, - { "add-wants", 3, VERB_ANY, 0, verb_add_dependency }, - { "add-requires", 3, VERB_ANY, 0, verb_add_dependency }, - { "edit", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_edit }, - { "bind", 3, 4, VERB_ONLINE_ONLY, verb_bind }, - { "mount-image", 4, 5, VERB_ONLINE_ONLY, verb_mount_image }, - { "whoami", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_whoami }, - {} - }; - - const Verb *verb = verbs_find_verb(args[0], verbs, verbs + ELEMENTSOF(verbs) - 1); - if (verb && (verb->flags & VERB_ONLINE_ONLY) && arg_root) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Verb '%s' cannot be used with --root= or --image=.", - args[0] ?: verb->verb); - - return _dispatch_verb_with_args(args, verbs, verbs + ELEMENTSOF(verbs) - 1, NULL); -} - static int run(int argc, char *argv[]) { _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; _cleanup_(umount_and_freep) char *mounted_dir = NULL; diff --git a/src/systemctl/systemctl.c b/src/systemctl/systemctl.c index 8a241495c0..817ee71980 100644 --- a/src/systemctl/systemctl.c +++ b/src/systemctl/systemctl.c @@ -28,6 +28,7 @@ #include "systemctl-compat-shutdown.h" #include "systemctl-logind.h" #include "time-util.h" +#include "verbs.h" char **arg_types = NULL; char **arg_states = NULL; @@ -109,130 +110,47 @@ STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); STATIC_DESTRUCTOR_REGISTER(arg_kill_subgroup, freep); static int systemctl_help(void) { + static const char *const vgroups[] = { + "Unit Commands", + "Unit File Commands", + "Machine Commands", + "Job Commands", + "Environment Commands", + "Manager State Commands", + "System Commands", + }; + + Table *vtables[ELEMENTSOF(vgroups)] = {}; + CLEANUP_ELEMENTS(vtables, table_unref_array_clear); _cleanup_(table_unrefp) Table *options_table = NULL; 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; + } + r = option_parser_get_help_table_ns("systemctl", &options_table); if (r < 0) return r; + assert_cc(ELEMENTSOF(vtables) == 7); + (void) table_sync_column_widths(0, options_table, + vtables[0], vtables[1], vtables[2], vtables[3], + vtables[4], vtables[5], vtables[6]); + pager_open(arg_pager_flags); help_cmdline("[OPTIONS…] COMMAND …"); help_abstract("Query or send control commands to the system manager."); - help_section("Unit Commands"); - printf(" list-units [PATTERN...] List units currently in memory\n" - " list-automounts [PATTERN...] List automount units currently in memory,\n" - " ordered by path\n" - " list-paths [PATTERN...] List path units currently in memory,\n" - " ordered by path\n" - " list-sockets [PATTERN...] List socket units currently in memory,\n" - " ordered by address\n" - " list-timers [PATTERN...] List timer units currently in memory,\n" - " ordered by next elapse\n" - " is-active PATTERN... Check whether units are active\n" - " is-failed [PATTERN...] Check whether units are failed or\n" - " system is in degraded state\n" - " status [PATTERN...|PID...] Show runtime status of one or more units\n" - " show [PATTERN...|JOB...] Show properties of one or more\n" - " units/jobs or the manager\n" - " cat PATTERN... Show files and drop-ins of specified units\n" - " help PATTERN...|PID... Show manual for one or more units\n" - " list-dependencies [UNIT...] Recursively show units which are required\n" - " or wanted by the units or by which those\n" - " units are required or wanted\n" - " start UNIT... Start (activate) one or more units\n" - " stop UNIT... Stop (deactivate) one or more units\n" - " reload UNIT... Reload one or more units\n" - " restart UNIT... Start or restart one or more units\n" - " try-restart UNIT... Restart one or more units if active\n" - " enqueue-marked Enqueue jobs for all marked units\n" - " reload-or-restart UNIT... Reload one or more units if possible,\n" - " otherwise start or restart\n" - " try-reload-or-restart UNIT... If active, reload one or more units,\n" - " if supported, otherwise restart\n" - " isolate UNIT Start one unit and stop all others\n" - " kill UNIT... Send signal to processes of a unit\n" - " clean UNIT... Clean runtime, cache, state, logs or\n" - " configuration of unit\n" - " freeze PATTERN... Freeze execution of unit processes\n" - " thaw PATTERN... Resume execution of a frozen unit\n" - " set-property UNIT PROPERTY=VALUE... Sets one or more properties of a unit\n" - " bind UNIT PATH [PATH] Bind-mount a path from the host into a\n" - " unit's namespace\n" - " mount-image UNIT PATH [PATH [OPTS]] Mount an image from the host into a\n" - " unit's namespace\n" - " service-log-level SERVICE [LEVEL] Get/set logging threshold for service\n" - " service-log-target SERVICE [TARGET] Get/set logging target for service\n" - " reset-failed [PATTERN...] Reset failed state for all, one, or more\n" - " units\n" - " whoami [PID...] Return unit caller or specified PIDs are\n" - " part of\n"); - - help_section("Unit File Commands"); - printf(" list-unit-files [PATTERN...] List installed unit files\n" - " enable [UNIT...|PATH...] Enable one or more unit files\n" - " disable UNIT... Disable one or more unit files\n" - " reenable UNIT... Reenable one or more unit files\n" - " preset UNIT... Enable/disable one or more unit files\n" - " based on preset configuration\n" - " preset-all Enable/disable all unit files based on\n" - " preset configuration\n" - " is-enabled UNIT... Check whether unit files are enabled\n" - " mask UNIT... Mask one or more units\n" - " unmask UNIT... Unmask one or more units\n" - " link PATH... Link one or more units files into\n" - " the search path\n" - " revert UNIT... Revert one or more unit files to vendor\n" - " version\n" - " add-wants TARGET UNIT... Add 'Wants' dependency for the target\n" - " on specified one or more units\n" - " add-requires TARGET UNIT... Add 'Requires' dependency for the target\n" - " on specified one or more units\n" - " edit UNIT... Edit one or more unit files\n" - " get-default Get the name of the default target\n" - " set-default TARGET Set the default target\n"); - - help_section("Machine Commands"); - printf(" list-machines [PATTERN...] List local containers and host\n"); - - help_section("Job Commands"); - printf(" list-jobs [PATTERN...] List jobs\n" - " cancel [JOB...] Cancel all, one, or more jobs\n"); - - help_section("Environment Commands"); - printf(" show-environment Dump environment\n" - " set-environment VARIABLE=VALUE... Set one or more environment variables\n" - " unset-environment VARIABLE... Unset one or more environment variables\n" - " import-environment VARIABLE... Import all or some environment variables\n"); - - help_section("Manager State Commands"); - printf(" daemon-reload Reload systemd manager configuration\n" - " daemon-reexec Reexecute systemd manager\n" - " log-level [LEVEL] Get/set logging threshold for manager\n" - " log-target [TARGET] Get/set logging target for manager\n" - " service-watchdogs [BOOL] Get/set service watchdog state\n"); - - help_section("System Commands"); - printf(" is-system-running Check whether system is fully running\n" - " default Enter system default mode\n" - " rescue Enter system rescue mode\n" - " emergency Enter system emergency mode\n" - " halt Shut down and halt the system\n" - " poweroff Shut down and power-off the system\n" - " reboot Shut down and reboot the system\n" - " kexec Shut down and reboot the system with kexec\n" - " soft-reboot Shut down and reboot userspace\n" - " exit [EXIT_CODE] Request user instance or container exit\n" - " switch-root [ROOT [INIT]] Change to a different root file system\n" - " sleep Put the system to sleep (through one of\n" - " the operations below)\n" - " suspend Suspend the system\n" - " hibernate Hibernate the system\n" - " hybrid-sleep Hibernate and suspend the system\n" - " suspend-then-hibernate Suspend the system, wake after a period of\n" - " time, and hibernate\n"); + 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_table); @@ -1003,5 +921,199 @@ int systemctl_dispatch_parse_argv(int argc, char *argv[], char ***remaining_args arg_action = ACTION_SYSTEMCTL; return systemctl_parse_argv(argc, argv, remaining_args); } +} +VERB_GROUP("Unit Commands"); + +VERB_SCOPE(, verb_list_units, "list-units", "[PATTERN…]", VERB_ANY, VERB_ANY, VERB_DEFAULT|VERB_ONLINE_ONLY, + "List units currently in memory"); +VERB_SCOPE(, verb_list_automounts, "list-automounts", "[PATTERN…]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "List automount units currently in memory, ordered by path"); +VERB_SCOPE(, verb_list_paths, "list-paths", "[PATTERN…]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "List path units currently in memory, ordered by path"); +VERB_SCOPE(, verb_list_sockets, "list-sockets", "[PATTERN…]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "List socket units currently in memory, ordered by address"); +VERB_SCOPE(, verb_list_timers, "list-timers", "[PATTERN…]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "List timer units currently in memory, ordered by next elapse"); +VERB_SCOPE(, verb_is_active, "is-active", "PATTERN…", 2, VERB_ANY, VERB_ONLINE_ONLY, + "Check whether units are active"); +VERB_SCOPE(, verb_is_failed, "is-failed", "[PATTERN…]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "Check whether units are failed or system is in degraded state"); +VERB_SCOPE(, verb_show, "status", "[PATTERN…|PID…]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "Show runtime status of one or more units"); +VERB_SCOPE(, verb_show, "show", "[PATTERN…|JOB…]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "Show properties of one or more units/jobs or the manager"); +VERB_SCOPE(, verb_cat, "cat", "PATTERN…", 2, VERB_ANY, VERB_ONLINE_ONLY, + "Show files and drop-ins of specified units"); +VERB_SCOPE(, verb_show, "help", "PATTERN…|PID…", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "Show manual for one or more units"); +VERB_SCOPE(, verb_list_dependencies, "list-dependencies", "[UNIT…]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "Recursively show units which are required or wanted by the units " + "or by which those units are required or wanted"); +VERB_SCOPE(, verb_start, "start", "UNIT…", 2, VERB_ANY, VERB_ONLINE_ONLY, + "Start (activate) one or more units"); +VERB_SCOPE(, verb_start, "stop", "UNIT…", 2, VERB_ANY, VERB_ONLINE_ONLY, + "Stop (deactivate) one or more units"); +VERB_SCOPE(, verb_start, "reload", "UNIT…", 2, VERB_ANY, VERB_ONLINE_ONLY, + "Reload one or more units"); +VERB_SCOPE(, verb_start, "restart", "UNIT…", 2, VERB_ANY, VERB_ONLINE_ONLY, + "Start or restart one or more units"); +VERB_SCOPE(, verb_start, "try-restart", "UNIT…", 2, VERB_ANY, VERB_ONLINE_ONLY, + "Restart one or more units if active"); +VERB_SCOPE(, verb_start, "enqueue-marked", NULL, 1, 1, VERB_ONLINE_ONLY, + "Enqueue jobs for all marked units"); +VERB_SCOPE(, verb_start, "reload-or-restart", "UNIT…", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "Reload one or more units if possible, otherwise start or restart"); +VERB_SCOPE(, verb_start, "try-reload-or-restart", "UNIT…", 2, VERB_ANY, VERB_ONLINE_ONLY, + "If active, reload one or more units, if supported, otherwise restart"); +VERB_SCOPE(, verb_start, "isolate", "UNIT", 2, 2, VERB_ONLINE_ONLY, + "Start one unit and stop all others"); +VERB_SCOPE(, verb_kill, "kill", "UNIT…", 2, VERB_ANY, VERB_ONLINE_ONLY, + "Send signal to processes of a unit"); +VERB_SCOPE(, verb_clean_or_freeze, "clean", "UNIT…", 2, VERB_ANY, VERB_ONLINE_ONLY, + "Clean runtime, cache, state, logs or configuration of unit"); +VERB_SCOPE(, verb_clean_or_freeze, "freeze", "PATTERN…", 2, VERB_ANY, VERB_ONLINE_ONLY, + "Freeze execution of unit processes"); +VERB_SCOPE(, verb_clean_or_freeze, "thaw", "PATTERN…", 2, VERB_ANY, VERB_ONLINE_ONLY, + "Resume execution of a frozen unit"); +VERB_SCOPE(, verb_set_property, "set-property", "UNIT PROPERTY=VALUE…", 3, VERB_ANY, VERB_ONLINE_ONLY, + "Sets one or more properties of a unit"); +VERB_SCOPE(, verb_bind, "bind", "UNIT PATH [PATH]", 3, 4, VERB_ONLINE_ONLY, + "Bind-mount a path from the host into a unit's namespace"); +VERB_SCOPE(, verb_mount_image, "mount-image", "UNIT PATH [PATH [OPTS]]", 4, 5, VERB_ONLINE_ONLY, + "Mount an image from the host into a unit's namespace"); +VERB_SCOPE(, verb_service_log_setting, "service-log-level", "SERVICE [LEVEL]", 2, 3, VERB_ONLINE_ONLY, + "Get/set logging threshold for service"); +VERB_SCOPE(, verb_service_log_setting, "service-log-target", "SERVICE [TARGET]", 2, 3, VERB_ONLINE_ONLY, + "Get/set logging target for service"); +VERB_SCOPE(, verb_reset_failed, "reset-failed", "[PATTERN…]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "Reset failed state for all, one, or more units"); +VERB_SCOPE(, verb_whoami, "whoami", "[PID…]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "Return unit caller or specified PIDs are part of"); + +VERB_GROUP("Unit File Commands"); + +VERB_SCOPE(, verb_list_unit_files, "list-unit-files", "[PATTERN…]", VERB_ANY, VERB_ANY, 0, + "List installed unit files"); +VERB_SCOPE(, verb_enable, "enable", "[UNIT…|PATH…]", 2, VERB_ANY, 0, + "Enable one or more unit files"); +VERB_SCOPE(, verb_enable, "disable", "UNIT…", 2, VERB_ANY, 0, + "Disable one or more unit files"); +VERB_SCOPE(, verb_enable, "reenable", "UNIT…", 2, VERB_ANY, 0, + "Reenable one or more unit files"); +VERB_SCOPE(, verb_enable, "preset", "UNIT…", 2, VERB_ANY, 0, + "Enable/disable one or more unit files based on preset configuration"); +VERB_SCOPE(, verb_preset_all, "preset-all", NULL, VERB_ANY, 1, 0, + "Enable/disable all unit files based on preset configuration"); +VERB_SCOPE(, verb_is_enabled, "is-enabled", "UNIT…", 2, VERB_ANY, 0, + "Check whether unit files are enabled"); +VERB_SCOPE(, verb_enable, "mask", "UNIT…", 2, VERB_ANY, 0, + "Mask one or more units"); +VERB_SCOPE(, verb_enable, "unmask", "UNIT…", 2, VERB_ANY, 0, + "Unmask one or more units"); +VERB_SCOPE(, verb_enable, "link", "PATH…", 2, VERB_ANY, 0, + "Link one or more units files into the search path"); +VERB_SCOPE(, verb_enable, "revert", "UNIT…", 2, VERB_ANY, 0, + "Revert one or more unit files to vendor version"); +VERB_SCOPE(, verb_add_dependency, "add-wants", "TARGET UNIT…", 3, VERB_ANY, 0, + "Add 'Wants' dependency for the target on specified one or more units"); +VERB_SCOPE(, verb_add_dependency, "add-requires", "TARGET UNIT…", 3, VERB_ANY, 0, + "Add 'Requires' dependency for the target on specified one or more units"); +VERB_SCOPE(, verb_edit, "edit", "UNIT…", 2, VERB_ANY, VERB_ONLINE_ONLY, + "Edit one or more unit files"); +VERB_SCOPE(, verb_get_default, "get-default", NULL, VERB_ANY, 1, 0, + "Get the name of the default target"); +VERB_SCOPE(, verb_set_default, "set-default", "TARGET", 2, 2, 0, + "Set the default target"); + +VERB_GROUP("Machine Commands"); + +VERB_SCOPE(, verb_list_machines, "list-machines", "[PATTERN…]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "List local containers and host"); + +VERB_GROUP("Job Commands"); + +VERB_SCOPE(, verb_list_jobs, "list-jobs", "[PATTERN…]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "List jobs"); +VERB_SCOPE(, verb_cancel, "cancel", "[JOB…]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "Cancel all, one, or more jobs"); + +VERB_GROUP("Environment Commands"); + +VERB_SCOPE(, verb_show_environment, "show-environment", NULL, VERB_ANY, 1, VERB_ONLINE_ONLY, + "Dump environment"); +VERB_SCOPE(, verb_set_environment, "set-environment", "VARIABLE=VALUE…", 2, VERB_ANY, VERB_ONLINE_ONLY, + "Set one or more environment variables"); +VERB_SCOPE(, verb_set_environment, "unset-environment", "VARIABLE…", 2, VERB_ANY, VERB_ONLINE_ONLY, + "Unset one or more environment variables"); +VERB_SCOPE(, verb_import_environment, "import-environment", "VARIABLE…", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "Import all or some environment variables"); + +VERB_GROUP("Manager State Commands"); + +VERB_SCOPE(, verb_daemon_reload, "daemon-reload", NULL, 1, 1, VERB_ONLINE_ONLY, + "Reload systemd manager configuration"); +VERB_SCOPE(, verb_daemon_reload, "daemon-reexec", NULL, 1, 1, VERB_ONLINE_ONLY, + "Reexecute systemd manager"); +VERB_SCOPE(, verb_log_setting, "log-level", "[LEVEL]", VERB_ANY, 2, VERB_ONLINE_ONLY, + "Get/set logging threshold for manager"); +VERB_SCOPE(, verb_log_setting, "log-target", "[TARGET]", VERB_ANY, 2, VERB_ONLINE_ONLY, + "Get/set logging target for manager"); +VERB_SCOPE(, verb_service_watchdogs, "service-watchdogs", "[BOOL]", VERB_ANY, 2, VERB_ONLINE_ONLY, + "Get/set service watchdog state"); + +VERB_GROUP("System Commands"); + +VERB_SCOPE(, verb_is_system_running, "is-system-running", NULL, VERB_ANY, 1, 0, + "Check whether system is fully running"); +VERB_SCOPE(, verb_start_special, "default", NULL, VERB_ANY, 1, VERB_ONLINE_ONLY, + "Enter system default mode"); +VERB_SCOPE(, verb_start_system_special, "rescue", NULL, VERB_ANY, 1, VERB_ONLINE_ONLY, + "Enter system rescue mode"); +VERB_SCOPE(, verb_start_system_special, "emergency", NULL, VERB_ANY, 1, VERB_ONLINE_ONLY, + "Enter system emergency mode"); +VERB_SCOPE(, verb_start_system_special, "halt", NULL, VERB_ANY, 1, VERB_ONLINE_ONLY, + "Shut down and halt the system"); +VERB_SCOPE(, verb_start_system_special, "poweroff", NULL, VERB_ANY, 1, VERB_ONLINE_ONLY, + "Shut down and power-off the system"); +VERB_SCOPE(, verb_start_system_special, "reboot", NULL, VERB_ANY, 1, VERB_ONLINE_ONLY, + "Shut down and reboot the system"); +VERB_SCOPE(, verb_start_system_special, "kexec", NULL, VERB_ANY, 1, VERB_ONLINE_ONLY, + "Shut down and reboot the system with kexec"); +VERB_SCOPE(, verb_start_system_special, "soft-reboot", NULL, VERB_ANY, 1, VERB_ONLINE_ONLY, + "Shut down and reboot userspace"); +VERB_SCOPE(, verb_start_special, "exit", "[EXIT_CODE]", VERB_ANY, 2, VERB_ONLINE_ONLY, + "Request user instance or container exit"); +VERB_SCOPE(, verb_switch_root, "switch-root", "[ROOT [INIT]]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, + "Change to a different root file system"); +VERB_SCOPE(, verb_start_system_special, "sleep", NULL, VERB_ANY, 1, VERB_ONLINE_ONLY, + "Put the system to sleep (through one of the operations below)"); +VERB_SCOPE(, verb_start_system_special, "suspend", NULL, VERB_ANY, 1, VERB_ONLINE_ONLY, + "Suspend the system"); +VERB_SCOPE(, verb_start_system_special, "hibernate", NULL, VERB_ANY, 1, VERB_ONLINE_ONLY, + "Hibernate the system"); +VERB_SCOPE(, verb_start_system_special, "hybrid-sleep", NULL, VERB_ANY, 1, VERB_ONLINE_ONLY, + "Hibernate and suspend the system"); +VERB_SCOPE(, verb_start_system_special, "suspend-then-hibernate", NULL, VERB_ANY, 1, VERB_ONLINE_ONLY, + "Suspend the system, wake after a period of time, and hibernate"); + +/* Compatibility aliases / deprecated verbs hidden from --help. */ +VERB_SCOPE(, verb_trivial_method, "clear-jobs", NULL, VERB_ANY, 1, VERB_ONLINE_ONLY, /* help= */ NULL); /* systemctl < 4 */ +VERB_SCOPE(, verb_start, "condstop", NULL, 2, VERB_ANY, VERB_ONLINE_ONLY, /* help= */ NULL); /* ALTLinux */ +VERB_SCOPE(, verb_start, "reload-or-try-restart", NULL, 2, VERB_ANY, VERB_ONLINE_ONLY, /* help= */ NULL); /* systemctl <= 228 */ +VERB_SCOPE(, verb_start, "force-reload", NULL, 2, VERB_ANY, VERB_ONLINE_ONLY, /* help= */ NULL); /* SysV */ +VERB_SCOPE(, verb_start, "condreload", NULL, 2, VERB_ANY, VERB_ONLINE_ONLY, /* help= */ NULL); /* ALTLinux */ +VERB_SCOPE(, verb_start, "condrestart", NULL, 2, VERB_ANY, VERB_ONLINE_ONLY, /* help= */ NULL); /* RH */ +VERB_SCOPE(, verb_is_active, "check", NULL, 2, VERB_ANY, VERB_ONLINE_ONLY, /* help= */ NULL); /* deprecated alias of is-active */ + +int systemctl_main(char **args) { + const Verb *verb = verbs_find_verb(args[0], + ALIGN_PTR(__start_SYSTEMD_VERBS), + __stop_SYSTEMD_VERBS); + if (verb && (verb->flags & VERB_ONLINE_ONLY) && arg_root) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Verb '%s' cannot be used with --root= or --image=.", + args[0] ?: verb->verb); + + return dispatch_verb_with_args(args, NULL); } diff --git a/src/systemctl/systemctl.h b/src/systemctl/systemctl.h index 3789fbb5ea..58ad1e5876 100644 --- a/src/systemctl/systemctl.h +++ b/src/systemctl/systemctl.h @@ -103,3 +103,4 @@ static inline const char* arg_job_mode(void) { } int systemctl_dispatch_parse_argv(int argc, char *argv[], char ***remaining_args); +int systemctl_main(char **args); From 45828705d6718e14ad85abd08fabb132da19d9ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 13 May 2026 22:56:25 +0200 Subject: [PATCH 08/12] systemctl: convert halt_parse_argv to OPTION macros Co-developed-by: Claude Opus 4.7 --- src/systemctl/systemctl-compat-halt.c | 125 +++++++++++--------------- 1 file changed, 51 insertions(+), 74 deletions(-) diff --git a/src/systemctl/systemctl-compat-halt.c b/src/systemctl/systemctl-compat-halt.c index cebdfb2413..3f19a263ff 100644 --- a/src/systemctl/systemctl-compat-halt.c +++ b/src/systemctl/systemctl-compat-halt.c @@ -1,12 +1,12 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include - #include "sd-daemon.h" -#include "alloc-util.h" +#include "ansi-color.h" +#include "format-table.h" +#include "help-util.h" #include "log.h" -#include "pretty-print.h" +#include "options.h" #include "process-util.h" #include "reboot-util.h" #include "systemctl.h" @@ -17,126 +17,103 @@ #include "utmp-wtmp.h" static int halt_help(void) { - _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *options = NULL; int r; - r = terminal_urlify_man("halt", "8", &link); + r = option_parser_get_help_table_ns("halt", &options); if (r < 0) - return log_oom(); + return r; /* Note: if you are tempted to add new command line switches here, please do not. Let this * compatibility command rest in peace. Its interface is not even owned by us as much as it is by * sysvinit. If you add something new, add it to "systemctl halt", "systemctl reboot", "systemctl * poweroff" instead. */ - printf("%s [OPTIONS...]%s\n" - "\n%s%s the system.%s\n" - "\nOptions:\n" - " --help Show this help\n" - " --halt Halt the machine\n" - " -p --poweroff Switch off the machine\n" - " --reboot Reboot the machine\n" - " -f --force Force immediate halt/power-off/reboot\n" - " -w --wtmp-only Don't halt/power-off/reboot, just write wtmp record\n" - " -d --no-wtmp Don't write wtmp record\n" - " --no-wall Don't send wall message before halt/power-off/reboot\n" - "\n%sThis is a compatibility interface, please use the more powerful 'systemctl %s' command instead.%s\n" - "\nSee the %s for details.\n", - program_invocation_short_name, - arg_action == ACTION_REBOOT ? " [ARG]" : "", - ansi_highlight(), arg_action == ACTION_REBOOT ? "Reboot" : - arg_action == ACTION_POWEROFF ? "Power off" : - "Halt", ansi_normal(), - ansi_highlight_red(), arg_action == ACTION_REBOOT ? "reboot" : - arg_action == ACTION_POWEROFF ? "poweroff" : - "halt", ansi_normal(), - link); + help_cmdline(arg_action == ACTION_REBOOT ? "[OPTIONS…] [ARG]" : "[OPTIONS…]"); + help_abstract(arg_action == ACTION_REBOOT ? "Reboot the system." : + arg_action == ACTION_POWEROFF ? "Power off the system." : + "Halt the system."); + help_section("Options"); + r = table_print_or_warn(options); + if (r < 0) + return r; + + printf("\n%sThis is a compatibility interface, please use the more powerful 'systemctl %s' command instead.%s\n", + ansi_highlight_red(), + arg_action == ACTION_REBOOT ? "reboot" : + arg_action == ACTION_POWEROFF ? "poweroff" : + "halt", + ansi_normal()); + + help_man_page_reference("halt", "8"); return 0; } int halt_parse_argv(int argc, char *argv[]) { - enum { - ARG_HELP = 0x100, - ARG_HALT, - ARG_REBOOT, - ARG_NO_WALL - }; - - static const struct option options[] = { - { "help", no_argument, NULL, ARG_HELP }, - { "halt", no_argument, NULL, ARG_HALT }, - { "poweroff", no_argument, NULL, 'p' }, - { "reboot", no_argument, NULL, ARG_REBOOT }, - { "force", no_argument, NULL, 'f' }, - { "wtmp-only", no_argument, NULL, 'w' }, - { "no-wtmp", no_argument, NULL, 'd' }, - { "no-sync", no_argument, NULL, 'n' }, - { "no-wall", no_argument, NULL, ARG_NO_WALL }, - {} - }; - - int c, r; + int r; assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "pfwdnih", options, NULL)) >= 0) + OptionParser opts = { argc, argv, .namespace = "halt" }; + + FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { - case ARG_HELP: + OPTION_NAMESPACE("halt"): {} + + OPTION_LONG("help", NULL, "Show this help"): return halt_help(); - case ARG_HALT: + OPTION_LONG("halt", NULL, "Halt the machine"): arg_action = ACTION_HALT; break; - case 'p': + OPTION('p', "poweroff", NULL, "Switch off the machine"): if (arg_action != ACTION_REBOOT) arg_action = ACTION_POWEROFF; break; - case ARG_REBOOT: + OPTION_LONG("reboot", NULL, "Reboot the machine"): arg_action = ACTION_REBOOT; break; - case 'f': + OPTION('f', "force", NULL, "Force immediate halt/power-off/reboot"): arg_force = 2; break; - case 'w': + OPTION('w', "wtmp-only", NULL, "Don't halt/power-off/reboot, just write wtmp record"): arg_dry_run = true; break; - case 'd': + OPTION('d', "no-wtmp", NULL, "Don't write wtmp record"): arg_no_wtmp = true; break; - case 'n': - arg_no_sync = true; - break; - - case ARG_NO_WALL: + OPTION_LONG("no-wall", NULL, "Don't send wall message before halt/power-off/reboot"): arg_no_wall = true; break; - case 'i': - case 'h': - /* Compatibility nops */ + /* Hidden compat-only options. */ + OPTION('n', "no-sync", NULL, /* help= */ NULL): + arg_no_sync = true; break; - case '?': - return -EINVAL; - - default: - assert_not_reached(); + OPTION_SHORT('i', NULL, /* help= */ NULL): {} + OPTION_SHORT('h', NULL, /* help= */ NULL): + /* Compatibility nops */ + break; } - if (arg_action == ACTION_REBOOT && (argc == optind || argc == optind + 1)) { - r = update_reboot_parameter_and_warn(argc == optind + 1 ? argv[optind] : NULL, false); + char **args = option_parser_get_args(&opts); + size_t n_args = option_parser_get_n_args(&opts); + + if (arg_action == ACTION_REBOOT && n_args <= 1) { + r = update_reboot_parameter_and_warn(args[0], false); if (r < 0) return r; - } else if (optind < argc) + } else if (n_args > 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too many arguments."); From 23ba690cca95e9f0226d55f9a0fbf42decb256c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 13 May 2026 23:02:48 +0200 Subject: [PATCH 09/12] systemctl: convert shutdown_parse_argv to OPTION macros Co-developed-by: Claude Opus 4.7 --- src/systemctl/systemctl-compat-shutdown.c | 137 +++++++++------------- 1 file changed, 55 insertions(+), 82 deletions(-) diff --git a/src/systemctl/systemctl-compat-shutdown.c b/src/systemctl/systemctl-compat-shutdown.c index cfc917073a..f4dbd80473 100644 --- a/src/systemctl/systemctl-compat-shutdown.c +++ b/src/systemctl/systemctl-compat-shutdown.c @@ -1,11 +1,11 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include - -#include "alloc-util.h" +#include "ansi-color.h" +#include "format-table.h" +#include "help-util.h" #include "log.h" +#include "options.h" #include "parse-util.h" -#include "pretty-print.h" #include "reboot-util.h" #include "string-util.h" #include "strv.h" @@ -14,38 +14,31 @@ #include "time-util.h" static int shutdown_help(void) { - _cleanup_free_ char *link = NULL; + _cleanup_(table_unrefp) Table *options = NULL; int r; - r = terminal_urlify_man("shutdown", "8", &link); + r = option_parser_get_help_table_ns("shutdown", &options); if (r < 0) - return log_oom(); + return r; /* Note: if you are tempted to add new command line switches here, please do not. Let this * compatibility command rest in peace. Its interface is not even owned by us as much as it is by * sysvinit. If you add something new, add it to "systemctl halt", "systemctl reboot", "systemctl * poweroff" instead. */ - printf("%s [OPTIONS...] [TIME] [WALL...]\n" - "\n%sShut down the system.%s\n" - "\nOptions:\n" - " --help Show this help\n" - " -H --halt Halt the machine\n" - " -P --poweroff Power-off the machine\n" - " -r --reboot Reboot the machine\n" - " -h Equivalent to --poweroff, overridden by --halt\n" - " -k Don't halt/power-off/reboot, just send warnings\n" - " --no-wall Don't send wall message before halt/power-off/reboot\n" - " -c Cancel a pending shutdown\n" - " --show Show pending shutdown\n" - "\n%sThis is a compatibility interface, please use the more powerful 'systemctl halt',\n" - "'systemctl poweroff', 'systemctl reboot' commands instead.%s\n" - "\nSee the %s for details.\n", - program_invocation_short_name, - ansi_highlight(), ansi_normal(), - ansi_highlight_red(), ansi_normal(), - link); + help_cmdline("[OPTIONS…] [TIME] [WALL…]"); + help_abstract("Shut down the system."); + help_section("Options"); + r = table_print_or_warn(options); + if (r < 0) + return r; + + printf("\n%sThis is a compatibility interface, please use the more powerful 'systemctl halt',\n" + "'systemctl poweroff', 'systemctl reboot' commands instead.%s\n", + ansi_highlight_red(), ansi_normal()); + + help_man_page_reference("shutdown", "8"); return 0; } @@ -124,104 +117,86 @@ static int parse_shutdown_time_spec(const char *t, usec_t *ret) { } int shutdown_parse_argv(int argc, char *argv[]) { - enum { - ARG_HELP = 0x100, - ARG_NO_WALL, - ARG_SHOW - }; - - static const struct option options[] = { - { "help", no_argument, NULL, ARG_HELP }, - { "halt", no_argument, NULL, 'H' }, - { "poweroff", no_argument, NULL, 'P' }, - { "reboot", no_argument, NULL, 'r' }, - { "kexec", no_argument, NULL, 'K' }, /* not documented extension */ - { "no-wall", no_argument, NULL, ARG_NO_WALL }, - { "show", no_argument, NULL, ARG_SHOW }, - {} - }; - - char **wall = NULL; - int c, r; + int r; assert(argc >= 0); assert(argv); - while ((c = getopt_long(argc, argv, "HPrhkKat:fFc", options, NULL)) >= 0) + OptionParser opts = { argc, argv, .namespace = "shutdown" }; + + FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { - case ARG_HELP: + OPTION_NAMESPACE("shutdown"): {} + + OPTION_LONG("help", NULL, "Show this help"): return shutdown_help(); - case 'H': + OPTION('H', "halt", NULL, "Halt the machine"): arg_action = ACTION_HALT; break; - case 'P': + OPTION('P', "poweroff", NULL, "Power-off the machine"): arg_action = ACTION_POWEROFF; break; - case 'r': + OPTION('r', "reboot", NULL, "Reboot the machine"): if (kexec_loaded()) arg_action = ACTION_KEXEC; else arg_action = ACTION_REBOOT; break; - case 'K': - arg_action = ACTION_KEXEC; - break; - - case 'h': + OPTION_SHORT('h', NULL, "Equivalent to --poweroff, overridden by --halt"): if (arg_action != ACTION_HALT) arg_action = ACTION_POWEROFF; break; - case 'k': + OPTION_SHORT('k', NULL, "Don't halt/power-off/reboot, just send warnings"): arg_dry_run = true; break; - case ARG_NO_WALL: + OPTION_LONG("no-wall", NULL, "Don't send wall message before halt/power-off/reboot"): arg_no_wall = true; break; - case 'a': - case 't': /* Note that we also ignore any passed argument to -t, not just the -t itself */ - case 'f': - case 'F': - /* Compatibility nops */ - break; - - case 'c': + OPTION_SHORT('c', NULL, "Cancel a pending shutdown"): arg_action = ACTION_CANCEL_SHUTDOWN; break; - case ARG_SHOW: + OPTION_LONG("show", NULL, "Show pending shutdown"): arg_action = ACTION_SHOW_SHUTDOWN; break; - case '?': - return -EINVAL; + /* Hidden compat options. */ + OPTION('K', "kexec", NULL, /* help= */ NULL): + arg_action = ACTION_KEXEC; + break; - default: - assert_not_reached(); + OPTION_SHORT('a', NULL, /* help= */ NULL): {} /* compatibility noops */ + OPTION_SHORT('f', NULL, /* help= */ NULL): {} + OPTION_SHORT('F', NULL, /* help= */ NULL): {} + OPTION_SHORT('t', "ARG", /* help= */ NULL): + break; } - if (argc > optind && arg_action != ACTION_CANCEL_SHUTDOWN) { - r = parse_shutdown_time_spec(argv[optind], &arg_when); - if (r < 0) { - log_error("Failed to parse time specification: %s", argv[optind]); - return r; - } + char **args = option_parser_get_args(&opts); + size_t n_args = option_parser_get_n_args(&opts); + + if (n_args > 0 && arg_action != ACTION_CANCEL_SHUTDOWN) { + r = parse_shutdown_time_spec(args[0], &arg_when); + if (r < 0) + return log_error_errno(r, "Failed to parse time specification: %s", args[0]); } else arg_when = USEC_INFINITY; /* logind chooses on server side */ - if (argc > optind && arg_action == ACTION_CANCEL_SHUTDOWN) + char **wall = NULL; + if (n_args > 0 && arg_action == ACTION_CANCEL_SHUTDOWN) /* No time argument for shutdown cancel */ - wall = argv + optind; - else if (argc > optind + 1) + wall = args; + else if (n_args > 1) /* We skip the time argument */ - wall = argv + optind + 1; + wall = args + 1; if (wall) { char **copy = strv_copy(wall); @@ -230,7 +205,5 @@ int shutdown_parse_argv(int argc, char *argv[]) { strv_free_and_replace(arg_wall, copy); } - optind = argc; - return 1; } From 0a33b37e93bc8163120ade659b5e1b18e2a9ba7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 13 May 2026 23:29:46 +0200 Subject: [PATCH 10/12] shared/options: implement the equivalent of 'opterr' All log messages during option parsing are emitted using log_full, and the level is set as LOG_ERR + state->log_level_shift. The default shift is 0, but if set to e.g. 4, we log at LOG_DEBUG, and if set to 5 or higher, logging is effectively suppressed. (Unless compiled with LOG_TRACE, when it'd be suppressed if the shift if set to 6 or higher.) So this gives something like 'opterr', except that without global state and potentially more flexible. --- src/shared/options.c | 55 ++++++++++++++++++++++++++++---------------- src/shared/options.h | 3 +++ 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/shared/options.c b/src/shared/options.c index 34d02d2850..ac36fe0765 100644 --- a/src/shared/options.c +++ b/src/shared/options.c @@ -39,6 +39,7 @@ static void shift_arg(char* argv[], int target, int source) { } static int partial_match_error( + const OptionParser *state, const Option options[], const Option options_end[], const char *optname, @@ -57,15 +58,17 @@ static int partial_match_error( r = strv_extendf(&s, "--%s", option->long_code); if (r < 0) - return log_error_errno(r, "Failed to format message: %m"); + return log_full_errno(LOG_ERR + state->log_level_shift, r, + "Failed to format message: %m"); } assert(strv_length(s) == n_partial_matches); _cleanup_free_ char *p = strv_join_full(s, ", ", /* prefix= */ NULL, /* escape_separator= */ false); - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "%s: option '%s' is ambiguous; possibilities: %s", - program_invocation_short_name, optname, strnull(p)); + return log_full_errno(LOG_ERR + state->log_level_shift, + SYNTHETIC_ERRNO(EINVAL), + "%s: option '%s' is ambiguous; possibilities: %s", + program_invocation_short_name, optname, strnull(p)); } int option_parse( @@ -84,9 +87,14 @@ int option_parse( case OPTION_PARSER_INIT: { assert(state->mode >= 0 && state->mode < _OPTION_PARSER_MODE_MAX); + /* Make sure the shifted log level is between LOG_EMERG/0 and LOG_DEBUG/7. */ + assert(state->log_level_shift >= LOG_EMERG - LOG_ERR && + state->log_level_shift <= LOG_DEBUG - LOG_ERR); if (state->argc < 1) { - r = log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "argv cannot be empty"); + r = log_full_errno(LOG_ERR + state->log_level_shift, + SYNTHETIC_ERRNO(EUCLEAN), + "argv cannot be empty"); goto fail; } @@ -134,7 +142,9 @@ int option_parse( goto done; case OPTION_PARSER_FAILED: - return log_error_errno(SYNTHETIC_ERRNO(ESTALE), "Option parser failed before, refusing."); + return log_full_errno(LOG_ERR + state->log_level_shift, + SYNTHETIC_ERRNO(ESTALE), + "Option parser failed before, refusing."); default: assert_not_reached(); @@ -203,7 +213,7 @@ int option_parse( if (eq) { optname = _optname = strndup(state->argv[state->optind], eq - state->argv[state->optind]); if (!_optname) { - r = log_oom(); + r = log_oom_full(LOG_ERR + state->log_level_shift); goto fail; } @@ -219,13 +229,15 @@ int option_parse( for (option = state->namespace_start;; option++) { if (option >= state->namespace_end) { if (n_partial_matches == 0) { - r = log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "%s: unrecognized option '%s'", - program_invocation_short_name, optname); + r = log_full_errno(LOG_ERR + state->log_level_shift, + SYNTHETIC_ERRNO(EINVAL), + "%s: unrecognized option '%s'", + program_invocation_short_name, optname); goto fail; } if (n_partial_matches > 1) { r = partial_match_error( + state, state->namespace_start, state->namespace_end, optname, @@ -261,16 +273,17 @@ int option_parse( char optchar = state->argv[state->optind][state->short_option_offset]; if (asprintf(&_optname, "-%c", optchar) < 0) { - r = log_oom(); + r = log_oom_full(LOG_ERR + state->log_level_shift); goto fail; } optname = _optname; for (option = state->namespace_start;; option++) { if (option >= state->namespace_end) { - r = log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "%s: unrecognized option '%s'", - program_invocation_short_name, optname); + r = log_full_errno(LOG_ERR + state->log_level_shift, + SYNTHETIC_ERRNO(EINVAL), + "%s: unrecognized option '%s'", + program_invocation_short_name, optname); goto fail; } @@ -295,16 +308,18 @@ int option_parse( assert(option); if (!handling_positional_arg && optval && !option_takes_arg(option)) { - r = log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "%s: option '%s' doesn't allow an argument", - program_invocation_short_name, optname); + r = log_full_errno(LOG_ERR + state->log_level_shift, + SYNTHETIC_ERRNO(EINVAL), + "%s: option '%s' doesn't allow an argument", + program_invocation_short_name, optname); goto fail; } if (!handling_positional_arg && !optval && option_arg_required(option)) { if (!state->argv[state->optind + 1]) { - r = log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "%s: option '%s' requires an argument", - program_invocation_short_name, optname); + r = log_full_errno(LOG_ERR + state->log_level_shift, + SYNTHETIC_ERRNO(EINVAL), + "%s: option '%s' requires an argument", + program_invocation_short_name, optname); goto fail; } optval = state->argv[state->optind + 1]; diff --git a/src/shared/options.h b/src/shared/options.h index f88275821a..d60c9afe81 100644 --- a/src/shared/options.h +++ b/src/shared/options.h @@ -200,6 +200,9 @@ typedef struct OptionParser { char **argv; /* The argv array, possibly reordered. */ OptionParserMode mode; const char *namespace; /* The namespace, may be NULL. */ + int log_level_shift; /* The log level difference from the default of LOG_ERR. + * Allowed values are -3..4. + * Use 4 == LOG_DEBUG - LOG_ERR to log at debug level. */ const Option *namespace_start, *namespace_end; /* The range of options that are part of our namespace. */ From 4a30028c85e137996fd87dff236dd037d14acad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 14 May 2026 00:21:25 +0200 Subject: [PATCH 11/12] fuzz-systemctl-parse-argv: update suppression of logging and resetting of state There's certainly more than one way to skin this particular cat, so I'm keeping this as a separate commit. --- src/systemctl/fuzz-systemctl-parse-argv.c | 8 +++----- src/systemctl/systemctl-compat-halt.c | 8 ++++++-- src/systemctl/systemctl-compat-halt.h | 3 +-- src/systemctl/systemctl-compat-shutdown.c | 8 ++++++-- src/systemctl/systemctl-compat-shutdown.h | 2 +- src/systemctl/systemctl-main.c | 2 +- src/systemctl/systemctl.c | 21 +++++++++++++-------- src/systemctl/systemctl.h | 2 +- 8 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/systemctl/fuzz-systemctl-parse-argv.c b/src/systemctl/fuzz-systemctl-parse-argv.c index c57faa6771..9d487f7c6e 100644 --- a/src/systemctl/fuzz-systemctl-parse-argv.c +++ b/src/systemctl/fuzz-systemctl-parse-argv.c @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include #include #include "bus-util.h" @@ -88,17 +87,16 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { log_warning_errno(orig_stdout_fd, "Failed to duplicate fd 1: %m"); else assert_se(freopen("/dev/null", "w", stdout)); - - opterr = 0; /* do not print errors */ } /* We need to reset some global state manually here since libfuzzer feeds a single process with * multiple inputs, so we might carry over state from previous invocations that can trigger * certain asserts. */ - optind = 0; /* this tells the getopt machinery to reinitialize */ arg_transport = BUS_TRANSPORT_LOCAL; - r = systemctl_dispatch_parse_argv(strv_length(argv), argv, /* remaining_args= */ NULL); + r = systemctl_dispatch_parse_argv(strv_length(argv), argv, + /* log_level_shift= */ LOG_DEBUG - LOG_ERR, + /* remaining_args= */ NULL); if (r < 0) log_error_errno(r, "Failed to parse args: %m"); else diff --git a/src/systemctl/systemctl-compat-halt.c b/src/systemctl/systemctl-compat-halt.c index 3f19a263ff..217487b643 100644 --- a/src/systemctl/systemctl-compat-halt.c +++ b/src/systemctl/systemctl-compat-halt.c @@ -50,13 +50,17 @@ static int halt_help(void) { return 0; } -int halt_parse_argv(int argc, char *argv[]) { +int halt_parse_argv(int argc, char *argv[], int log_level_shift) { int r; assert(argc >= 0); assert(argv); - OptionParser opts = { argc, argv, .namespace = "halt" }; + OptionParser opts = { + argc, argv, + .namespace = "halt", + .log_level_shift = log_level_shift, + }; FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { diff --git a/src/systemctl/systemctl-compat-halt.h b/src/systemctl/systemctl-compat-halt.h index 85b9dda0e4..3658e52e1a 100644 --- a/src/systemctl/systemctl-compat-halt.h +++ b/src/systemctl/systemctl-compat-halt.h @@ -1,6 +1,5 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once -int halt_parse_argv(int argc, char *argv[]); - +int halt_parse_argv(int argc, char *argv[], int log_level_shift); int halt_main(void); diff --git a/src/systemctl/systemctl-compat-shutdown.c b/src/systemctl/systemctl-compat-shutdown.c index f4dbd80473..d93fa91ca0 100644 --- a/src/systemctl/systemctl-compat-shutdown.c +++ b/src/systemctl/systemctl-compat-shutdown.c @@ -116,13 +116,17 @@ static int parse_shutdown_time_spec(const char *t, usec_t *ret) { return 0; } -int shutdown_parse_argv(int argc, char *argv[]) { +int shutdown_parse_argv(int argc, char *argv[], int log_level_shift) { int r; assert(argc >= 0); assert(argv); - OptionParser opts = { argc, argv, .namespace = "shutdown" }; + OptionParser opts = { + argc, argv, + .namespace = "shutdown", + .log_level_shift = log_level_shift, + }; FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { diff --git a/src/systemctl/systemctl-compat-shutdown.h b/src/systemctl/systemctl-compat-shutdown.h index 7acf9414c9..9181a31f86 100644 --- a/src/systemctl/systemctl-compat-shutdown.h +++ b/src/systemctl/systemctl-compat-shutdown.h @@ -1,4 +1,4 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once -int shutdown_parse_argv(int argc, char *argv[]); +int shutdown_parse_argv(int argc, char *argv[], int log_level_shift); diff --git a/src/systemctl/systemctl-main.c b/src/systemctl/systemctl-main.c index 395874d563..8e2b1c170d 100644 --- a/src/systemctl/systemctl-main.c +++ b/src/systemctl/systemctl-main.c @@ -26,7 +26,7 @@ static int run(int argc, char *argv[]) { setlocale(LC_ALL, ""); log_setup(); - r = systemctl_dispatch_parse_argv(argc, argv, &args); + r = systemctl_dispatch_parse_argv(argc, argv, /* log_level_shift= */ 0, &args); if (r <= 0) goto finish; diff --git a/src/systemctl/systemctl.c b/src/systemctl/systemctl.c index 817ee71980..d5473cdc0a 100644 --- a/src/systemctl/systemctl.c +++ b/src/systemctl/systemctl.c @@ -373,7 +373,7 @@ static int parse_what_argument(const char *value, char ***clean_what) { return 1; } -static int systemctl_parse_argv(int argc, char *argv[], char ***remaining_args) { +static int systemctl_parse_argv(int argc, char *argv[], int log_level_shift, char ***remaining_args) { int r; assert(argc >= 0); @@ -382,7 +382,11 @@ static int systemctl_parse_argv(int argc, char *argv[], char ***remaining_args) /* We default to allowing interactive authorization only in systemctl (not in the legacy commands) */ arg_ask_password = true; - OptionParser opts = { argc, argv, .namespace = "systemctl" }; + OptionParser opts = { + argc, argv, + .namespace = "systemctl", + .log_level_shift = log_level_shift, + }; FOREACH_OPTION_OR_RETURN(c, &opts) switch (c) { @@ -898,28 +902,29 @@ static int systemctl_parse_argv(int argc, char *argv[], char ***remaining_args) return 1; } -int systemctl_dispatch_parse_argv(int argc, char *argv[], char ***remaining_args) { +int systemctl_dispatch_parse_argv(int argc, char *argv[], int log_level_shift, char ***remaining_args) { assert(argc >= 0); assert(argv); if (invoked_as(argv, "halt")) { arg_action = ACTION_HALT; - return halt_parse_argv(argc, argv); + return halt_parse_argv(argc, argv, log_level_shift); } else if (invoked_as(argv, "poweroff")) { arg_action = ACTION_POWEROFF; - return halt_parse_argv(argc, argv); + return halt_parse_argv(argc, argv, log_level_shift); } else if (invoked_as(argv, "reboot")) { arg_action = ACTION_REBOOT; - return halt_parse_argv(argc, argv); + return halt_parse_argv(argc, argv, log_level_shift); } else if (invoked_as(argv, "shutdown")) { arg_action = ACTION_POWEROFF; - return shutdown_parse_argv(argc, argv); + return shutdown_parse_argv(argc, argv, log_level_shift); + } else { arg_action = ACTION_SYSTEMCTL; - return systemctl_parse_argv(argc, argv, remaining_args); + return systemctl_parse_argv(argc, argv, log_level_shift, remaining_args); } } diff --git a/src/systemctl/systemctl.h b/src/systemctl/systemctl.h index 58ad1e5876..e157ab12f0 100644 --- a/src/systemctl/systemctl.h +++ b/src/systemctl/systemctl.h @@ -102,5 +102,5 @@ static inline const char* arg_job_mode(void) { return _arg_job_mode ?: "replace"; } -int systemctl_dispatch_parse_argv(int argc, char *argv[], char ***remaining_args); +int systemctl_dispatch_parse_argv(int argc, char *argv[], int log_level_shift, char ***remaining_args); int systemctl_main(char **args); From 440d701f3c930b53ea054b25cd859388f3cd9a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 14 May 2026 00:34:03 +0200 Subject: [PATCH 12/12] fuzz-systemctl-parse-argv: add two corpus files to test compat parsers Looking at the corpus examples, I'm not sure the fuzzer even went into the compat parsers. None of the files have argv[0] that'd cause invoked_as() to go into the compat paths. So add the files to provide a quick test and possibly bias the fuzzer search into the right direction. --- test/fuzz/fuzz-systemctl-parse-argv/halt.input | Bin 0 -> 104 bytes .../fuzz/fuzz-systemctl-parse-argv/shutdown.input | Bin 0 -> 102 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/fuzz/fuzz-systemctl-parse-argv/halt.input create mode 100644 test/fuzz/fuzz-systemctl-parse-argv/shutdown.input diff --git a/test/fuzz/fuzz-systemctl-parse-argv/halt.input b/test/fuzz/fuzz-systemctl-parse-argv/halt.input new file mode 100644 index 0000000000000000000000000000000000000000..26b15bd1833ba3be7d053ba0c52f49bab6691b49 GIT binary patch literal 104 zcmXYnK@NZ*3dhYo@QpVB}xxOg{3X*Y8Y(D*%#vjSdR=#>>T}S Z1eNKo`0}?XvhALl0@XmpyXq6Ao-en2Af5mK literal 0 HcmV?d00001 diff --git a/test/fuzz/fuzz-systemctl-parse-argv/shutdown.input b/test/fuzz/fuzz-systemctl-parse-argv/shutdown.input new file mode 100644 index 0000000000000000000000000000000000000000..cc62a983c1f25e07416044f920abe642a6481121 GIT binary patch literal 102 zcmWlQK@NZ*3