shared/verbs: allow commands to filter option groups

So far, option groups were only used for display: when parsing options,
the group markers were ignored and all options within a namespace were
considered as potential matches. The group was reported in introspection
data and used as section seperator in help display. To allow a single
option namespace to be used for two commands, pipe through a nulstr with
option group names to look at when parsing options. The field is set in
the Command description and used in various option-handling contexts:
- when parsing and looking for an option, do not consider the entries
  that are in the group that is not listed in the nulstr,
- when listing options in help or introspection, skip over unlisted
  groups.

The verbs_find_verb and verbs_find_command helpers are adjusted to have
a consistent interface.
This commit is contained in:
Zbigniew Jędrzejewski-Szmek
2026-08-25 09:33:59 +02:00
parent dfe557dc7c
commit 5f45122e75
5 changed files with 99 additions and 33 deletions
+54 -8
View File
@@ -4,6 +4,7 @@
#include "format-table.h"
#include "log.h"
#include "nulstr-util.h"
#include "options.h"
#include "stdio-util.h"
#include "string-util.h"
@@ -198,18 +199,27 @@ int option_parse(
* First, figure out if we have a long option or a short option. */
assert(handling_positional_arg || state->argv[state->optind][0] == '-');
if (handling_positional_arg)
/* We are supposed to return the positional arg to be handled. */
if (handling_positional_arg) {
/* Find the positional arg to be handled. */
const char *group = NULL; /* We start in the default unnamed group. */
for (option = state->namespace_start;; option++) {
/* If OPTION_PARSER_RETURN_POSITIONAL_ARGS is specified,
* OPTION_POSITIONAL must be used. */
assert(option < state->namespace_end);
/* If state.option_groups are specified, ignore options outside of the group */
if (FLAGS_SET(option->flags, OPTION_GROUP_MARKER)) /* Start of a new option group */
group = option->long_code;
if (state->option_groups && !(group && nulstr_contains(state->option_groups, group)))
continue;
if (FLAGS_SET(option->flags, OPTION_POSITIONAL_ENTRY))
break;
}
else if (state->argv[state->optind][1] == '-') {
} else if (state->argv[state->optind][1] == '-') {
/* We have a long option. */
char *eq = strchr(state->argv[state->optind], '=');
if (eq) {
@@ -238,6 +248,8 @@ int option_parse(
const Option *last_partial = NULL;
unsigned n_partial_matches = 0; /* The commandline option matches a defined prefix. */
const char *group = NULL; /* We start in the default unnamed group. */
for (option = state->namespace_start;; option++) {
if (option >= state->namespace_end) {
if (n_partial_matches == 0) {
@@ -262,6 +274,12 @@ int option_parse(
break;
}
/* If state.option_groups are specified, ignore options outside of the group */
if (FLAGS_SET(option->flags, OPTION_GROUP_MARKER)) /* Start of a new option group */
group = option->long_code;
if (state->option_groups && !(group && nulstr_contains(state->option_groups, group)))
continue;
if (option_is_metadata(option) || !option->long_code)
continue;
@@ -290,6 +308,8 @@ int option_parse(
}
optname = _optname;
const char *group = NULL; /* We start in the default unnamed group. */
for (option = state->namespace_start;; option++) {
if (option >= state->namespace_end) {
r = log_full_errno(LOG_ERR + state->log_level_shift,
@@ -299,6 +319,12 @@ int option_parse(
goto fail;
}
/* If state.option_groups are specified, ignore options outside of the group */
if (FLAGS_SET(option->flags, OPTION_GROUP_MARKER)) /* Start of a new option group */
group = option->long_code;
if (state->option_groups && !(group && nulstr_contains(state->option_groups, group)))
continue;
if (option_is_metadata(option) || optchar != option->short_code)
continue;
@@ -573,6 +599,7 @@ int _option_parser_get_help_table_full(
int options_get_help_table_group(
const Option options[],
const Option options_end[],
const char *option_groups,
Table **ret,
const char **ret_group) {
int r;
@@ -580,15 +607,24 @@ int options_get_help_table_group(
assert(ret);
assert(ret_group);
_cleanup_(table_unrefp) Table *table = table_new("names", "help");
if (!table)
return log_oom();
_cleanup_(table_unrefp) Table *table = NULL;
assert(options_end > options);
bool group_marker = FLAGS_SET(options[0].flags, OPTION_GROUP_MARKER);
const char *group = group_marker ? ASSERT_PTR(options[0].long_code) : NULL;
/* Determine if we should ignore the whole option group. We do this if the option_groups option is
* specified and the current group name is not listed. The unnamed group cannot be specified by the
* nulstr, so always ignore it. */
bool ignore_group = option_groups && !(group && nulstr_contains(option_groups, group));
if (!ignore_group) {
table = table_new("names", "help");
if (!table)
return log_oom();
}
const Option *opt;
for (opt = options + group_marker; opt < options_end; opt++) {
if (FLAGS_SET(opt->flags, OPTION_NAMESPACE_MARKER))
@@ -599,6 +635,9 @@ int options_get_help_table_group(
/* End of the group */
break;
if (ignore_group)
continue;
if (!opt->help)
/* No help string — we do not show the option */
continue;
@@ -629,9 +668,11 @@ int options_get_help_table_group(
return table_log_add_error(r);
}
assert(!table_isempty(table)); /* Empty group? Something is off. */
if (!ignore_group) {
assert(!table_isempty(table)); /* Empty group? Something is off. */
table_set_header(table, false);
}
table_set_header(table, false);
*ret = TAKE_PTR(table);
*ret_group = group;
@@ -687,6 +728,7 @@ int options_build_json(
const Option options[],
const Option options_end[],
const char *namespace,
const char *option_groups,
sd_json_variant **ret) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL;
@@ -714,6 +756,10 @@ int options_build_json(
/* Positional and help entries are only used for display */
continue;
if (option_groups && !(group && nulstr_contains(option_groups, group)))
/* This group shall be ignored */
continue;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *o = NULL;
r = option_build_json(opt, group, &o);
if (r < 0)
+4
View File
@@ -226,6 +226,8 @@ typedef struct OptionParser {
char **argv; /* The argv array, possibly reordered. */
OptionParserMode mode;
const char *namespace; /* The namespace, may be NULL. */
const char *option_groups; /* Optional nulstr with option groups to look at. When specified,
* only named option groups found in this list are used. */
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. */
@@ -297,6 +299,7 @@ int _option_parser_get_help_table_full(
int options_get_help_table_group(
const Option options[],
const Option options_end[],
const char *option_groups,
Table **ret,
const char **ret_group);
@@ -306,4 +309,5 @@ int options_build_json(
const Option options[],
const Option options_end[],
const char *namespace,
const char *option_groups,
sd_json_variant **ret);
+31 -21
View File
@@ -72,7 +72,7 @@ static bool verb_is_metadata(const Verb *verb) {
FLAGS_SET(ASSERT_PTR(verb)->flags, VERB_GROUP_MARKER);
}
const Verb* verbs_find_verb(const char *name, const Verb verbs[], const Verb verbs_end[]) {
const Verb* _verbs_find_verb(const Verb verbs[], const Verb verbs_end[], const char *name) {
assert(verbs);
assert(verbs_end > verbs);
assert((uintptr_t) verbs % sizeof(void*) == 0);
@@ -101,7 +101,7 @@ int _dispatch_verb(char **args, const Verb verbs[], const Verb verbs_end[], void
const char *name = args ? args[0] : NULL;
size_t left = strv_length(args);
const Verb *verb = verbs_find_verb(name, verbs, verbs_end);
const Verb *verb = _verbs_find_verb(verbs, verbs_end, name);
if (!verb) {
_cleanup_strv_free_ char **verb_strv = NULL;
@@ -224,7 +224,12 @@ static int verb_add_help_one(Table *table, const Verb *verb) {
return 0;
}
static const Verb* verbs_get_command(const Verb verbs[], const Verb verbs_end[], const char *name) {
const Verb* _verbs_find_command(
const Verb verbs[],
const Verb verbs_end[],
const char *name,
const CommandDescription **ret_cmd) {
assert(verbs);
assert(verbs_end > verbs);
assert((uintptr_t) verbs % sizeof(void*) == 0);
@@ -235,8 +240,14 @@ static const Verb* verbs_get_command(const Verb verbs[], const Verb verbs_end[],
continue;
const CommandDescription *cmd = (const CommandDescription*) ASSERT_PTR(verb->data);
if (!name || nulstr_contains(cmd->names, name))
return verb;
if (name && !nulstr_contains(cmd->names, name))
continue;
/* This function returns the Verb slot through the return value, and the
* CommandDescription it points to in the optional output parameter. */
if (ret_cmd)
*ret_cmd = cmd;
return verb;
}
/* At the end of the list? */
@@ -332,7 +343,8 @@ static int print_verb_option_help(
assert(verb->verb);
/* What is the the first group? */
/* The group marker gives us the group name.
* If it's missing, we're in the default (unnamed) group. */
const char *group = FLAGS_SET(verb->flags, VERB_GROUP_MARKER) ? verb->verb : NULL;
_cleanup_(table_unrefp) Table *table = NULL;
@@ -368,17 +380,19 @@ static int print_verb_option_help(
/* End of our namespace */
break;
r = options_get_help_table_group(opt, options_end, &table, &group);
r = options_get_help_table_group(opt, options_end, cmd->option_groups, &table, &group);
if (r < 0)
return r;
if (!GREEDY_REALLOC0(tables, n_verbs + n_opts + 2))
return -ENOMEM;
if (!GREEDY_REALLOC0(groups, n_verbs + n_opts + 2))
return -ENOMEM;
if (table) {
if (!GREEDY_REALLOC0(tables, n_verbs + n_opts + 2))
return -ENOMEM;
if (!GREEDY_REALLOC0(groups, n_verbs + n_opts + 2))
return -ENOMEM;
tables[n_verbs + n_opts] = TAKE_PTR(table);
groups[n_verbs + n_opts++] = group ?: "Options";
tables[n_verbs + n_opts] = TAKE_PTR(table);
groups[n_verbs + n_opts++] = group ?: "Options";
}
opt += r; /* Skip over the whole option group */
}
@@ -497,13 +511,9 @@ int _command_print_help_full(
const char *footer_ansi_seq) {
int r;
const Verb *cmdverb = verbs_get_command(verbs, verbs_end, name);
if (!cmdverb)
return log_error_errno(SYNTHETIC_ERRNO(ENODATA),
"Command %s not known", name);
const CommandDescription *cmd = (const CommandDescription*) ASSERT_PTR(cmdverb->data);
const Verb *verbverbs = verbs_find_cmd_verbs(cmdverb, verbs_end);
const CommandDescription *cmd;
const Verb *cmdverb = ASSERT_PTR(_verbs_find_command(verbs, verbs_end, name, &cmd));
const Verb *verbverbs = ASSERT_PTR(verbs_find_cmd_verbs(cmdverb, verbs_end));
if (cmd->pager_flags)
pager_open(*cmd->pager_flags);
@@ -603,7 +613,7 @@ static int command_build_json(
}
assert(names); /* At least the primary name must be defined */
r = options_build_json(options, options_end, cmd->option_namespace, &opts);
r = options_build_json(options, options_end, cmd->option_namespace, cmd->option_groups, &opts);
if (r < 0)
return r;
+9 -2
View File
@@ -21,7 +21,8 @@ typedef struct CommandDescription {
const char *argspec; /* optional specification of positional args in synopsis */
const char *footer; /* optional footer to print right above man page links */
const char *man_pages; /* nulstr with man page names */
const char *option_namespace; /* to be used when the options are in a namespace */
const char *option_namespace; /* optional option namespace for this command */
const char *option_groups; /* optional nulstr with option groups for this command */
const PagerFlags *pager_flags; /* optional pointer to the runtime pager flags variable */
CommandFlags flags;
} CommandDescription;
@@ -107,7 +108,13 @@ bool running_in_chroot_or_offline(void);
bool should_bypass(const char *env_prefix);
const Verb* verbs_find_verb(const char *name, const Verb verbs[], const Verb verbs_end[]);
const Verb* _verbs_find_command(const Verb verbs[], const Verb verbs_end[], const char *name, const CommandDescription **ret_cmd);
#define verbs_find_command(name, ret_cmd) \
_verbs_find_command(__start_SYSTEMD_VERBS, __stop_SYSTEMD_VERBS, name, ret_cmd)
const Verb* _verbs_find_verb(const Verb verbs[], const Verb verbs_end[], const char *name);
#define verbs_find_verb(name) \
_verbs_find_verb(__start_SYSTEMD_VERBS, __stop_SYSTEMD_VERBS, name)
int _dispatch_verb(char **args, const Verb verbs[], const Verb verbs_end[], void *userdata);
#define dispatch_verb(args, userdata) \
+1 -2
View File
@@ -1093,8 +1093,7 @@ VERB_SCOPE(, verb_start, "condrestart", NULL, 2, VERB_ANY,
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) {
assert((uintptr_t) __start_SYSTEMD_VERBS % sizeof(void*) == 0);
const Verb *verb = verbs_find_verb(args[0], __start_SYSTEMD_VERBS, __stop_SYSTEMD_VERBS);
const Verb *verb = verbs_find_verb(args[0]);
if (verb && (verb->flags & VERB_ONLINE_ONLY) && arg_root)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),