typedef struct Entry {
const char *long_code;
const char *argument;
+ char short_code;
} Entry;
static void test_option_parse_one(
for (const Option *o = options; o->short_code != 0 || o->long_code; o++)
n_options++;
- for (const Entry *e = entries; e && e->long_code; e++)
+ for (const Entry *e = entries; e && (e->long_code || e->short_code != 0); e++)
n_entries++;
OptionParser state = {};
strnull(opt->metavar), strnull(arg));
ASSERT_LT(i, n_entries);
- ASSERT_TRUE(streq_ptr(opt->long_code, entries[i].long_code));
+ if (entries[i].long_code)
+ ASSERT_TRUE(streq_ptr(opt->long_code, entries[i].long_code));
+ if (entries[i].short_code != 0)
+ ASSERT_EQ(opt->short_code, entries[i].short_code);
ASSERT_TRUE(streq_ptr(arg, entries[i].argument));
i++;
}
ASSERT_STREQ(argv[0], saved_argv0);
}
+static void test_option_invalid_one(
+ char **argv,
+ const Option options[static 1]) {
+
+ _cleanup_free_ char *joined = strv_join(argv, ", ");
+ log_debug("/* %s(%s) */", __func__, joined);
+
+ _cleanup_free_ char *saved_argv0 = NULL;
+ ASSERT_NOT_NULL(saved_argv0 = strdup(argv[0]));
+
+ int argc = strv_length(argv);
+
+ size_t n_options = 0;
+ for (const Option *o = options; o->short_code != 0 || o->long_code; o++)
+ n_options++;
+
+ OptionParser state = {};
+ const Option *opt;
+ const char *arg;
+
+ int c = option_parse(options, options + n_options, &state, argc, argv, &opt, &arg);
+ ASSERT_ERROR(c, EINVAL);
+}
+
TEST(option_parse) {
static const Option options[] = {
{ 1, .short_code = 'h', .long_code = "help" },
"--optional1"));
}
+TEST(option_stops_parsing) {
+ static const Option options[] = {
+ { 1, .short_code = 'h', .long_code = "help" },
+ { 2, .long_code = "version" },
+ { 3, .short_code = 'r', .long_code = "required", .metavar = "ARG" },
+ { 4, .long_code = "exec", .flags = OPTION_STOPS_PARSING },
+ {}
+ };
+
+ /* --exec stops parsing, subsequent --help is positional */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "--exec",
+ "--help",
+ "foo"),
+ options,
+ (Entry[]) {
+ { "exec" },
+ {}
+ },
+ STRV_MAKE("--help",
+ "foo"));
+
+ /* Options before --exec are still parsed */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "--help",
+ "--exec",
+ "--version",
+ "bar"),
+ options,
+ (Entry[]) {
+ { "help" },
+ { "exec" },
+ {}
+ },
+ STRV_MAKE("--version",
+ "bar"));
+
+ /* --exec with no trailing args */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "--exec"),
+ options,
+ (Entry[]) {
+ { "exec" },
+ {}
+ },
+ NULL);
+
+ /* --exec after positional args */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "pos1",
+ "--exec",
+ "--help",
+ "--required", "val"),
+ options,
+ (Entry[]) {
+ { "exec" },
+ {}
+ },
+ STRV_MAKE("pos1",
+ "--help",
+ "--required",
+ "val"));
+
+ /* "--" 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
+ * sense: we're unlikely to ever want to specify "--" as the first argument of whatever
+ * sequence, but the user may want to specify it for clarity. */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "--exec",
+ "--",
+ "--help"),
+ options,
+ (Entry[]) {
+ { "exec" },
+ {}
+ },
+ STRV_MAKE("--help"));
+
+ /* "--" before --exec: "--" terminates first, --exec is positional */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "--",
+ "--exec",
+ "--help"),
+ options,
+ NULL,
+ STRV_MAKE("--exec",
+ "--help"));
+
+ /* Multiple options then --exec then more option-like args */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "--help",
+ "-r", "val1",
+ "--exec",
+ "-h",
+ "--required", "val2"),
+ options,
+ (Entry[]) {
+ { "help" },
+ { "required", "val1" },
+ { "exec" },
+ {}
+ },
+ STRV_MAKE("-h",
+ "--required",
+ "val2"));
+}
+
+TEST(option_group_marker) {
+ static const Option options[] = {
+ { 1, .short_code = 'h', .long_code = "help" },
+ { 2, .long_code = "version" },
+ { 0, .long_code = "AdvancedGroup", .flags = OPTION_GROUP_MARKER },
+ { 3, .long_code = "debug" },
+ { 4, .long_code = "Advance" }, /* prefix match with the group */
+ { 5, .long_code = "defilbrilate" },
+ {}
+ };
+
+ /* Group markers are skipped by the parser — only real options are returned */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "--help",
+ "--debug"),
+ options,
+ (Entry[]) {
+ { "help" },
+ { "debug" },
+ {}
+ },
+ NULL);
+
+ /* Check that group marker name is ignored */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "--debug",
+ "--version"),
+ options,
+ (Entry[]) {
+ { "debug" },
+ { "version" },
+ {}
+ },
+ NULL);
+
+ /* Verify that the group marker is not mistaken for an option */
+ test_option_invalid_one(STRV_MAKE("arg0",
+ "--AdvancedGroup"),
+ options);
+
+ /* Verify that the group marker is not mistaken for an option */
+ test_option_invalid_one(STRV_MAKE("arg0",
+ "--AdvancedGroup=2"),
+ options);
+
+ /* Verify that the group marker is not mistaken for an option, prefix match */
+ test_option_invalid_one(STRV_MAKE("arg0",
+ "--Advanced"),
+ options);
+
+ /* Check that group marker name is ignored */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "--Advance",
+ "--Advan"), /* prefix match with unique prefix */
+ options,
+ (Entry[]) {
+ { "Advance" },
+ { "Advance" },
+ {}
+ },
+ NULL);
+
+ /* Partial match with multiple candidates */
+ test_option_invalid_one(STRV_MAKE("arg0",
+ "--de"),
+ options);
+}
+
+TEST(option_optional_arg) {
+ static const Option options[] = {
+ { 1, .short_code = 'o', .long_code = "output", .metavar = "FILE", .flags = OPTION_OPTIONAL_ARG },
+ { 2, .short_code = 'h', .long_code = "help" },
+ {}
+ };
+
+ /* Long option with = gets the argument */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "--output=foo.txt"),
+ options,
+ (Entry[]) {
+ { "output", "foo.txt" },
+ {}
+ },
+ NULL);
+
+ /* Long option without = does NOT consume the next arg */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "--output", "foo.txt"),
+ options,
+ (Entry[]) {
+ { "output", NULL },
+ {}
+ },
+ STRV_MAKE("foo.txt"));
+
+ /* Short option with inline arg */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "-ofoo.txt"),
+ options,
+ (Entry[]) {
+ { "output", "foo.txt" },
+ {}
+ },
+ NULL);
+
+ /* Short option without inline arg does NOT consume the next arg */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "-o", "foo.txt"),
+ options,
+ (Entry[]) {
+ { "output", NULL },
+ {}
+ },
+ STRV_MAKE("foo.txt"));
+
+ /* Optional arg option at end of argv */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "--output"),
+ options,
+ (Entry[]) {
+ { "output", NULL },
+ {}
+ },
+ NULL);
+
+ /* Mixed: optional arg with other options */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "--help",
+ "--output=bar",
+ "--help"),
+ options,
+ (Entry[]) {
+ { "help" },
+ { "output", "bar" },
+ { "help" },
+ {}
+ },
+ NULL);
+
+ /* Short combo: -ho (h then o with no arg) */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "-ho", "pos1"),
+ options,
+ (Entry[]) {
+ { "help" },
+ { "output", NULL },
+ {}
+ },
+ STRV_MAKE("pos1"));
+
+ /* Short combo: -hobar (h then o with inline arg "bar") */
+ test_option_parse_one(STRV_MAKE("arg0",
+ "-hobar"),
+ options,
+ (Entry[]) {
+ { "help" },
+ { "output", "bar" },
+ {}
+ },
+ NULL);
+}
+
+/* Test the OPTION, OPTION_LONG, OPTION_SHORT, OPTION_FULL, OPTION_GROUP macros
+ * by using them in a FOREACH_OPTION_FULL switch, as they would be used in real code. */
+
+static void test_macros_parse_one(
+ char **argv,
+ const Entry *entries,
+ char **remaining) {
+
+ _cleanup_free_ char *joined = strv_join(argv, ", ");
+ log_debug("/* %s(%s) */", __func__, joined);
+
+ _cleanup_free_ char *saved_argv0 = NULL;
+ ASSERT_NOT_NULL(saved_argv0 = strdup(argv[0]));
+
+ int argc = strv_length(argv);
+ size_t i = 0, n_entries = 0;
+
+ for (const Entry *e = entries; e && (e->long_code || e->short_code != 0); e++)
+ n_entries++;
+
+ OptionParser state = {};
+ const Option *opt;
+ const char *arg;
+
+ FOREACH_OPTION_FULL(&state, c, argc, argv, &opt, &arg, ASSERT_TRUE(false)) {
+ log_debug("%c %s: %s=%s",
+ opt->short_code != 0 ? opt->short_code : ' ',
+ opt->long_code ?: "",
+ strnull(opt->metavar), strnull(arg));
+
+ ASSERT_LT(i, n_entries);
+ if (entries[i].long_code)
+ ASSERT_TRUE(streq_ptr(opt->long_code, entries[i].long_code));
+ if (entries[i].short_code != 0)
+ ASSERT_EQ(opt->short_code, entries[i].short_code);
+ ASSERT_TRUE(streq_ptr(arg, entries[i].argument));
+ i++;
+
+ switch (c) {
+
+ /* OPTION: short + long, no arg */
+ OPTION('h', "help", NULL, "Show this help"):
+ break;
+
+ /* OPTION_LONG: long only, no arg */
+ OPTION_LONG("version", NULL, "Show package version"):
+ break;
+
+ /* OPTION_SHORT: short only, no arg */
+ OPTION_SHORT('v', NULL, "Enable verbose mode"):
+ break;
+
+ /* OPTION: short + long, required arg */
+ OPTION('r', "required", "ARG", "Required arg option"):
+ break;
+
+ /* OPTION_FULL: optional arg */
+ OPTION_FULL(OPTION_OPTIONAL_ARG, 'o', "optional", "ARG", "Optional arg option"):
+ break;
+
+ /* OPTION_FULL: stops parsing */
+ OPTION_FULL(OPTION_STOPS_PARSING, 0, "exec", NULL, "Stop parsing after this"):
+ break;
+
+ /* OPTION_GROUP: group marker (never returned by parser) */
+ OPTION_GROUP("Advanced"):
+ break;
+
+ /* OPTION_LONG: long only, in the "Advanced" group */
+ OPTION_LONG("debug", NULL, "Enable debug mode"):
+ break;
+
+ default:
+ log_error("Unexpected option id: %d", c);
+ ASSERT_TRUE(false);
+ }
+ }
+
+ ASSERT_EQ(i, n_entries);
+
+ char **args = option_parser_get_args(&state, argc, argv);
+ ASSERT_TRUE(strv_equal(args, remaining));
+ ASSERT_STREQ(argv[0], saved_argv0);
+}
+
+TEST(option_macros) {
+ /* OPTION: long form */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "--help"),
+ (Entry[]) {
+ { "help" },
+ {}
+ },
+ NULL);
+
+ /* OPTION: short form */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "-h"),
+ (Entry[]) {
+ { "help" },
+ {}
+ },
+ NULL);
+
+ /* OPTION_LONG: only accessible via long form */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "--version"),
+ (Entry[]) {
+ { "version" },
+ {}
+ },
+ NULL);
+
+ /* OPTION_SHORT: only accessible via short form */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "-v"),
+ (Entry[]) {
+ { .short_code = 'v' },
+ {}
+ },
+ NULL);
+
+ /* OPTION with required arg: long --required=ARG */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "--required=val1"),
+ (Entry[]) {
+ { "required", "val1" },
+ {}
+ },
+ NULL);
+
+ /* OPTION with required arg: long --required ARG */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "--required", "val1"),
+ (Entry[]) {
+ { "required", "val1" },
+ {}
+ },
+ NULL);
+
+ /* OPTION with required arg: short -r ARG */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "-r", "val1"),
+ (Entry[]) {
+ { "required", "val1" },
+ {}
+ },
+ NULL);
+
+ /* OPTION with required arg: short -rARG */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "-rval1"),
+ (Entry[]) {
+ { "required", "val1" },
+ {}
+ },
+ NULL);
+
+ /* OPTION_FULL with OPTION_OPTIONAL_ARG: long with = */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "--optional=val1"),
+ (Entry[]) {
+ { "optional", "val1" },
+ {}
+ },
+ NULL);
+
+ /* OPTION_FULL with OPTION_OPTIONAL_ARG: long without = doesn't consume next */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "--optional", "pos1"),
+ (Entry[]) {
+ { "optional", NULL },
+ {}
+ },
+ STRV_MAKE("pos1"));
+
+ /* OPTION_FULL with OPTION_OPTIONAL_ARG: short inline */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "-oval1"),
+ (Entry[]) {
+ { "optional", "val1" },
+ {}
+ },
+ NULL);
+
+ /* OPTION_FULL with OPTION_OPTIONAL_ARG: short without inline */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "-o", "pos1"),
+ (Entry[]) {
+ { "optional", NULL },
+ {}
+ },
+ STRV_MAKE("pos1"));
+
+ /* OPTION_FULL with OPTION_STOPS_PARSING: stops further option parsing */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "--exec",
+ "--help",
+ "--version"),
+ (Entry[]) {
+ { "exec" },
+ {}
+ },
+ STRV_MAKE("--help",
+ "--version"));
+
+ /* OPTION_STOPS_PARSING: options before are still parsed */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "--help",
+ "--exec",
+ "-h",
+ "--debug"),
+ (Entry[]) {
+ { "help" },
+ { "exec" },
+ {}
+ },
+ STRV_MAKE("-h",
+ "--debug"));
+
+ /* OPTION_STOPS_PARSING with "--": "--" after exec is still consumed */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "--exec",
+ "--",
+ "--help"),
+ (Entry[]) {
+ { "exec" },
+ {}
+ },
+ STRV_MAKE("--help"));
+
+ /* OPTION_STOPS_PARSING with "--": "--" before exec takes precedence */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "--",
+ "--exec",
+ "--help"),
+ (Entry[]) {
+ {}
+ },
+ STRV_MAKE("--exec",
+ "--help"));
+
+ /* OPTION_GROUP: group marker is transparent to parsing, --debug in Advanced group works */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "--debug"),
+ (Entry[]) {
+ { "debug" },
+ {}
+ },
+ NULL);
+
+ /* Mixed: all macro types together */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "pos1",
+ "-h",
+ "--version",
+ "-v",
+ "--required=rval",
+ "--optional=oval",
+ "--debug",
+ "pos2",
+ "-o",
+ "--help"),
+ (Entry[]) {
+ { "help" },
+ { "version" },
+ { .short_code = 'v' },
+ { "required", "rval" },
+ { "optional", "oval" },
+ { "debug" },
+ { "optional", NULL },
+ { "help" },
+ {}
+ },
+ STRV_MAKE("pos1",
+ "pos2"));
+
+ /* Short option combos with macros: -hv (help + verbose) */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "-hv"),
+ (Entry[]) {
+ { "help" },
+ { .short_code = 'v' },
+ {}
+ },
+ NULL);
+
+ /* Short option combo with required arg: -hrval (help + required with arg "val") */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "-hrval"),
+ (Entry[]) {
+ { "help" },
+ { "required", "val" },
+ {}
+ },
+ NULL);
+
+ /* Short option combo with optional arg: -hoval (help + optional with arg "val") */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "-hoval"),
+ (Entry[]) {
+ { "help" },
+ { "optional", "val" },
+ {}
+ },
+ NULL);
+
+ /* OPTION_STOPS_PARSING then "--": "--" is still consumed after exec */
+ test_macros_parse_one(STRV_MAKE("arg0",
+ "--help",
+ "--exec",
+ "--version",
+ "--",
+ "-h"),
+ (Entry[]) {
+ { "help" },
+ { "exec" },
+ {}
+ },
+ STRV_MAKE("--version",
+ "-h"));
+}
+
DEFINE_TEST_MAIN(LOG_DEBUG);