shared/verbs: allow verbs to declare an option namespace

Verbs like udevadm's subcommands parse their own options from a dedicated
option namespace. Allow the namespace to be declared in the verb
definition (via the new VERB_SCOPE_NS* macros), and report the options of
that namespace in the verb's CLI introspection object.

This is conformant with the CLI-Introspection Specification: verbs are
represented as nested command objects, which may carry their own "options"
array just like the top-level command object.
This commit is contained in:
Zbigniew Jędrzejewski-Szmek
2026-08-28 09:29:04 +02:00
parent 23e8e4e80f
commit 057fc4251a
3 changed files with 73 additions and 13 deletions
+18 -2
View File
@@ -550,7 +550,13 @@ int _command_print_help_full(
return 0;
}
static int verb_build_json(const Verb *verb, sd_json_variant **ret) {
static int verb_build_json(
const Verb *verb,
const Option options[],
const Option options_end[],
sd_json_variant **ret) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *opts = NULL;
int r;
assert(verb);
@@ -559,17 +565,27 @@ static int verb_build_json(const Verb *verb, sd_json_variant **ret) {
/* Verbs are represented as command objects, as described in the CLI-Introspection
* Specification (https://uapi-group.org/specifications/specs/cli_introspection/).
* In particular, a verb which declares an option namespace carries its own "options"
* array, just like the top-level command object.
*
* The "minArguments", "maxArguments", "isDefault", and "isOnlineOnly" fields
* are extensions not (yet) covered by the specification. Note that the argument
* counts include the verb itself. */
if (verb->option_namespace) {
r = options_build_json(options, options_end, verb->option_namespace,
/* option_groups= */ NULL, &opts);
if (r < 0)
return r;
}
r = sd_json_buildo(
ret,
SD_JSON_BUILD_PAIR_STRV("names", STRV_MAKE(verb->verb)),
SD_JSON_BUILD_PAIR_CONDITION(
!!verb->help,
"abstract", SD_JSON_BUILD_STRV(STRV_MAKE(verb->help))),
SD_JSON_BUILD_PAIR_CONDITION(!!opts, "options", SD_JSON_BUILD_VARIANT(opts)),
SD_JSON_BUILD_PAIR_CONDITION(
verb->min_args != VERB_ANY,
"minArguments", SD_JSON_BUILD_UNSIGNED(verb->min_args)),
@@ -625,7 +641,7 @@ static int command_build_json(
continue;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *o = NULL;
r = verb_build_json(verb, &o);
r = verb_build_json(verb, options, options_end, &o);
if (r < 0)
return r;
+24 -10
View File
@@ -43,10 +43,11 @@ typedef struct _alignptr_ Verb {
uintptr_t data;
const char *argspec;
const char *help;
const char *option_namespace; /* optional namespace with the verb's own options */
} Verb;
assert_cc(sizeof(Verb) % sizeof(void*) == 0);
#define _VERB_DATA(d, v, a, amin, amax, f, dat, h) \
#define _VERB_DATA(d, v, ns, a, amin, amax, f, dat, h) \
_section_("SYSTEMD_VERBS") \
_alignptr_ \
_used_ \
@@ -62,21 +63,33 @@ assert_cc(sizeof(Verb) % sizeof(void*) == 0);
.data = dat, \
.argspec = a, \
.help = h, \
.option_namespace = ns, \
}
/* Forward-define function d. scope specifies the scope, e.g. static. */
#define VERB_SCOPE_FULL(scope, d, v, a, amin, amax, f, dat, h) \
/* Forward-define function d. scope specifies the scope, e.g. static. ns declares the option
* namespace with the verb's own options, which are then shown in the verb's help (see
* command_print_verb_help()) and reported in the CLI introspection. */
#define VERB_SCOPE_NS_FULL(scope, d, v, ns, a, amin, amax, f, dat, h) \
DISABLE_WARNING_REDUNDANT_DECLS \
scope int d(int, char**, uintptr_t, void*); \
REENABLE_WARNING \
_VERB_DATA(d, v, a, amin, amax, f, dat, h)
/* The same as VERB_SCOPE_FULL with scope hardwired to 'static'. */
_VERB_DATA(d, v, ns, a, amin, amax, f, dat, h)
/* The same as VERB_SCOPE_NS_FULL, but for verbs without options of their own. */
#define VERB_SCOPE_FULL(scope, d, v, a, amin, amax, f, dat, h) \
VERB_SCOPE_NS_FULL(scope, d, v, /* ns= */ NULL, a, amin, amax, f, dat, h)
/* The same as VERB_SCOPE_NS_FULL/VERB_SCOPE_FULL with scope hardwired to 'static'. */
#define VERB_NS_FULL(d, v, ns, a, amin, amax, f, dat, h) \
VERB_SCOPE_NS_FULL(static, d, v, ns, a, amin, amax, f, dat, h)
#define VERB_FULL(d, v, a, amin, amax, f, dat, h) \
VERB_SCOPE_FULL(static, d, v, a, amin, amax, f, dat, h)
/* The same as VERB_SCOPE_FULL/VERB_FULL, but without the data argument. */
/* The same as the macros above, but without the data argument. */
#define VERB_SCOPE_NS(scope, d, v, ns, a, amin, amax, f, h) \
VERB_SCOPE_NS_FULL(scope, d, v, ns, a, amin, amax, f, /* dat= */ 0, h)
#define VERB_SCOPE(scope, d, v, a, amin, amax, f, h) \
VERB_SCOPE_FULL(scope, d, v, a, amin, amax, f, /* dat= */ 0, h)
#define VERB_NS(d, v, ns, a, amin, amax, f, h) \
VERB_SCOPE_NS(static, d, v, ns, a, amin, amax, f, h)
#define VERB(d, v, a, amin, amax, f, h) \
VERB_SCOPE(static, d, v, a, amin, amax, f, h)
@@ -91,13 +104,14 @@ assert_cc(sizeof(Verb) % sizeof(void*) == 0);
/* Magic entry in the table (which will not be returned) that designates the start of the group <gr>.
* The macro works as a separator between groups and must be between other VERB* stanzas. */
#define VERB_GROUP(gr) \
_VERB_DATA(/* d= */ NULL, /* v= */ gr, /* a= */ NULL, /* amin= */ 0, /* amax= */ 0, \
/* f= */ VERB_GROUP_MARKER, /* dat= */ 0, /* h= */ NULL)
_VERB_DATA(/* d= */ NULL, /* v= */ gr, /* ns= */ NULL, /* a= */ NULL, \
/* amin= */ 0, /* amax= */ 0, /* f= */ VERB_GROUP_MARKER, /* dat= */ 0, /* h= */ NULL)
#define _COMMAND(u, ...) \
static const CommandDescription UNIQ_T(description, u) = { __VA_ARGS__ }; \
_VERB_DATA(/* d= */ NULL, /* v= */ NULL, /* a= */ NULL, /* amin= */ 0, /* amax= */ 0, \
/* f= */ VERB_COMMAND_MARKER, /* dat= */ (uintptr_t) &UNIQ_T(description, u), /* h= */ NULL)
_VERB_DATA(/* d= */ NULL, /* v= */ NULL, /* ns= */ NULL, /* a= */ NULL, \
/* amin= */ 0, /* amax= */ 0, /* f= */ VERB_COMMAND_MARKER, \
/* dat= */ (uintptr_t) &UNIQ_T(description, u), /* h= */ NULL)
#define COMMAND(...) _COMMAND(UNIQ, __VA_ARGS__)
/* This is magically mapped to the beginning and end of the section */
+31 -1
View File
@@ -1,5 +1,8 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-json.h"
#include "options.h"
#include "strv.h"
#include "tests.h"
#include "verbs.h"
@@ -9,7 +12,7 @@ static int noop_dispatcher(int argc, char *argv[], uintptr_t _data, void *userda
}
#define test_dispatch_one(argv, verbs, expected) \
assert_se(_dispatch_verb(argv, verbs, verbs + ELEMENTSOF(verbs) - 1, NULL) == expected);
ASSERT_EQ(_dispatch_verb(argv, verbs, verbs + ELEMENTSOF(verbs) - 1, NULL), expected);
TEST(verbs) {
static const Verb verbs[] = {
@@ -84,4 +87,31 @@ TEST(verbs_no_default_many) {
test_dispatch_one(STRV_MAKE("Specials"), verbs, -EINVAL);
}
static const CommandDescription introspect_command = {
.names = "test-verbs\0",
.abstract = "Test the verb machinery.",
.man_pages = "systemd(1)\0",
};
static const Verb introspect_verbs[] = {
{ .flags = VERB_COMMAND_MARKER, .data = (uintptr_t) &introspect_command },
{ .verb = "alpha", .min_args = VERB_ANY, .max_args = VERB_ANY, .dispatch = noop_dispatcher,
.argspec = "ARG", .help = "Alpha verb", .option_namespace = "test-verbs-alpha" },
{ .verb = "beta", .min_args = VERB_ANY, .max_args = VERB_ANY, .dispatch = noop_dispatcher,
.help = "Beta verb" },
{}
};
static const Option introspect_options[] = {
{ 1, .long_code = "test-verbs-alpha", .flags = OPTION_NAMESPACE_MARKER },
{ 2, .short_code = 'x', .long_code = "example", .metavar = "ARG", .help = "An example option" },
{}
};
TEST(introspect_cli_verb_options) {
ASSERT_OK(_introspect_cli(introspect_verbs, introspect_verbs + ELEMENTSOF(introspect_verbs) - 1,
introspect_options, introspect_options + ELEMENTSOF(introspect_options) - 1,
SD_JSON_FORMAT_OFF));
}
DEFINE_TEST_MAIN(LOG_INFO);