diff --git a/src/shared/verbs.c b/src/shared/verbs.c index 37ceb9fed6..b9b0c40a76 100644 --- a/src/shared/verbs.c +++ b/src/shared/verbs.c @@ -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; diff --git a/src/shared/verbs.h b/src/shared/verbs.h index 72b6d70f4f..b8a990a62b 100644 --- a/src/shared/verbs.h +++ b/src/shared/verbs.h @@ -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 . * 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 */ diff --git a/src/test/test-verbs.c b/src/test/test-verbs.c index 56062b0bd6..8fc9e2a25c 100644 --- a/src/test/test-verbs.c +++ b/src/test/test-verbs.c @@ -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);