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:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user