[PATCH 5/6] parse-options: build early scan options from a struct option array
From: Christian Couder <hidden>
Date: 2026-09-02 16:11:28
Subsystem:
the rest · Maintainer:
Linus Torvalds
A command that scans its arguments early has to know which options take a value, so that it can skip that value instead of mistaking it for an option. When it also parses its options with the parse-options API, that information is already available in its `struct option` array, and duplicating it by hand in a `struct early_scan_option` array is both tedious and easy to get out of sync when an option is added. So let's add early_scan_options_from_options() to build the latter array from the former, using parse_options_takes_argument() to find out which options take a value. Its caller only has to name the options it wants to be reported. Note: This early scanner translation intentionally leaves out a few complex option types to keep the scan simple and fast: - Short options are ignored: early_scan_options_from_options() explicitly skips options without a `long_name`, and the scanner only looks for `--`. Properly handling short options would require parsing bundled flags (e.g., `-abc value`), which requires replicating the full parse_options() state machine. - Conditional values: Options with `PARSE_OPT_LASTARG_DEFAULT` or `PARSE_OPT_OPTARG` are treated as not taking a separate argument. Because the scanner does not evaluate context (like whether an argument is the final one in `argv`), it must err on the side of caution to avoid accidentally consuming the `--` separator or a path. - Abbreviated options remain unrecognized: Even though the scanner is now provided with the full option array, the underlying early_scan_options() engine still relies on exact string matches. Safely resolving abbreviations would require duplicating the ambiguity-checking logic from the main parser. - Negated options are not automatically derived: The scanner strictly matches the defined long name. It does not automatically recognize the `--no-<name>` variants of boolean options. (This is harmless in practice for current callers, as negated options do not take values to skip, and boolean defaults align with the ignored state). The above shortcomings can be addressed later, for example, when commands that use short options or options with conditional values need an early scan or are ported to use `struct option`. Despite these limitations, this abstraction is a significant improvement. It allows commands like `fast-import` to reuse their existing `struct option` array for early scanning, ensuring the scanner and the main parser agree on which options take arguments, and preventing developers from having to maintain a separate, hardcoded list that could drift out of sync. Signed-off-by: Christian Couder <redacted> --- parse-options.c | 39 +++++++++++++++++++++++++++++++++++ parse-options.h | 22 ++++++++++++++++++++ t/helper/test-parse-options.c | 32 ++++++++++++++++++++++++++++ t/helper/test-tool.c | 1 + t/helper/test-tool.h | 1 + t/t0040-parse-options.sh | 26 +++++++++++++++++++++++ 6 files changed, 121 insertions(+)
diff --git a/parse-options.c b/parse-options.c
index 70851a385b..6cdc9c64cc 100644
--- a/parse-options.c
+++ b/parse-options.c@@ -1323,6 +1323,45 @@ int early_scan_options(int argc, const char **argv, return argc; } +struct early_scan_option * +early_scan_options_from_options(const struct option *options, + const char **wanted) +{ + struct early_scan_option *early; + size_t nr = 0; + + for (const struct option *opt = options; opt->type != OPTION_END; opt++) + if (opt->long_name) + nr++; + + CALLOC_ARRAY(early, nr + 1); + + nr = 0; + for (const struct option *opt = options; opt->type != OPTION_END; opt++) { + if (!opt->long_name) + continue; + early[nr].name = opt->long_name; + early[nr].takes_value = !!parse_options_takes_argument(opt); + nr++; + } + + for (; wanted && *wanted; wanted++) { + size_t i; + + for (i = 0; i < nr; i++) { + if (strcmp(early[i].name, *wanted)) + continue; + early[i].wanted = 1; + break; + } + if (i == nr) + BUG("wanted option '%s' is not in the options array", + *wanted); + } + + return early; +} + static int usage_argh(const struct option *opts, FILE *outfile) { const char *s;
diff --git a/parse-options.h b/parse-options.h
index b96e93508e..fb81f2ed38 100644
--- a/parse-options.h
+++ b/parse-options.h@@ -561,6 +561,28 @@ int early_scan_options(int argc, const char **argv, enum early_scan_flags flags, early_scan_fn *fn, void *data); +/* + * Build the `struct early_scan_option` array to pass to + * early_scan_options() from the `options` array that the actual option + * parsing uses, so that both agree on which options take a value. + * + * Note some intentional limitations to keep the scan simple and fast: + * short options are ignored, options with PARSE_OPT_LASTARG_DEFAULT or + * PARSE_OPT_OPTARG are treated as not taking a separate value, negated + * options ("--no-...") are not automatically generated, and abbreviated + * options will not be matched. + * + * The options named in the NULL terminated `wanted` array get their + * `wanted` bit set, the other ones are only there to be skipped along + * with their value. It is a BUG() for a name in `wanted` not to appear + * in `options`. + * + * The returned array is allocated and should be free()d by the caller. + */ +struct early_scan_option * +early_scan_options_from_options(const struct option *options, + const char **wanted); + /*----- incremental advanced APIs -----*/ struct parse_opt_cmdmode_list;
diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c
index 96ab941d29..0187a25ccb 100644
--- a/t/helper/test-parse-options.c
+++ b/t/helper/test-parse-options.c@@ -422,3 +422,35 @@ int cmd__early_scan_options(int argc, const char **argv) return 0; } + +int cmd__early_scan_from_options(int argc, const char **argv) +{ + int an_int = 0, a_bool = 0; + char *a_string = NULL; + const struct option options[] = { + OPT_STRING(0, "string", &a_string, "str", "get a string"), + OPT_INTEGER(0, "int", &an_int, "get an integer"), + OPT_BOOL(0, "bool", &a_bool, "get a boolean"), + OPT_STRING_F(0, "optarg", &a_string, "str", + "string with an optional value", + PARSE_OPT_OPTARG), + OPT_END() + }; + static const char *wanted[] = { "bool", NULL }; + struct early_scan_option *early; + int stopped; + + early = early_scan_options_from_options(options, wanted); + + for (const struct early_scan_option *o = early; o->name; o++) + printf("option: %s takes_value: %d wanted: %d\n", + o->name, o->takes_value, o->wanted); + + stopped = early_scan_options(argc - 1, argv + 1, early, 0, + show_early_option, NULL); + printf("stopped at: %d of %d\n", stopped, argc - 1); + + free(early); + + return 0; +}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 5d2f5877d9..f1b208a5af 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c@@ -51,6 +51,7 @@ static struct test_cmd cmds[] = { { "parse-options", cmd__parse_options }, { "parse-options-flags", cmd__parse_options_flags }, { "early-scan-options", cmd__early_scan_options }, + { "early-scan-from-options", cmd__early_scan_from_options }, { "parse-pathspec-file", cmd__parse_pathspec_file }, { "parse-subcommand", cmd__parse_subcommand }, { "partial-clone", cmd__partial_clone },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index 071306d52d..97334ce3c6 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h@@ -44,6 +44,7 @@ int cmd__pack_mtimes(int argc, const char **argv); int cmd__parse_options(int argc, const char **argv); int cmd__parse_options_flags(int argc, const char **argv); int cmd__early_scan_options(int argc, const char **argv); +int cmd__early_scan_from_options(int argc, const char **argv); int cmd__parse_pathspec_file(int argc, const char** argv); int cmd__parse_subcommand(int argc, const char **argv); int cmd__partial_clone(int argc, const char **argv);
diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh
index d760d8cfbd..bb72a6544d 100755
--- a/t/t0040-parse-options.sh
+++ b/t/t0040-parse-options.sh@@ -922,4 +922,30 @@ test_expect_success 'early_scan_options() ignores abbreviated options' ' test_cmp expect actual ' +test_expect_success 'early_scan_options_from_options() derives takes_value' ' + test-tool early-scan-from-options >actual && + cat >expect <<-\EOF && + option: string takes_value: 1 wanted: 0 + option: int takes_value: 1 wanted: 0 + option: bool takes_value: 0 wanted: 1 + option: optarg takes_value: 0 wanted: 0 + stopped at: 0 of 0 + EOF + test_cmp expect actual +' + +test_expect_success 'early_scan_options_from_options() skips values' ' + test-tool early-scan-from-options --string --bool >out && + tail -1 out >actual && + echo "stopped at: 2 of 2" >expect && + test_cmp expect actual && + test-tool early-scan-from-options --string v --bool >out && + tail -2 out >actual && + cat >expect <<-\EOF && + found: bool at 2 + stopped at: 3 of 3 + EOF + test_cmp expect actual +' + test_done
--
2.55.0.787.g3f9e2241eb.dirty