From: Zbigniew Jędrzejewski-Szmek Date: Tue, 7 Apr 2026 15:48:25 +0000 (+0200) Subject: shared/options: add helper function to peek at or consume the next arg X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f293418a5035ec6ead2e28e80329e768d4b9b500;p=thirdparty%2Fsystemd.git shared/options: add helper function to peek at or consume the next arg The test was partially written with Claude Opus 4.6. It's a bit on the verbose side, but does the job. --- diff --git a/src/shared/options.c b/src/shared/options.c index f00d9674d7a..20ca7948e5b 100644 --- a/src/shared/options.c +++ b/src/shared/options.c @@ -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. diff --git a/src/shared/options.h b/src/shared/options.h index 1b0488579f9..e834cbededa 100644 --- a/src/shared/options.h +++ b/src/shared/options.h @@ -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); diff --git a/src/test/test-options.c b/src/test/test-options.c index a9cafbdd733..b91ac54884c 100644 --- a/src/test/test-options.c +++ b/src/test/test-options.c @@ -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);