]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
shared/verbs: introduce verb groups
authorZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Wed, 25 Mar 2026 12:57:54 +0000 (13:57 +0100)
committerZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Thu, 26 Mar 2026 09:31:23 +0000 (10:31 +0100)
This mirrors the idea and implementation for options.

Previously, the Verb struct was named as verb_func_data_nn, but
with the group marker entry, we don't have 'verb_func', so let's
just call the item verb_data_nn. Using the verb here is a complication
that is not needed.

src/fundamental/macro-fundamental.h
src/shared/options.c
src/shared/verbs.c
src/shared/verbs.h
src/test/test-verbs.c

index 1941e88d3760e5782efecffcbcf45827f03f9336..39004183d90f286dd6f45e13738055ff95fad7a2 100644 (file)
 
 #define XCONCATENATE(x, y) x ## y
 #define CONCATENATE(x, y) XCONCATENATE(x, y)
-#define CONCATENATE3(x, y, z) CONCATENATE(x, CONCATENATE(y, z))
 
 #define assert_cc(expr) _Static_assert(expr, #expr)
 
index 86f274821eda83f3b2ace5a2a82ee8401a8d3a1c..c5118f33426c32fb4aa037d74afe02245cc5720e 100644 (file)
@@ -266,7 +266,7 @@ int _option_parser_get_help_table(
 
         bool in_group = group == NULL;  /* Are we currently in the section on the array that forms
                                          * group <group>? The first part is the default group, so
-                                         * the group was not specified, we are in. */
+                                         * if the group was not specified, we are in. */
 
         for (const Option *opt = options; opt < options_end; opt++) {
                 bool group_marker = FLAGS_SET(opt->flags, OPTION_GROUP_MARKER);
index 47f219fab6fccef5af8a96b228789c863ec5f2ee..dfecf048612b79e083ab24827823a2b893a5a085 100644 (file)
@@ -58,12 +58,21 @@ bool should_bypass(const char *env_prefix) {
         return true;
 }
 
+static bool verb_is_metadata(const Verb *verb) {
+        /* A metadata entry that is not a real verb, like the group marker */
+        return FLAGS_SET(ASSERT_PTR(verb)->flags, VERB_GROUP_MARKER);
+}
+
 const Verb* verbs_find_verb(const char *name, const Verb verbs[], const Verb verbs_end[]) {
         assert(verbs);
 
-        for (const Verb *verb = verbs; verb < verbs_end; verb++)
+        for (const Verb *verb = verbs; verb < verbs_end; verb++) {
+                if (verb_is_metadata(verb))
+                        continue;
+
                 if (name ? streq(name, verb->verb) : FLAGS_SET(verb->flags, VERB_DEFAULT))
                         return verb;
+        }
 
         /* At the end of the list? */
         return NULL;
@@ -85,6 +94,9 @@ int _dispatch_verb_with_args(char **args, const Verb verbs[], const Verb verbs_e
                 _cleanup_strv_free_ char **verb_strv = NULL;
 
                 for (verb = verbs; verb < verbs_end; verb++) {
+                        if (verb_is_metadata(verb))
+                                continue;
+
                         r = strv_extend(&verb_strv, verb->verb);
                         if (r < 0)
                                 return log_oom();
@@ -142,13 +154,17 @@ int dispatch_verb(int argc, char *argv[], const Verb verbs[], void *userdata) {
         assert(argc >= optind);
 
         size_t n = 0;
-        while (verbs[n].dispatch)
+        while (verbs[n].verb)
                 n++;
 
         return _dispatch_verb_with_args(strv_skip(argv, optind), verbs, verbs + n, userdata);
 }
 
-int _verbs_get_help_table(const Verb verbs[], const Verb verbs_end[], Table **ret) {
+int _verbs_get_help_table(
+                const Verb verbs[],
+                const Verb verbs_end[],
+                const char *group,
+                Table **ret) {
         int r;
 
         assert(ret);
@@ -157,10 +173,23 @@ int _verbs_get_help_table(const Verb verbs[], const Verb verbs_end[], Table **re
         if (!table)
                 return log_oom();
 
+        bool in_group = group == NULL;  /* Are we currently in the section on the array that forms
+                                         * group <group>? The first part is the default group, so
+                                         * if the group was not specified, we are in. */
+
         for (const Verb *verb = verbs; verb < verbs_end; verb++) {
-                assert(verb->dispatch);
+                assert(verb->verb);
+
+                bool group_marker = FLAGS_SET(verb->flags, VERB_GROUP_MARKER);
+                if (!in_group) {
+                        in_group = group_marker && streq(group, verb->verb);
+                        continue;
+                }
+                if (group_marker)
+                        break;  /* End of group */
 
                 if (!verb->help)
+                        /* No help string — we do not show the verb */
                         continue;
 
                 /* We indent the option string by two spaces. We could set the minimum cell width and
index 7980db62fe9136b693897018a160263d82f0e843..6b380041b81e5c8d47e1e7c4ce1edb8ebd0227ab 100644 (file)
@@ -8,6 +8,7 @@
 typedef enum VerbFlags {
         VERB_DEFAULT      = 1 << 0,  /* The verb to run if no verb is specified */
         VERB_ONLINE_ONLY  = 1 << 1,  /* Just do nothing when running in chroot or offline */
+        VERB_GROUP_MARKER = 1 << 2,  /* Fake verb entry to separate groups */
 } VerbFlags;
 
 typedef struct {
@@ -20,16 +21,13 @@ typedef struct {
         const char *help;
 } Verb;
 
-#define VERB_FULL(d, v, a, amin, amax, f, dat, h)                       \
-        DISABLE_WARNING_REDUNDANT_DECLS                                 \
-        static int d(int, char**, uintptr_t, void*);                    \
-        REENABLE_WARNING                                                \
+#define _VERB_DATA(d, v, a, amin, amax, f, dat, h)                      \
         _section_("SYSTEMD_VERBS")                                      \
         _alignptr_                                                      \
         _used_                                                          \
         _retain_                                                        \
         _variable_no_sanitize_address_                                  \
-        static const Verb CONCATENATE3(d, _data_, __COUNTER__) = {      \
+        static const Verb CONCATENATE(verb_data_, __COUNTER__) = {      \
                 .verb = v,                                              \
                 .min_args = amin,                                       \
                 .max_args = amax,                                       \
@@ -40,6 +38,12 @@ typedef struct {
                 .help = h,                                              \
         }
 
+#define VERB_FULL(d, v, a, amin, amax, f, dat, h)                       \
+        DISABLE_WARNING_REDUNDANT_DECLS                                 \
+        static int d(int, char**, uintptr_t, void*);                    \
+        REENABLE_WARNING                                                \
+        _VERB_DATA(d, v, a, amin, amax, f, dat, h)
+
 /* The same as VERB_FULL, but without the data argument */
 #define VERB(d, v, a, amin, amax, f, h)                                 \
         VERB_FULL(d, v, a, amin, amax, f, /* dat= */ 0, h)
@@ -48,6 +52,11 @@ typedef struct {
 #define VERB_NOARG(d, v, h)                                             \
         VERB(d, v, /* a= */ NULL, /* amin= */ VERB_ANY, /* amax= */ 1, /* f= */ 0, h)
 
+/* Magic entry in the table (which will not be returned) that designates the start of the group <gr>. */
+#define VERB_GROUP(gr)                                                  \
+        _VERB_DATA(/* d= */ NULL, /* v= */ gr, /* a= */ NULL, /* amin= */ 0, /* amax= */ 0, \
+                  /* f= */ VERB_GROUP_MARKER, /* dat= */ 0, /* h= */ NULL)
+
 /* This is magically mapped to the beginning and end of the section */
 extern const Verb __start_SYSTEMD_VERBS[];
 extern const Verb __stop_SYSTEMD_VERBS[];
@@ -64,9 +73,15 @@ int _dispatch_verb_with_args(char **args, const Verb verbs[], const Verb verbs_e
 
 int dispatch_verb(int argc, char *argv[], const Verb verbs[], void *userdata);
 
-int _verbs_get_help_table(const Verb verbs[], const Verb verbs_end[], Table **ret);
-#define verbs_get_help_table(ret) \
-        _verbs_get_help_table(ALIGN_PTR(__start_SYSTEMD_VERBS), __stop_SYSTEMD_VERBS, ret)
+int _verbs_get_help_table(
+                const Verb verbs[],
+                const Verb verbs_end[],
+                const char *group,
+                Table **ret);
+#define verbs_get_help_table_group(group, ret)                          \
+        _verbs_get_help_table(ALIGN_PTR(__start_SYSTEMD_VERBS), __stop_SYSTEMD_VERBS, group, ret)
+#define verbs_get_help_table(ret)                                       \
+        verbs_get_help_table_group(/* group= */ NULL, ret)
 
 #define _VERB_COMMON_HELP_IMPL(impl)                                    \
         static int verb_help(int argc, char **argv, uintptr_t data, void *userdata) { \
index 79c5c27dd94f32003c966010638159c00cff7330..41ae5a87f3766fc431549633fc8365d6807fa149 100644 (file)
@@ -16,14 +16,16 @@ static int noop_dispatcher(int argc, char *argv[], uintptr_t _data, void *userda
 
 TEST(verbs) {
         static const Verb verbs[] = {
-                { "help",        VERB_ANY, VERB_ANY, 0,            noop_dispatcher },
-                { "list-images", VERB_ANY, 1,        0,            noop_dispatcher },
-                { "list",        VERB_ANY, 2,        VERB_DEFAULT, noop_dispatcher },
-                { "status",      2,        VERB_ANY, 0,            noop_dispatcher },
-                { "show",        VERB_ANY, VERB_ANY, 0,            noop_dispatcher },
-                { "terminate",   2,        VERB_ANY, 0,            noop_dispatcher },
-                { "login",       2,        2,        0,            noop_dispatcher },
-                { "copy-to",     3,        4,        0,            noop_dispatcher },
+                { "help",        VERB_ANY, VERB_ANY, 0,                 noop_dispatcher },
+                { "list-images", VERB_ANY, 1,        0,                 noop_dispatcher },
+                { "list",        VERB_ANY, 2,        VERB_DEFAULT,      noop_dispatcher },
+                { "status",      2,        VERB_ANY, 0,                 noop_dispatcher },
+                { "Group2",      VERB_ANY, VERB_ANY, VERB_GROUP_MARKER, NULL            },
+                { "show",        VERB_ANY, VERB_ANY, 0,                 noop_dispatcher },
+                { "terminate",   2,        VERB_ANY, 0,                 noop_dispatcher },
+                { "Group3",      0,        0,        VERB_GROUP_MARKER, NULL            },
+                { "login",       2,        2,        0,                 noop_dispatcher },
+                { "copy-to",     3,        4,        0,                 noop_dispatcher },
                 {}
         };
 
@@ -44,6 +46,12 @@ TEST(verbs) {
 
         /* no verb, but a default is set */
         test_dispatch_one(STRV_EMPTY, verbs, 0);
+
+        /* the group entry shall not be found */
+        test_dispatch_one(STRV_MAKE("Group2"), verbs, -EINVAL);
+
+        /* the group entry shall not be found */
+        test_dispatch_one(STRV_MAKE("Group3"), verbs, -EINVAL);
 }
 
 TEST(verbs_no_default) {
@@ -60,14 +68,15 @@ TEST(verbs_no_default) {
 
 TEST(verbs_no_default_many) {
         static const Verb verbs[] = {
-                { "help",        VERB_ANY, VERB_ANY, 0,            noop_dispatcher },
-                { "list-images", VERB_ANY, 1,        0,            noop_dispatcher },
-                { "list",        VERB_ANY, 2,        0,            noop_dispatcher },
-                { "status",      2,        VERB_ANY, 0,            noop_dispatcher },
-                { "show",        VERB_ANY, VERB_ANY, 0,            noop_dispatcher },
-                { "terminate",   2,        VERB_ANY, 0,            noop_dispatcher },
-                { "login",       2,        2,        0,            noop_dispatcher },
-                { "copy-to",     3,        4,        0,            noop_dispatcher },
+                { "help",        VERB_ANY, VERB_ANY, 0,                 noop_dispatcher },
+                { "list-images", VERB_ANY, 1,        0,                 noop_dispatcher },
+                { "list",        VERB_ANY, 2,        0,                 noop_dispatcher },
+                { "status",      2,        VERB_ANY, 0,                 noop_dispatcher },
+                { "Specials",    VERB_ANY, VERB_ANY, VERB_GROUP_MARKER, NULL            },
+                { "show",        VERB_ANY, VERB_ANY, 0,                 noop_dispatcher },
+                { "terminate",   2,        VERB_ANY, 0,                 noop_dispatcher },
+                { "login",       2,        2,        0,                 noop_dispatcher },
+                { "copy-to",     3,        4,        0,                 noop_dispatcher },
                 {}
         };
 
@@ -75,6 +84,7 @@ TEST(verbs_no_default_many) {
         test_dispatch_one(STRV_MAKE("hel"), verbs, -EINVAL);
         test_dispatch_one(STRV_MAKE("helpp"), verbs, -EINVAL);
         test_dispatch_one(STRV_MAKE("hgrejgoraoiosafso"), verbs, -EINVAL);
+        test_dispatch_one(STRV_MAKE("Specials"), verbs, -EINVAL);
 }
 
 DEFINE_TEST_MAIN(LOG_INFO);