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:
+18
-2
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user