From: Christian Couder <christian.couder@gmail.com>
To: git@vger.kernel.org
Cc: Junio C Hamano <gitster@pobox.com>,
Patrick Steinhardt <ps@pks.im>, Elijah Newren <newren@gmail.com>,
Jeff King <peff@peff.net>,
"brian m . carlson" <sandals@crustytoothpaste.net>,
Johannes Schindelin <Johannes.Schindelin@gmx.de>,
Justin Tobler <jltobler@gmail.com>,
Christian Couder <christian.couder@gmail.com>
Subject: [PATCH 5/6] parse-options: build early scan options from a struct option array
Date: Wed, 2 Sep 2026 18:10:46 +0200 [thread overview]
Message-ID: <20260902161047.476753-6-christian.couder@gmail.com> (raw)
In-Reply-To: <20260902161047.476753-1-christian.couder@gmail.com>
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 <christian.couder@gmail.com>
---
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
next prev parent reply other threads:[~2026-09-02 16:11 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-02 16:10 [PATCH 0/6] Standardize early option scanning to fix argument parsing bugs Christian Couder
2026-09-02 16:10 ` [PATCH 1/6] parse-options: add early_scan_options() Christian Couder
2026-09-02 22:11 ` Junio C Hamano
2026-09-02 16:10 ` [PATCH 2/6] bisect: fix "--" detection when a term name is "--" Christian Couder
2026-09-02 22:30 ` Junio C Hamano
2026-09-02 16:10 ` [PATCH 3/6] rev-parse: fix "--" detection when it is an option value Christian Couder
2026-09-02 16:10 ` [PATCH 4/6] parse-options: add parse_options_takes_argument() Christian Couder
2026-09-02 16:10 ` Christian Couder [this message]
2026-09-02 16:10 ` [PATCH 6/6] fast-import: use early_scan_options() for --allow-unsafe-features Christian Couder
2026-09-04 3:38 ` Junio C Hamano
2026-09-02 18:52 ` [PATCH 0/6] Standardize early option scanning to fix argument parsing bugs Junio C Hamano
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260902161047.476753-6-christian.couder@gmail.com \
--to=christian.couder@gmail.com \
--cc=Johannes.Schindelin@gmx.de \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=jltobler@gmail.com \
--cc=newren@gmail.com \
--cc=peff@peff.net \
--cc=ps@pks.im \
--cc=sandals@crustytoothpaste.net \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.