]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
shared/options: introduce "namespaces" for options
authorZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Mon, 27 Apr 2026 20:56:50 +0000 (22:56 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Tue, 28 Apr 2026 15:13:24 +0000 (17:13 +0200)
This allows multiple option parsers to be defined in a single
compilation unit. We put the OPTION_NAMESPACE("name") to split
up the options. The basic implementation is similar to groups,
except that groups only matter for help display, while namespaces
matter for both help display and actual option parsing. When parsing,
we locate the appropriate range between the beginning of options
and the next namespace marker or between two namespace markers and
only look at that range.

src/shared/options.c
src/shared/options.h
src/test/test-options.c

index faefbe659eae6812e2cbc216329381c297d27838..364b57792c8778c093cdafcdda9928f707c0d979 100644 (file)
@@ -21,7 +21,8 @@ static bool option_arg_required(const Option *opt) {
 
 static bool option_is_metadata(const Option *opt) {
         /* A metadata entry that is not a real option, like the group marker */
-        return ASSERT_PTR(opt)->flags & (OPTION_GROUP_MARKER |
+        return ASSERT_PTR(opt)->flags & (OPTION_NAMESPACE_MARKER |
+                                         OPTION_GROUP_MARKER |
                                          OPTION_POSITIONAL_ENTRY |
                                          OPTION_HELP_ENTRY |
                                          OPTION_HELP_ENTRY_VERBATIM);
@@ -81,7 +82,7 @@ int option_parse(
         /* Check and initialize */
         switch (state->state) {
 
-        case OPTION_PARSER_INIT:
+        case OPTION_PARSER_INIT: {
                 assert(state->mode >= 0 && state->mode < _OPTION_PARSER_MODE_MAX);
 
                 if (state->argc < 1) {
@@ -90,9 +91,34 @@ int option_parse(
                 }
 
                 assert_se((size_t) state->argc == strv_length(state->argv)); /* Make sure argc/argv are consistent */
+
+                /* Figure out the right range of options */
+                bool in_ns = state->namespace == NULL;  /* Are we currently in the section of the array that
+                                                         * forms namespace <namespace>? The first part is the
+                                                         * default unnamed namespace, so if the namespace was
+                                                         * not specified, we are in it. */
+                if (in_ns)
+                        state->namespace_start = options;
+
+                const Option *opt;
+                for (opt = options; opt < options_end; opt++) {
+                        bool ns_marker = FLAGS_SET(opt->flags, OPTION_NAMESPACE_MARKER);
+                        if (!in_ns) {
+                                in_ns = ns_marker && streq(state->namespace, opt->long_code);
+                                if (in_ns)
+                                        state->namespace_start = opt + 1;
+                                continue;
+                        }
+                        if (ns_marker)
+                                break;  /* End of namespace */
+                }
+                assert(state->namespace_start);
+                state->namespace_end = opt;
+
                 state->optind = state->positional_offset = 1;
                 state->state = OPTION_PARSER_RUNNING;
                 break;
+        }
 
         case OPTION_PARSER_RUNNING:
         case OPTION_PARSER_STOPPING:
@@ -156,10 +182,10 @@ int option_parse(
 
                 if (handling_positional_arg)
                         /* We are supposed to return the positional arg to be handled. */
-                        for (option = options;; option++) {
+                        for (option = state->namespace_start;; option++) {
                                 /* If OPTION_PARSER_RETURN_POSITIONAL_ARGS is specified,
                                  * OPTION_POSITIONAL must be used. */
-                                assert(option < options_end);
+                                assert(option < state->namespace_end);
 
                                 if (FLAGS_SET(option->flags, OPTION_POSITIONAL_ENTRY))
                                         break;
@@ -184,8 +210,8 @@ int option_parse(
                         const Option *last_partial = NULL;
                         unsigned n_partial_matches = 0;  /* The commandline option matches a defined prefix. */
 
-                        for (option = options;; option++) {
-                                if (option >= options_end) {
+                        for (option = state->namespace_start;; option++) {
+                                if (option >= state->namespace_end) {
                                         if (n_partial_matches == 0) {
                                                 r = log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                                                     "%s: unrecognized option '%s'",
@@ -193,7 +219,11 @@ int option_parse(
                                                 goto fail;
                                         }
                                         if (n_partial_matches > 1) {
-                                                r = partial_match_error(options, options_end, optname, n_partial_matches);
+                                                r = partial_match_error(
+                                                                state->namespace_start,
+                                                                state->namespace_end,
+                                                                optname,
+                                                                n_partial_matches);
                                                 goto fail;
                                         }
 
@@ -230,8 +260,8 @@ int option_parse(
                 }
                 optname = _optname;
 
-                for (option = options;; option++) {
-                        if (option >= options_end) {
+                for (option = state->namespace_start;; option++) {
+                        if (option >= state->namespace_end) {
                                 r = log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                                     "%s: unrecognized option '%s'",
                                                     program_invocation_short_name, optname);
@@ -354,7 +384,8 @@ size_t option_parser_get_n_args(const OptionParser *state) {
 
 char* option_get_synopsis(const char *prefix, const Option *opt, const char *joiner, bool show_metavar) {
         assert(opt);
-        assert(!FLAGS_SET(opt->flags, OPTION_GROUP_MARKER));  /* A group marker should not be displayed */
+        assert(!(opt->flags & (OPTION_NAMESPACE_MARKER |
+                               OPTION_GROUP_MARKER)));  /* The markers should not be displayed */
 
         if (!prefix)
                 prefix = "";
@@ -405,9 +436,10 @@ char* option_get_synopsis(const char *prefix, const Option *opt, const char *joi
                        option_arg_optional(opt) ? "]" : "");
 }
 
-int _option_parser_get_help_table(
+int _option_parser_get_help_table_full(
                 const Option options[],
                 const Option options_end[],
+                const char *namespace,
                 const char *group,
                 Table **ret) {
         int r;
@@ -418,11 +450,23 @@ int _option_parser_get_help_table(
         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. */
+        bool in_ns = namespace == NULL;  /* Are we currently in the section of the array that forms namespace
+                                          * <namespace>? The first part is the default unnamed namespace, so
+                                          * if the namespace was not specified, we are in it. */
+
+        bool in_group = group == NULL;  /* Are we currently in the section of the array that forms group
+                                         * <group>? The first part is the default group, so if the group was
+                                         * not specified, we are in it. */
 
         for (const Option *opt = options; opt < options_end; opt++) {
+                bool ns_marker = FLAGS_SET(opt->flags, OPTION_NAMESPACE_MARKER);
+                if (!in_ns) {
+                        in_ns = ns_marker && streq(namespace, opt->long_code);
+                        continue;
+                }
+                if (ns_marker)
+                        break;  /* End of namespace */
+
                 bool group_marker = FLAGS_SET(opt->flags, OPTION_GROUP_MARKER);
                 if (!in_group) {
                         in_group = group_marker && streq(group, opt->long_code);
@@ -455,6 +499,8 @@ int _option_parser_get_help_table(
                         return table_log_add_error(r);
         }
 
+        assert(!table_isempty(table));  /* The namespace or group were not found. Something is off. */
+
         table_set_header(table, false);
         *ret = TAKE_PTR(table);
         return 0;
index 81b92d2da5ff10533648a5b3ae037ece61f691d8..7d8507d0e54449321070f08377ea51655ba305af 100644 (file)
@@ -4,13 +4,21 @@
 #include "memory-util.h"
 #include "shared-forward.h"
 
+/* Option namespace/group explanation:
+ * the list of options is split into namespaces, and a namespace is split into groups.
+ * By default, options defined in a single program are all placed in a single (unnamed) namespace
+ * and in a single (unnamed) group. OPTION_NAMESPACE() marks the beginning of a named namespace.
+ * OPTION_GROUP() marks the beginning of a named group.
+ */
+
 typedef enum OptionFlags {
         OPTION_OPTIONAL_ARG        = 1U << 0,  /* Same as optional_argument in getopt */
         OPTION_POSITIONAL_ENTRY    = 1U << 1,  /* The "option" to handle positional arguments */
         OPTION_STOPS_PARSING       = 1U << 2,  /* This option acts like "--" */
-        OPTION_GROUP_MARKER        = 1U << 3,  /* Fake option entry to separate groups */
-        OPTION_HELP_ENTRY          = 1U << 4,  /* Fake option entry to insert an additional help line */
-        OPTION_HELP_ENTRY_VERBATIM = 1U << 5,  /* Same, but use the long_code in the first column as written */
+        OPTION_NAMESPACE_MARKER    = 1U << 3,  /* Fake option entry to separate namespaces */
+        OPTION_GROUP_MARKER        = 1U << 4,  /* Fake option entry to separate groups */
+        OPTION_HELP_ENTRY          = 1U << 5,  /* Fake option entry to insert an additional help line */
+        OPTION_HELP_ENTRY_VERBATIM = 1U << 6,  /* Same, but use the long_code in the first column as written */
 } OptionFlags;
 
 typedef struct Option {
@@ -41,9 +49,13 @@ typedef struct Option {
         };                                                              \
         case (0x100 + counter)
 
+/* Magic entry in the table (which will not be returned) that designates the start of the namespace <ns>.
+ * The define is structured as 'case' so that it can be followed by ':' and indented appropriately. */
+#define OPTION_NAMESPACE(ns)                                            \
+        _OPTION(__COUNTER__, OPTION_NAMESPACE_MARKER, /* sc= */ 0, /* lc= */ ns, /* mv= */ NULL, /* d= */ 0u, /* h= */ NULL)
+
 /* Magic entry in the table (which will not be returned) that designates the start of the group <gr>.
- * The define is structured as 'case' so that it can be followed by ':' and indented appropriately.
- */
+ * The define is structured as 'case' so that it can be followed by ':' and indented appropriately. */
 #define OPTION_GROUP(gr)                                                \
         _OPTION(__COUNTER__, OPTION_GROUP_MARKER, /* sc= */ 0, /* lc= */ gr, /* mv= */ NULL, /* d= */ 0u, /* h= */ NULL)
 
@@ -164,11 +176,15 @@ typedef enum OptionParserState {
 } OptionParserState;
 
 typedef struct OptionParser {
-        /* Those three should stay first so that it's possible to initialize the struct as { argc, argv }
-         * or { argc, argv, mode }. */
+        /* Those four should stay first so that it's possible to initialize the struct as { argc, argv }
+         * or { argc, argv, mode } or { argc, argv, mode, namespace }. */
         int argc;                     /* The original argc. */
         char **argv;                  /* The argv array, possibly reordered. */
         OptionParserMode mode;
+        const char *namespace;        /* The namespace, may be NULL. */
+
+        const Option *namespace_start, *namespace_end; /* The range of options that are part of our namespace. */
+
         OptionParserState state;
         int optind;                   /* Position of the parameter being handled.
                                        * 0 → option parsing hasn't been started yet. */
@@ -203,12 +219,15 @@ char** option_parser_get_args(const OptionParser *state);
 size_t option_parser_get_n_args(const OptionParser *state);
 char* option_get_synopsis(const char *prefix, const Option *opt, const char *joiner, bool show_metavar);
 
-int _option_parser_get_help_table(
+int _option_parser_get_help_table_full(
                 const Option options[],
                 const Option options_end[],
+                const char *namespace,
                 const char *group,
                 Table **ret);
+#define option_parser_get_help_table_full(namespace, group, ret)        \
+        _option_parser_get_help_table_full(ALIGN_PTR(__start_SYSTEMD_OPTIONS), __stop_SYSTEMD_OPTIONS, namespace, group, ret)
 #define option_parser_get_help_table_group(group, ret)                  \
-        _option_parser_get_help_table(ALIGN_PTR(__start_SYSTEMD_OPTIONS), __stop_SYSTEMD_OPTIONS, group, ret)
+        option_parser_get_help_table_full(/* namespace= */ NULL, group, ret)
 #define option_parser_get_help_table(ret)                               \
         option_parser_get_help_table_group(/* group= */ NULL, ret)
index 6a0744e67363d1eb47bb06e24fdef103ef07a950..a77964df4e1fa40234a6454167cf7e280331f3f2 100644 (file)
@@ -15,7 +15,8 @@ static void test_option_parse_one(
                 const Option options[],
                 const Entry *entries,
                 char **remaining,
-                OptionParserMode mode) {
+                OptionParserMode mode,
+                const char *namespace) {
 
         _cleanup_free_ char *joined = strv_join(argv, ", ");
         log_debug("/* %s(%s) */", __func__, joined);
@@ -32,7 +33,7 @@ static void test_option_parse_one(
         for (const Entry *e = entries; e && (e->long_code || e->short_code != 0); e++)
                 n_entries++;
 
-        OptionParser opts = { argc, argv, mode };
+        OptionParser opts = { argc, argv, mode, namespace };
         for (int c; (c = option_parse(options, options + n_options, &opts)) != 0; ) {
                 ASSERT_OK(c);
                 ASSERT_NOT_NULL(opts.opt);
@@ -90,6 +91,12 @@ TEST(option_parse) {
                 { 4, .long_code = "required2", .metavar = "ARG" },
                 { 5, .short_code = 'o', .long_code = "optional1", .metavar = "ARG", .flags = OPTION_OPTIONAL_ARG },
                 { 6, .long_code = "optional2", .metavar = "ARG", .flags = OPTION_OPTIONAL_ARG },
+                { 7, .long_code = "NS2", .flags = OPTION_NAMESPACE_MARKER },
+                { 8, .short_code = 'h', .long_code = "help2" },
+                { 9, .long_code = "version2" },
+                { 10, .long_code = "NS3", .flags = OPTION_NAMESPACE_MARKER },
+                { 11, .short_code = 'h', .long_code = "help3" },
+                { 12, .long_code = "version3" },
                 {}
         };
 
@@ -97,7 +104,8 @@ TEST(option_parse) {
                               options,
                               NULL,
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "string1",
@@ -110,7 +118,8 @@ TEST(option_parse) {
                                         "string2",
                                         "string3",
                                         "string4"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "--",
@@ -124,7 +133,8 @@ TEST(option_parse) {
                                         "--help",
                                         "-h",
                                         "string4"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "string1",
@@ -138,7 +148,8 @@ TEST(option_parse) {
                                         "string2",
                                         "--",
                                         "string4"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "string1",
@@ -152,7 +163,8 @@ TEST(option_parse) {
                                         "string2",
                                         "string3",
                                         "string4"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "--help"),
@@ -162,7 +174,8 @@ TEST(option_parse) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "--help",
@@ -175,7 +188,8 @@ TEST(option_parse) {
                               },
                               STRV_MAKE("string1",
                                         "--help"),
-                              OPTION_PARSER_STOP_AT_FIRST_NONOPTION);
+                              OPTION_PARSER_STOP_AT_FIRST_NONOPTION,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "-h"),
@@ -185,7 +199,8 @@ TEST(option_parse) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "--help",
@@ -202,7 +217,8 @@ TEST(option_parse) {
                                         "string2",
                                         "string3",
                                         "string4"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "-h",
@@ -219,7 +235,8 @@ TEST(option_parse) {
                                         "string2",
                                         "string3",
                                         "string4"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "string1",
@@ -236,7 +253,8 @@ TEST(option_parse) {
                                         "string2",
                                         "string3",
                                         "string4"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "string1",
@@ -253,7 +271,8 @@ TEST(option_parse) {
                                         "string2",
                                         "string3",
                                         "string4"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "string1",
@@ -270,7 +289,8 @@ TEST(option_parse) {
                                         "string2",
                                         "string3",
                                         "string4"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "string1",
@@ -287,7 +307,8 @@ TEST(option_parse) {
                                         "string2",
                                         "string3",
                                         "string4"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "--required1", "reqarg1"),
@@ -297,7 +318,8 @@ TEST(option_parse) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "-r", "reqarg1"),
@@ -307,7 +329,8 @@ TEST(option_parse) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "string1",
@@ -320,7 +343,8 @@ TEST(option_parse) {
                               },
                               STRV_MAKE("string1",
                                         "string2"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "string1",
@@ -331,7 +355,8 @@ TEST(option_parse) {
                               STRV_MAKE("string1",
                                         "string2",
                                         "-r", "reqarg1"),
-                              OPTION_PARSER_STOP_AT_FIRST_NONOPTION);
+                              OPTION_PARSER_STOP_AT_FIRST_NONOPTION,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "--optional1=optarg1"),
@@ -341,7 +366,8 @@ TEST(option_parse) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_STOP_AT_FIRST_NONOPTION);
+                              OPTION_PARSER_STOP_AT_FIRST_NONOPTION,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "--optional1", "string1"),
@@ -351,7 +377,8 @@ TEST(option_parse) {
                                       {}
                               },
                               STRV_MAKE("string1"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "-ooptarg1"),
@@ -361,7 +388,8 @@ TEST(option_parse) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "-o", "string1"),
@@ -371,7 +399,8 @@ TEST(option_parse) {
                                       {}
                               },
                               STRV_MAKE("string1"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         test_option_parse_one(STRV_MAKE("arg0",
                                         "string1",
@@ -441,7 +470,42 @@ TEST(option_parse) {
                                         "--help",
                                         "--required1",
                                         "--optional1"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
+
+        /* Check that we can access options from NS2 */
+        test_option_parse_one(STRV_MAKE("arg0",
+                                        "-h",   /* This verifies that we're using the right namespace */
+                                        "--help2",
+                                        "--version2",
+                                        "string1"),
+                              options,
+                              (Entry[]) {
+                                      { "help2"                 },
+                                      { "help2"                 },
+                                      { "version2"              },
+                                      {}
+                              },
+                              STRV_MAKE("string1"),
+                              OPTION_PARSER_NORMAL,
+                              "NS2");
+
+        /* Check that we can access options from NS3 */
+        test_option_parse_one(STRV_MAKE("arg0",
+                                        "-h",   /* This verifies that we're using the right namespace */
+                                        "--help3",
+                                        "--version3",
+                                        "string1"),
+                              options,
+                              (Entry[]) {
+                                      { "help3"                 },
+                                      { "help3"                 },
+                                      { "version3"              },
+                                      {}
+                              },
+                              STRV_MAKE("string1"),
+                              OPTION_PARSER_NORMAL,
+                              "NS3");
 }
 
 TEST(option_stops_parsing) {
@@ -465,7 +529,8 @@ TEST(option_stops_parsing) {
                               },
                               STRV_MAKE("--help",
                                         "foo"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* Options before --exec are still parsed */
         test_option_parse_one(STRV_MAKE("arg0",
@@ -481,7 +546,8 @@ TEST(option_stops_parsing) {
                               },
                               STRV_MAKE("--version",
                                         "bar"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* --exec with no trailing args */
         test_option_parse_one(STRV_MAKE("arg0",
@@ -492,7 +558,8 @@ TEST(option_stops_parsing) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* --exec after positional args */
         test_option_parse_one(STRV_MAKE("arg0",
@@ -509,7 +576,8 @@ TEST(option_stops_parsing) {
                                         "--help",
                                         "--required",
                                         "val"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* "--" after --exec: "--" is still consumed as end-of-options marker. This is needed for
          * backwards compatibility, systemd-dissect implemented this behaviour. But also, it makes
@@ -525,7 +593,8 @@ TEST(option_stops_parsing) {
                                       {}
                               },
                               STRV_MAKE("--help"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* "--" before --exec: "--" terminates first, --exec is positional */
         test_option_parse_one(STRV_MAKE("arg0",
@@ -536,7 +605,8 @@ TEST(option_stops_parsing) {
                               NULL,
                               STRV_MAKE("--exec",
                                         "--help"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* Multiple options then --exec then more option-like args */
         test_option_parse_one(STRV_MAKE("arg0",
@@ -555,7 +625,8 @@ TEST(option_stops_parsing) {
                               STRV_MAKE("-h",
                                         "--required",
                                         "val2"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 }
 
 TEST(option_group_marker) {
@@ -580,7 +651,8 @@ TEST(option_group_marker) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* Check that group marker name is ignored */
         test_option_parse_one(STRV_MAKE("arg0",
@@ -593,7 +665,8 @@ TEST(option_group_marker) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* Verify that the group marker is not mistaken for an option */
         test_option_invalid_one(STRV_MAKE("arg0",
@@ -621,7 +694,8 @@ TEST(option_group_marker) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* Partial match with multiple candidates */
         test_option_invalid_one(STRV_MAKE("arg0",
@@ -645,7 +719,8 @@ TEST(option_optional_arg) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* Long option without = does NOT consume the next arg */
         test_option_parse_one(STRV_MAKE("arg0",
@@ -656,7 +731,8 @@ TEST(option_optional_arg) {
                                       {}
                               },
                               STRV_MAKE("foo.txt"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* Short option with inline arg */
         test_option_parse_one(STRV_MAKE("arg0",
@@ -667,7 +743,8 @@ TEST(option_optional_arg) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* Short option without inline arg does NOT consume the next arg */
         test_option_parse_one(STRV_MAKE("arg0",
@@ -678,7 +755,8 @@ TEST(option_optional_arg) {
                                       {}
                               },
                               STRV_MAKE("foo.txt"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* Optional arg option at end of argv */
         test_option_parse_one(STRV_MAKE("arg0",
@@ -689,7 +767,8 @@ TEST(option_optional_arg) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* Mixed: optional arg with other options */
         test_option_parse_one(STRV_MAKE("arg0",
@@ -704,7 +783,8 @@ TEST(option_optional_arg) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* Short combo: -ho (h then o with no arg) */
         test_option_parse_one(STRV_MAKE("arg0",
@@ -716,7 +796,8 @@ TEST(option_optional_arg) {
                                       {}
                               },
                               STRV_MAKE("pos1"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* Short combo: -hobar (h then o with inline arg "bar") */
         test_option_parse_one(STRV_MAKE("arg0",
@@ -728,7 +809,8 @@ TEST(option_optional_arg) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 }
 
 /* Test the OPTION, OPTION_LONG, OPTION_SHORT, OPTION_FULL, OPTION_GROUP macros
@@ -738,7 +820,8 @@ static void test_macros_parse_one(
                 char **argv,
                 const Entry *entries,
                 char **remaining,
-                OptionParserMode mode) {
+                OptionParserMode mode,
+                const char *namespace) {
 
         _cleanup_free_ char *joined = strv_join(argv, ", ");
         log_debug("/* %s(%s) */", __func__, joined);
@@ -752,7 +835,7 @@ static void test_macros_parse_one(
         for (const Entry *e = entries; e && (e->long_code || e->short_code != 0); e++)
                 n_entries++;
 
-        OptionParser opts = { argc, argv, mode };
+        OptionParser opts = { argc, argv, mode, namespace };
 
         FOREACH_OPTION(c, &opts, assert_not_reached()) {
                 log_debug("%c %s: %s=%s",
@@ -814,9 +897,14 @@ static void test_macros_parse_one(
                 OPTION_POSITIONAL:
                         break;
 
+                OPTION_NAMESPACE("namespaced options"): {}
+
+                OPTION('r', "required2", "ARG", "Required arg option"):
+                        break;
+
                 default:
                         log_error("Unexpected option id: %d", c);
-                        ASSERT_TRUE(false);
+                        assert_not_reached();
                 }
         }
 
@@ -838,7 +926,8 @@ TEST(option_macros) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION: short form */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -848,7 +937,8 @@ TEST(option_macros) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION_LONG: only accessible via long form */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -858,7 +948,8 @@ TEST(option_macros) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION_SHORT: only accessible via short form */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -868,7 +959,8 @@ TEST(option_macros) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION with required arg: long --required=ARG */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -878,7 +970,8 @@ TEST(option_macros) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION with required arg: long --required ARG */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -888,7 +981,8 @@ TEST(option_macros) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION with required arg: short -r ARG */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -898,7 +992,8 @@ TEST(option_macros) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION with required arg: short -rARG */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -908,7 +1003,8 @@ TEST(option_macros) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION_FULL with OPTION_OPTIONAL_ARG: long with = */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -918,7 +1014,8 @@ TEST(option_macros) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION_FULL with OPTION_OPTIONAL_ARG: long without = doesn't consume next */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -928,7 +1025,8 @@ TEST(option_macros) {
                                       {}
                               },
                               STRV_MAKE("pos1"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION_FULL with OPTION_OPTIONAL_ARG: short inline */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -938,7 +1036,8 @@ TEST(option_macros) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION_FULL with OPTION_OPTIONAL_ARG: short without inline */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -948,7 +1047,8 @@ TEST(option_macros) {
                                       {}
                               },
                               STRV_MAKE("pos1"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION_FULL with OPTION_STOPS_PARSING: stops further option parsing */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -961,7 +1061,8 @@ TEST(option_macros) {
                               },
                               STRV_MAKE("--help",
                                         "--version"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION_STOPS_PARSING: options before are still parsed */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -976,7 +1077,8 @@ TEST(option_macros) {
                               },
                               STRV_MAKE("-h",
                                         "--debug"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION_STOPS_PARSING with "--": "--" after exec is still consumed */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -988,7 +1090,8 @@ TEST(option_macros) {
                                       {}
                               },
                               STRV_MAKE("--help"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION_STOPS_PARSING with "--": "--" before exec takes precedence */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1000,7 +1103,8 @@ TEST(option_macros) {
                               },
                               STRV_MAKE("--exec",
                                         "--help"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION_GROUP: group marker is transparent to parsing, --debug in Advanced group works */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1010,7 +1114,8 @@ TEST(option_macros) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* Mixed: all macro types together */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1039,7 +1144,8 @@ TEST(option_macros) {
                               },
                               STRV_MAKE("pos1",
                                         "pos2"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* Short option combos with macros: -hv (help + verbose) */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1050,7 +1156,8 @@ TEST(option_macros) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* Short option combo with required arg: -hrval (help + required with arg "val") */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1061,7 +1168,8 @@ TEST(option_macros) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* Short option combo with optional arg: -hoval (help + optional with arg "val") */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1072,7 +1180,8 @@ TEST(option_macros) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION_STOPS_PARSING then "--": "--" is still consumed after exec */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1088,7 +1197,8 @@ TEST(option_macros) {
                               },
                               STRV_MAKE("--version",
                                         "-h"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION_STOPS_PARSING then later "--": "--" is not consumed */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1105,7 +1215,8 @@ TEST(option_macros) {
                               STRV_MAKE("--version",
                                         "--",
                                         "-h"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* OPTION_STOPS_PARSING then "--" twice: second "--" is not consumed */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1123,7 +1234,8 @@ TEST(option_macros) {
                               STRV_MAKE("--",
                                         "--version",
                                         "-h"),
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* Basic OPTION_POSITIONAL use */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1139,7 +1251,8 @@ TEST(option_macros) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_RETURN_POSITIONAL_ARGS);
+                              OPTION_PARSER_RETURN_POSITIONAL_ARGS,
+                              NULL);
 
         /* OPTION_POSITIONAL combined with OPTION_STOPS_PARSING */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1154,7 +1267,30 @@ TEST(option_macros) {
                                       {}
                               },
                               STRV_MAKE("arg2"),
-                              OPTION_PARSER_RETURN_POSITIONAL_ARGS);
+                              OPTION_PARSER_RETURN_POSITIONAL_ARGS,
+                              NULL);
+
+        /* Second namespace, OPTION: long form */
+        test_macros_parse_one(STRV_MAKE("arg0",
+                                        "--required2=arg"),
+                              (Entry[]) {
+                                      { "required2", "arg" },
+                                      {}
+                              },
+                              NULL,
+                              OPTION_PARSER_NORMAL,
+                              "namespaced options");
+
+        /* Second namespace, OPTION: short form */
+        test_macros_parse_one(STRV_MAKE("arg0",
+                                        "-rarg"),
+                              (Entry[]) {
+                                      { "required2", "arg" },
+                                      {}
+                              },
+                              NULL,
+                              OPTION_PARSER_NORMAL,
+                              "namespaced options");
 }
 
 /* Test the pattern used by nspawn's --user: an optional-arg option that also
@@ -1176,7 +1312,8 @@ TEST(option_optional_arg_consume) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* --user without arg: next arg is an option, so no consumption */
         test_option_parse_one(STRV_MAKE("arg0",
@@ -1189,7 +1326,8 @@ TEST(option_optional_arg_consume) {
                                       {}
                               },
                               NULL,
-                              OPTION_PARSER_NORMAL);
+                              OPTION_PARSER_NORMAL,
+                              NULL);
 
         /* --user without arg: next arg is positional (doesn't start with -).
          * The option parser returns NULL for the arg. The caller would then