From: Zbigniew Jędrzejewski-Szmek Date: Wed, 15 Apr 2026 20:52:06 +0000 (+0200) Subject: shared/options: add equivalent of "-" in getopt_long X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=f4c07a08950c6dc28f0df4918edd8483cb20bb0f;p=thirdparty%2Fsystemd.git shared/options: add equivalent of "-" in getopt_long 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. --- diff --git a/src/shared/options.c b/src/shared/options.c index 5d4df946b83..fbf9a311817 100644 --- a/src/shared/options.c +++ b/src/shared/options.c @@ -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++); diff --git a/src/shared/options.h b/src/shared/options.h index a10f914da26..cd9f64fd494 100644 --- a/src/shared/options.h +++ b/src/shared/options.h @@ -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; diff --git a/src/test/test-options.c b/src/test/test-options.c index e6b18d7a7ea..05fd4bcbe07 100644 --- a/src/test/test-options.c +++ b/src/test/test-options.c @@ -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