]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
shared/options: add equivalent of "-" in getopt_long
authorZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Wed, 15 Apr 2026 20:52:06 +0000 (22:52 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Thu, 16 Apr 2026 09:48:35 +0000 (11:48 +0200)
The parsing "mode" is specified as an exclusive mode, i.e. a combination
of "+" and "-" is not supported. In principle this could be supported,
but we don't use that in our code and are unlikely ever to do so.

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

index 5d4df946b83fb3a010fa5f8370ae8ce9f0236d79..fbf9a3118175435094e4aa043e83777cccacd5fe 100644 (file)
@@ -22,6 +22,7 @@ static bool option_arg_required(const Option *opt) {
 static bool option_is_metadata(const Option *opt) {
         /* A metadata entry that is not a real option, like the group marker */
         return ASSERT_PTR(opt)->flags & (OPTION_GROUP_MARKER |
+                                         OPTION_POSITIONAL_ENTRY |
                                          OPTION_HELP_ENTRY |
                                          OPTION_HELP_ENTRY_VERBATIM);
 }
@@ -89,9 +90,10 @@ int option_parse(
         const char *optname = NULL, *optval = NULL;
         _cleanup_free_ char *_optname = NULL;  /* allocated option name */
         bool separate_optval = false;
+        bool handling_positional_arg = false;
 
         if (state->short_option_offset == 0) {
-                /* Skip over non-option parameters */
+                /* Handle non-option parameters */
                 for (;;) {
                         if (state->optind == state->argc)
                                 return 0;
@@ -116,14 +118,31 @@ int option_parse(
                                 return 0;
                         }
 
+                        if (state->mode == OPTION_PARSER_RETURN_POSITIONAL_ARGS) {
+                                handling_positional_arg = true;
+                                optval = state->argv[state->optind];
+                                break;
+                        }
+
                         state->optind++;
                 }
 
                 /* Find matching option entry.
                  * First, figure out if we have a long option or a short option. */
-                assert(state->argv[state->optind][0] == '-');
+                assert(handling_positional_arg || state->argv[state->optind][0] == '-');
+
+                if (handling_positional_arg)
+                        /* We are supposed to return the positional arg to be handled. */
+                        for (option = options;; option++) {
+                                /* If OPTION_PARSER_RETURN_POSITIONAL_ARGS is specified,
+                                 * OPTION_POSITIONAL must be used. */
+                                assert(option < options_end);
 
-                if (state->argv[state->optind][1] == '-') {
+                                if (FLAGS_SET(option->flags, OPTION_POSITIONAL_ENTRY))
+                                        break;
+                        }
+
+                else if (state->argv[state->optind][1] == '-') {
                         /* We have a long option. */
                         char *eq = strchr(state->argv[state->optind], '=');
                         if (eq) {
@@ -206,11 +225,11 @@ int option_parse(
 
         assert(option);
 
-        if (optval && !option_takes_arg(option))
+        if (!handling_positional_arg && optval && !option_takes_arg(option))
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                        "%s: option '%s' doesn't allow an argument",
                                        program_invocation_short_name, optname);
-        if (!optval && option_arg_required(option)) {
+        if (!handling_positional_arg && !optval && option_arg_required(option)) {
                 if (!state->argv[state->optind + 1])
                         return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                                "%s: option '%s' requires an argument",
@@ -220,7 +239,13 @@ int option_parse(
         }
 
         if (state->short_option_offset == 0) {
-                /* We're done with this option. Adjust the array and position. */
+                /* We're done with this parameter. Adjust the array and position. */
+                if (handling_positional_arg) {
+                        /* Sanity check */
+                        assert(state->positional_offset == state->optind);
+                        assert(!separate_optval);
+                }
+
                 shift_arg(state->argv, state->positional_offset++, state->optind++);
                 if (separate_optval)
                         shift_arg(state->argv, state->positional_offset++, state->optind++);
index a10f914da266c6eb9aa6ed8aecebb970ac7a1728..cd9f64fd4949af655367a5356d9edf9ad426f97e 100644 (file)
@@ -6,10 +6,11 @@
 
 typedef enum OptionFlags {
         OPTION_OPTIONAL_ARG        = 1U << 0,  /* Same as optional_argument in getopt */
-        OPTION_STOPS_PARSING       = 1U << 1,  /* This option acts like "--" */
-        OPTION_GROUP_MARKER        = 1U << 2,  /* Fake option entry to separate groups */
-        OPTION_HELP_ENTRY          = 1U << 3,  /* Fake option entry to insert an additional help line */
-        OPTION_HELP_ENTRY_VERBATIM = 1U << 4,  /* Same, but use the long_code in the first column as written */
+        OPTION_POSITIONAL_ENTRY    = 1U << 1,  /* The "option" to handle positional arguments */
+        OPTION_STOPS_PARSING       = 1U << 2,  /* This option acts like "--" */
+        OPTION_GROUP_MARKER        = 1U << 3,  /* Fake option entry to separate groups */
+        OPTION_HELP_ENTRY          = 1U << 4,  /* Fake option entry to insert an additional help line */
+        OPTION_HELP_ENTRY_VERBATIM = 1U << 5,  /* Same, but use the long_code in the first column as written */
 } OptionFlags;
 
 typedef struct Option {
@@ -50,6 +51,7 @@ typedef struct Option {
 #define OPTION_LONG_FLAGS(fl, lc, mv, h) OPTION_FULL(fl, /* sc= */ 0, lc, mv, h)
 #define OPTION_SHORT(sc, mv, h) OPTION(sc, /* lc= */ NULL, mv, h)
 #define OPTION_SHORT_FLAGS(fl, sc, mv, h) OPTION_FULL(fl, sc, /* lc= */ NULL, mv, h)
+#define OPTION_POSITIONAL OPTION_FULL(OPTION_POSITIONAL_ENTRY, /* sc= */ 0, "(positional)", /* mv= */ NULL, /* h= */ NULL)
 #define OPTION_HELP_VERBATIM(lc, h) OPTION_FULL(OPTION_HELP_ENTRY_VERBATIM, /* sc= */ 0, lc, /* mv= */ NULL, h)
 
 #define OPTION_COMMON_HELP \
@@ -99,6 +101,10 @@ typedef enum OptionParserMode {
         /* Same as "+…" for getopt_long — only parse options before the first positional argument. */
         OPTION_PARSER_STOP_AT_FIRST_NONOPTION,
 
+        /* Same as "-…" for getopt_long — return positional arguments as "options" to be handled by the
+         * option handler specified with OPTION_POSITIONAL. */
+        OPTION_PARSER_RETURN_POSITIONAL_ARGS,
+
         _OPTION_PARSER_MODE_MAX,
 } OptionParserMode;
 
index e6b18d7a7eaeb3e4e82daa310bd1ae5ce29ea98f..05fd4bcbe0761c0b45322ead85a985678897fd9b 100644 (file)
@@ -741,7 +741,8 @@ TEST(option_optional_arg) {
 static void test_macros_parse_one(
                 char **argv,
                 const Entry *entries,
-                char **remaining) {
+                char **remaining,
+                OptionParserMode mode) {
 
         _cleanup_free_ char *joined = strv_join(argv, ", ");
         log_debug("/* %s(%s) */", __func__, joined);
@@ -755,7 +756,7 @@ static void test_macros_parse_one(
         for (const Entry *e = entries; e && (e->long_code || e->short_code != 0); e++)
                 n_entries++;
 
-        OptionParser state = { argc, argv };
+        OptionParser state = { argc, argv, mode };
         const Option *opt;
         const char *arg;
 
@@ -807,6 +808,9 @@ static void test_macros_parse_one(
                 OPTION_LONG("debug", NULL, "Enable debug mode"):
                         break;
 
+                OPTION_POSITIONAL:
+                        break;
+
                 default:
                         log_error("Unexpected option id: %d", c);
                         ASSERT_TRUE(false);
@@ -828,7 +832,8 @@ TEST(option_macros) {
                                       { "help" },
                                       {}
                               },
-                              NULL);
+                              NULL,
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION: short form */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -837,7 +842,8 @@ TEST(option_macros) {
                                       { "help" },
                                       {}
                               },
-                              NULL);
+                              NULL,
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION_LONG: only accessible via long form */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -846,7 +852,8 @@ TEST(option_macros) {
                                       { "version" },
                                       {}
                               },
-                              NULL);
+                              NULL,
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION_SHORT: only accessible via short form */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -855,7 +862,8 @@ TEST(option_macros) {
                                       { .short_code = 'v' },
                                       {}
                               },
-                              NULL);
+                              NULL,
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION with required arg: long --required=ARG */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -864,7 +872,8 @@ TEST(option_macros) {
                                       { "required", "val1" },
                                       {}
                               },
-                              NULL);
+                              NULL,
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION with required arg: long --required ARG */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -873,7 +882,8 @@ TEST(option_macros) {
                                       { "required", "val1" },
                                       {}
                               },
-                              NULL);
+                              NULL,
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION with required arg: short -r ARG */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -882,7 +892,8 @@ TEST(option_macros) {
                                       { "required", "val1" },
                                       {}
                               },
-                              NULL);
+                              NULL,
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION with required arg: short -rARG */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -891,7 +902,8 @@ TEST(option_macros) {
                                       { "required", "val1" },
                                       {}
                               },
-                              NULL);
+                              NULL,
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION_FULL with OPTION_OPTIONAL_ARG: long with = */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -900,7 +912,8 @@ TEST(option_macros) {
                                       { "optional", "val1" },
                                       {}
                               },
-                              NULL);
+                              NULL,
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION_FULL with OPTION_OPTIONAL_ARG: long without = doesn't consume next */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -909,7 +922,8 @@ TEST(option_macros) {
                                       { "optional", NULL },
                                       {}
                               },
-                              STRV_MAKE("pos1"));
+                              STRV_MAKE("pos1"),
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION_FULL with OPTION_OPTIONAL_ARG: short inline */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -918,7 +932,8 @@ TEST(option_macros) {
                                       { "optional", "val1" },
                                       {}
                               },
-                              NULL);
+                              NULL,
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION_FULL with OPTION_OPTIONAL_ARG: short without inline */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -927,7 +942,8 @@ TEST(option_macros) {
                                       { "optional", NULL },
                                       {}
                               },
-                              STRV_MAKE("pos1"));
+                              STRV_MAKE("pos1"),
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION_FULL with OPTION_STOPS_PARSING: stops further option parsing */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -939,7 +955,8 @@ TEST(option_macros) {
                                       {}
                               },
                               STRV_MAKE("--help",
-                                        "--version"));
+                                        "--version"),
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION_STOPS_PARSING: options before are still parsed */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -953,7 +970,8 @@ TEST(option_macros) {
                                       {}
                               },
                               STRV_MAKE("-h",
-                                        "--debug"));
+                                        "--debug"),
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION_STOPS_PARSING with "--": "--" after exec is still consumed */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -964,7 +982,8 @@ TEST(option_macros) {
                                       { "exec" },
                                       {}
                               },
-                              STRV_MAKE("--help"));
+                              STRV_MAKE("--help"),
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION_STOPS_PARSING with "--": "--" before exec takes precedence */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -975,7 +994,8 @@ TEST(option_macros) {
                                       {}
                               },
                               STRV_MAKE("--exec",
-                                        "--help"));
+                                        "--help"),
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION_GROUP: group marker is transparent to parsing, --debug in Advanced group works */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -984,7 +1004,8 @@ TEST(option_macros) {
                                       { "debug" },
                                       {}
                               },
-                              NULL);
+                              NULL,
+                              OPTION_PARSER_NORMAL);
 
         /* Mixed: all macro types together */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1010,7 +1031,8 @@ TEST(option_macros) {
                                       {}
                               },
                               STRV_MAKE("pos1",
-                                        "pos2"));
+                                        "pos2"),
+                              OPTION_PARSER_NORMAL);
 
         /* Short option combos with macros: -hv (help + verbose) */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1020,7 +1042,8 @@ TEST(option_macros) {
                                       { .short_code = 'v' },
                                       {}
                               },
-                              NULL);
+                              NULL,
+                              OPTION_PARSER_NORMAL);
 
         /* Short option combo with required arg: -hrval (help + required with arg "val") */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1030,7 +1053,8 @@ TEST(option_macros) {
                                       { "required", "val" },
                                       {}
                               },
-                              NULL);
+                              NULL,
+                              OPTION_PARSER_NORMAL);
 
         /* Short option combo with optional arg: -hoval (help + optional with arg "val") */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1040,7 +1064,8 @@ TEST(option_macros) {
                                       { "optional", "val" },
                                       {}
                               },
-                              NULL);
+                              NULL,
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION_STOPS_PARSING then "--": "--" is still consumed after exec */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1055,7 +1080,8 @@ TEST(option_macros) {
                                       {}
                               },
                               STRV_MAKE("--version",
-                                        "-h"));
+                                        "-h"),
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION_STOPS_PARSING then later "--": "--" is not consumed */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1071,7 +1097,8 @@ TEST(option_macros) {
                               },
                               STRV_MAKE("--version",
                                         "--",
-                                        "-h"));
+                                        "-h"),
+                              OPTION_PARSER_NORMAL);
 
         /* OPTION_STOPS_PARSING then "--" twice: second "--" is not consumed */
         test_macros_parse_one(STRV_MAKE("arg0",
@@ -1088,7 +1115,39 @@ TEST(option_macros) {
                               },
                               STRV_MAKE("--",
                                         "--version",
-                                        "-h"));
+                                        "-h"),
+                              OPTION_PARSER_NORMAL);
+
+        /* Basic OPTION_POSITIONAL use */
+        test_macros_parse_one(STRV_MAKE("arg0",
+                                        "--help",
+                                        "arg1",
+                                        "--debug",
+                                        "arg2"),
+                              (Entry[]) {
+                                      { "help" },
+                                      { "(positional)", "arg1" },
+                                      { "debug" },
+                                      { "(positional)", "arg2" },
+                                      {}
+                              },
+                              NULL,
+                              OPTION_PARSER_RETURN_POSITIONAL_ARGS);
+
+        /* OPTION_POSITIONAL combined with OPTION_STOPS_PARSING */
+        test_macros_parse_one(STRV_MAKE("arg0",
+                                        "--help",
+                                        "arg1",
+                                        "--exec",
+                                        "arg2"),
+                              (Entry[]) {
+                                      { "help" },
+                                      { "(positional)", "arg1" },
+                                      { "exec" },
+                                      {}
+                              },
+                              STRV_MAKE("arg2"),
+                              OPTION_PARSER_RETURN_POSITIONAL_ARGS);
 }
 
 /* Test the pattern used by nspawn's --user: an optional-arg option that also