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.
This commit is contained in:
Zbigniew Jędrzejewski-Szmek
2026-04-07 23:56:22 +02:00
parent bf5bc9a7b2
commit f293418a50
3 changed files with 166 additions and 0 deletions
+22
View 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.
+3
View 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);
+141
View 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);