Thread (11 messages) flat view 11 messages, 2 authors, 1d ago
WARM1d

[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
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help