]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
shared/options: add helper function to peek at or consume the next arg
authorZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Tue, 7 Apr 2026 15:48:25 +0000 (17:48 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Tue, 7 Apr 2026 21:56:22 +0000 (23:56 +0200)
The test was partially written with Claude Opus 4.6. It's a bit on the
verbose side, but does the job.

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

index f00d9674d7aff196b60147ecc5c3388fc479e54e..20ca7948e5b31b883154439f3506afe7598a266c 100644 (file)
@@ -239,6 +239,28 @@ int option_parse(
         return option->id;
 }
 
+char* option_parser_next_arg(const OptionParser *state) {
+        /* Peek at the next argument, whatever it is (option or position arg).
+         * May return NULL. */
+
+        assert(state->optind > 0);
+        assert(state->positional_offset <= state->argc);
+
+        return state->optind < state->argc ? state->argv[state->optind] : NULL;
+}
+
+char* option_parser_consume_next_arg(OptionParser *state) {
+        /* "Take" the next argument, whatever it is (option or position arg).
+         * The argument remains in the array, but the optind pointer is moved
+         * so we won't try to interpret it as an option.
+         * May return NULL. */
+
+        char *t = option_parser_next_arg(state);
+        if (t)
+                shift_arg(state->argv, state->positional_offset++, state->optind++);
+        return t;
+}
+
 char** option_parser_get_args(const OptionParser *state) {
         /* Returns positional args as a strv.
          * If "--" was found, it has been moved before state->positional_offset.
index 1b0488579f94b1c856acf494ffcea1c26c0dfcf3..e834cbededa4f776a4db26963c6438225c2baeaf 100644 (file)
@@ -113,6 +113,9 @@ int option_parse(
 #define FOREACH_OPTION(parser, opt, ret_a, on_error) \
         FOREACH_OPTION_FULL(parser, opt, /* ret_o= */ NULL, ret_a, on_error)
 
+char* option_parser_next_arg(const OptionParser *state);
+char* option_parser_consume_next_arg(OptionParser *state);
+
 char** option_parser_get_args(const OptionParser *state);
 size_t option_parser_get_n_args(const OptionParser *state);
 
index a9cafbdd73327b82aa7604bcea72ea6a2763258c..b91ac54884c6bb0d6e6993ec0714b01f420db931 100644 (file)
@@ -1091,4 +1091,145 @@ TEST(option_macros) {
                                         "-h"));
 }
 
+/* Test the pattern used by nspawn's --user: an optional-arg option that also
+ * peeks at the next arg to handle legacy "space-separated" form. */
+TEST(option_optional_arg_consume) {
+        static const Option options[] = {
+                { 1, .short_code = 'h', .long_code = "help" },
+                { 2, .long_code = "user", .metavar = "NAME", .flags = OPTION_OPTIONAL_ARG },
+                { 3, .short_code = 'u', .long_code = "uid", .metavar = "USER" },
+                {}
+        };
+
+        /* --user=NAME: optional arg provided via = */
+        test_option_parse_one(STRV_MAKE("arg0",
+                                        "--user=root"),
+                              options,
+                              (Entry[]) {
+                                      { "user", "root" },
+                                      {}
+                              },
+                              NULL,
+                              /* stop_at_first_nonoption= */ false);
+
+        /* --user without arg: next arg is an option, so no consumption */
+        test_option_parse_one(STRV_MAKE("arg0",
+                                        "--user",
+                                        "--help"),
+                              options,
+                              (Entry[]) {
+                                      { "user", NULL },
+                                      { "help" },
+                                      {}
+                              },
+                              NULL,
+                              /* stop_at_first_nonoption= */ false);
+
+        /* --user without arg: next arg is positional (doesn't start with -).
+         * The option parser returns NULL for the arg. The caller would then
+         * use option_parser_next_arg/consume_next_arg to grab it. */
+        {
+                char **argv = STRV_MAKE("arg0", "--user", "someuser", "pos1");
+                int argc = strv_length(argv);
+
+                OptionParser state = { argc, argv };
+                const Option *opt;
+                const char *arg;
+
+                ASSERT_OK_POSITIVE(option_parse(options, options + 3, &state, &opt, &arg));
+                ASSERT_STREQ(opt->long_code, "user");
+                ASSERT_NULL(arg);
+                ASSERT_STREQ(option_parser_next_arg(&state), "someuser");
+                ASSERT_STREQ(option_parser_consume_next_arg(&state), "someuser");
+
+                ASSERT_EQ(option_parse(options, options + 3, &state, &opt, &arg), 0);
+
+                ASSERT_TRUE(strv_equal(option_parser_get_args(&state), STRV_MAKE("pos1")));
+        }
+
+        /* --user at end of args: no next arg, so scope mode */
+        {
+                char **argv = STRV_MAKE("arg0", "--user");
+                int argc = strv_length(argv);
+
+                OptionParser state = { argc, argv };
+                const Option *opt;
+                const char *arg;
+
+                ASSERT_OK_POSITIVE(option_parse(options, options + 3, &state, &opt, &arg));
+                ASSERT_STREQ(opt->long_code, "user");
+                ASSERT_NULL(arg);
+                ASSERT_NULL(option_parser_next_arg(&state));
+                ASSERT_NULL(option_parser_consume_next_arg(&state));
+
+                ASSERT_EQ(option_parse(options, options + 3, &state, &opt, &arg), 0);
+
+                ASSERT_TRUE(strv_isempty(option_parser_get_args(&state)));
+        }
+
+        /* --user followed by -u (option): scope mode, -u gets its own processing */
+        {
+                char **argv = STRV_MAKE("arg0", "--user", "-u", "nobody");
+                int argc = strv_length(argv);
+
+                OptionParser state = { argc, argv };
+                const Option *opt;
+                const char *arg;
+
+                ASSERT_OK_POSITIVE(option_parse(options, options + 3, &state, &opt, &arg));
+                ASSERT_STREQ(opt->long_code, "user");
+                ASSERT_NULL(arg);
+                ASSERT_STREQ(option_parser_next_arg(&state), "-u");
+
+                ASSERT_OK_POSITIVE(option_parse(options, options + 3, &state, &opt, &arg));
+                ASSERT_STREQ(opt->long_code, "uid");
+                ASSERT_STREQ(arg, "nobody");
+                ASSERT_NULL(option_parser_next_arg(&state));
+                ASSERT_NULL(option_parser_consume_next_arg(&state));
+
+                ASSERT_EQ(option_parse(options, options + 3, &state, &opt, &arg), 0);
+
+                ASSERT_TRUE(strv_isempty(option_parser_get_args(&state)));
+        }
+
+        /* "Functional test": --user followed by -u (option): scope mode, -u gets its own processing,
+         * handled like in a real option parser. */
+        {
+                char **argv = STRV_MAKE("arg0", "--user", "-u", "nobody", "nogroup", "--user=nobody", "--user");
+                int argc = strv_length(argv);
+
+                OptionParser state = { argc, argv };
+                const Option *opt;
+                const char *arg;
+                int scope_seen = 0;
+                int nobody_seen = 0;
+
+                for (int c; (c = option_parse(options, options + 3, &state, &opt, &arg)) != 0; ) {
+                        ASSERT_OK(c);
+
+                        if (streq_ptr(opt->long_code, "user")) {
+                                if (!arg) {
+                                        const char *t = option_parser_next_arg(&state);
+                                        if (t && t[0] != '-')
+                                                arg = option_parser_consume_next_arg(&state);
+                                }
+
+                                if (arg) {
+                                        ASSERT_STREQ(arg, "nobody");
+                                        nobody_seen ++;
+                                } else
+                                        scope_seen ++;
+
+                        } else if (streq_ptr(opt->long_code, "uid")) {
+                                ASSERT_STREQ(arg, "nobody");
+                                nobody_seen ++;
+                        }
+                }
+
+                ASSERT_EQ(nobody_seen, 2);
+                ASSERT_EQ(scope_seen, 2);
+                ASSERT_TRUE(strv_equal(option_parser_get_args(&state), STRV_MAKE("nogroup")));
+        }
+}
+
 DEFINE_TEST_MAIN(LOG_DEBUG);