diff --git a/parse-options.c b/parse-options.c index 4519ead9dc..b3d19446cd 100644 --- a/parse-options.c +++ b/parse-options.c @@ -1244,6 +1244,76 @@ int parse_options(int argc, const char **argv, return parse_options_end(&ctx); } +/* + * Look for `arg` among `options`. On success, return the matching option + * and set `value` to the value stuck to it, if any, or to NULL. + */ +static const struct early_scan_option * +find_early_scan_option(const char *arg, + const struct early_scan_option *options, + const char **value) +{ + if (!skip_prefix(arg, "--", &arg)) + return NULL; + + for (; options->name; options++) { + const char *rest; + + if (!skip_prefix(arg, options->name, &rest)) + continue; + if (!*rest) { + *value = NULL; + return options; + } + /* Only an option taking a value can be stuck to one. */ + if (*rest == '=' && options->takes_value) { + *value = rest + 1; + return options; + } + } + + return NULL; +} + +int early_scan_options(int argc, const char **argv, + const struct early_scan_option *options, + enum early_scan_flags flags, + early_scan_fn *fn, void *data) +{ + for (int i = 0; i < argc; i++) { + const char *arg = argv[i]; + const char *value; + const struct early_scan_option *opt; + int pos = i; + + if ((flags & EARLY_SCAN_STOP_AT_DASHDASH) && + !strcmp(arg, "--")) + return i; + + opt = find_early_scan_option(arg, options, &value); + if (!opt) { + if ((flags & EARLY_SCAN_STOP_AT_NON_OPTION) && + (*arg != '-' || !arg[1])) + return i; + continue; + } + + /* + * When an option takes a value, but that value is not + * stuck to it with '=', then the next argument is the + * value and it has to be skipped so that it isn't + * taken for an option itself. + */ + if (opt->takes_value && !value && i + 1 < argc) + value = argv[++i]; + + if (opt->wanted && fn(opt, value, pos, data)) + return i; + } + + return argc; +} + static int usage_argh(const struct option *opts, FILE *outfile) { const char *s; diff --git a/parse-options.h b/parse-options.h index d7f896a933..abc73d8399 100644 --- a/parse-options.h +++ b/parse-options.h @@ -491,6 +491,66 @@ static inline void die_for_incompatible_opt2(int opt1, const char *opt1_name, BUG("option callback expects an argument"); \ } while(0) +/*----- Early scan: scanning argv before the actual option parsing -----*/ + +/* + * Some commands need to look at a few options before they can parse + * their command line for real, for example because the result decides + * whether a repository is needed at all. + * + * Such an early scan has to know which options take their value as a + * separate argument, or it could mistake such a value for an option. The + * `struct early_scan_option` array passed to early_scan_options() below + * describes the options to look for, as well as the ones that only need + * to be skipped along with their value. + */ +struct early_scan_option { + const char *name; /* Option name, without the leading dashes */ + unsigned takes_value:1; /* "--option=value" or "--option value" expected? */ + unsigned wanted:1; /* Report option to callback? */ +}; + +#define EARLY_SCAN_SKIP_VALUE(n) { .name = (n), .takes_value = 1 } +#define EARLY_SCAN_WANT(n) { .name = (n), .wanted = 1 } +#define EARLY_SCAN_WANT_VALUE(n) { .name = (n), .takes_value = 1, .wanted = 1 } +#define EARLY_SCAN_END() { NULL } + +/* + * Called by early_scan_options() for each argument matching a + * `struct early_scan_option` that has its `wanted` bit set. + * + * `option` is the matching option, `value` its value or NULL if it + * doesn't take one, and `pos` the index of the option in argv. + * + * Returning a non-zero value stops the scan. + */ +typedef int early_scan_fn(const struct early_scan_option *option, + const char *value, int pos, void *data); + +enum early_scan_flags { + EARLY_SCAN_STOP_AT_DASHDASH = 1 << 0, /* Stop at "--" */ + EARLY_SCAN_STOP_AT_NON_OPTION = 1 << 1, +}; + +/* + * Scan `argv` for the options described by `options`, calling `fn` + * for each of those that are `wanted`. `argv` is not modified. + * + * `fn` may be NULL when no option is `wanted`, which is useful to only + * find out where the scan stops. + * + * Note that abbreviated options are not recognized, as a scan cannot + * know about the options it hasn't been told about, and would then + * resolve abbreviations differently from the actual option parsing. + * + * Returns the index at which the scan stopped, which is `argc` when the + * whole array was scanned. + */ +int early_scan_options(int argc, const char **argv, + const struct early_scan_option *options, + enum early_scan_flags flags, + early_scan_fn *fn, void *data); + /*----- 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 f181f0c02d..96ab941d29 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -383,3 +383,42 @@ int cmd__parse_subcommand(int argc, const char **argv) return parse_subcommand__cmd(argc, argv, test_flags); } + +static int show_early_option(const struct early_scan_option *opt, + const char *value, int pos, void *data UNUSED) +{ + printf("found: %s at %d", opt->name, pos); + if (value) + printf(" value: %s", value); + putchar('\n'); + return 0; +} + +int cmd__early_scan_options(int argc, const char **argv) +{ + static const struct early_scan_option options[] = { + EARLY_SCAN_WANT("wanted"), + EARLY_SCAN_WANT_VALUE("wanted-value"), + EARLY_SCAN_SKIP_VALUE("skipped-value"), + EARLY_SCAN_END() + }; + enum early_scan_flags flags = 0; + int stopped; + + while (argc > 1 && *argv[1] == '-') { + if (!strcmp(argv[1], "--stop-at-dashdash")) + flags |= EARLY_SCAN_STOP_AT_DASHDASH; + else if (!strcmp(argv[1], "--stop-at-non-option")) + flags |= EARLY_SCAN_STOP_AT_NON_OPTION; + else + break; + argc--; + argv++; + } + + stopped = early_scan_options(argc - 1, argv + 1, options, flags, + show_early_option, NULL); + printf("stopped at: %d of %d\n", stopped, argc - 1); + + return 0; +} diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c index b71a22b43b..5d2f5877d9 100644 --- a/t/helper/test-tool.c +++ b/t/helper/test-tool.c @@ -50,6 +50,7 @@ static struct test_cmd cmds[] = { { "pack-mtimes", cmd__pack_mtimes }, { "parse-options", cmd__parse_options }, { "parse-options-flags", cmd__parse_options_flags }, + { "early-scan-options", cmd__early_scan_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 f2885b33d5..071306d52d 100644 --- a/t/helper/test-tool.h +++ b/t/helper/test-tool.h @@ -43,6 +43,7 @@ int cmd__pack_deltas(int argc, const char **argv); 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__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 449fff4d34..d760d8cfbd 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -845,4 +845,81 @@ test_expect_success 'u16 limits range' ' test_grep "value 65536 for option .u16. not in range \[0,65535\]" err ' +test_expect_success 'early_scan_options() finds a wanted option' ' + test-tool early-scan-options --wanted >actual && + cat >expect <<-\EOF && + found: wanted at 0 + stopped at: 1 of 1 + EOF + test_cmp expect actual +' + +test_expect_success 'early_scan_options() reads a stuck or separate value' ' + test-tool early-scan-options --wanted-value=one >actual && + cat >expect <<-\EOF && + found: wanted-value at 0 value: one + stopped at: 1 of 1 + EOF + test_cmp expect actual && + test-tool early-scan-options --wanted-value two >actual && + cat >expect <<-\EOF && + found: wanted-value at 0 value: two + stopped at: 2 of 2 + EOF + test_cmp expect actual +' + +test_expect_success 'early_scan_options() skips the value of other options' ' + test-tool early-scan-options --skipped-value --wanted >actual && + cat >expect <<-\EOF && + stopped at: 2 of 2 + EOF + test_cmp expect actual && + test-tool early-scan-options --skipped-value one --wanted >actual && + cat >expect <<-\EOF && + found: wanted at 2 + stopped at: 3 of 3 + EOF + test_cmp expect actual +' + +test_expect_success 'early_scan_options() can stop at "--"' ' + test-tool early-scan-options --stop-at-dashdash -- --wanted >actual && + cat >expect <<-\EOF && + stopped at: 0 of 2 + EOF + test_cmp expect actual && + test-tool early-scan-options --stop-at-dashdash \ + --skipped-value -- --wanted >actual && + cat >expect <<-\EOF && + found: wanted at 2 + stopped at: 3 of 3 + EOF + test_cmp expect actual +' + +test_expect_success 'early_scan_options() can stop at a non-option' ' + test-tool early-scan-options --stop-at-non-option \ + arg --wanted >actual && + cat >expect <<-\EOF && + stopped at: 0 of 2 + EOF + test_cmp expect actual && + test-tool early-scan-options --stop-at-non-option \ + --skipped-value arg --wanted >actual && + cat >expect <<-\EOF && + found: wanted at 2 + stopped at: 3 of 3 + EOF + test_cmp expect actual +' + +test_expect_success 'early_scan_options() ignores abbreviated options' ' + test-tool early-scan-options --want >actual && + cat >expect <<-\EOF && + stopped at: 1 of 1 + EOF + test_cmp expect actual +' + test_done