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:
+54
-8
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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) \
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user