diff --git a/parse-options.c b/parse-options.c index 093eaf2db8..e0c94b0546 100644 --- a/parse-options.c +++ b/parse-options.c @@ -70,42 +70,10 @@ static void fix_filename(const char *prefix, char **file) *file = prefix_filename_except_for_dash(prefix, *file); } -static enum parse_opt_result opt_command_mode_error( - const struct option *opt, - const struct option *all_opts, - enum opt_parsed flags) -{ - const struct option *that; - struct strbuf that_name = STRBUF_INIT; - - /* - * Find the other option that was used to set the variable - * already, and report that this is not compatible with it. - */ - for (that = all_opts; that->type != OPTION_END; that++) { - if (that == opt || - !(that->flags & PARSE_OPT_CMDMODE) || - that->value != opt->value || - that->defval != *(int *)opt->value) - continue; - - if (that->long_name) - strbuf_addf(&that_name, "--%s", that->long_name); - else - strbuf_addf(&that_name, "-%c", that->short_name); - error(_("%s is incompatible with %s"), - optname(opt, flags), that_name.buf); - strbuf_release(&that_name); - return PARSE_OPT_ERROR; - } - return error(_("%s : incompatible with something else"), - optname(opt, flags)); -} - -static enum parse_opt_result get_value(struct parse_opt_ctx_t *p, - const struct option *opt, - const struct option *all_opts, - enum opt_parsed flags) +static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, + const struct option *opt, + enum opt_parsed flags, + const char **argp) { const char *s, *arg; const int unset = flags & OPT_UNSET; @@ -118,14 +86,6 @@ static enum parse_opt_result get_value(struct parse_opt_ctx_t *p, if (!(flags & OPT_SHORT) && p->opt && (opt->flags & PARSE_OPT_NOARG)) return error(_("%s takes no value"), optname(opt, flags)); - /* - * Giving the same mode option twice, although unnecessary, - * is not a grave error, so let it pass. - */ - if ((opt->flags & PARSE_OPT_CMDMODE) && - *(int *)opt->value && *(int *)opt->value != opt->defval) - return opt_command_mode_error(opt, all_opts, flags); - switch (opt->type) { case OPTION_LOWLEVEL_CALLBACK: return opt->ll_callback(p, opt, NULL, unset); @@ -200,6 +160,8 @@ static enum parse_opt_result get_value(struct parse_opt_ctx_t *p, p_unset = 0; p_arg = arg; } + if (opt->flags & PARSE_OPT_CMDMODE) + *argp = p_arg; if (opt->callback) return (*opt->callback)(opt, p_arg, p_unset) ? (-1) : 0; else @@ -247,16 +209,91 @@ static enum parse_opt_result get_value(struct parse_opt_ctx_t *p, } } +struct parse_opt_cmdmode_list { + int value, *value_ptr; + const struct option *opt; + const char *arg; + enum opt_parsed flags; + struct parse_opt_cmdmode_list *next; +}; + +static void build_cmdmode_list(struct parse_opt_ctx_t *ctx, + const struct option *opts) +{ + ctx->cmdmode_list = NULL; + + for (; opts->type != OPTION_END; opts++) { + struct parse_opt_cmdmode_list *elem = ctx->cmdmode_list; + int *value_ptr = opts->value; + + if (!(opts->flags & PARSE_OPT_CMDMODE) || !value_ptr) + continue; + + while (elem && elem->value_ptr != value_ptr) + elem = elem->next; + if (elem) + continue; + + CALLOC_ARRAY(elem, 1); + elem->value_ptr = value_ptr; + elem->value = *value_ptr; + elem->next = ctx->cmdmode_list; + ctx->cmdmode_list = elem; + } +} + +static char *optnamearg(const struct option *opt, const char *arg, + enum opt_parsed flags) +{ + if (flags & OPT_SHORT) + return xstrfmt("-%c%s", opt->short_name, arg ? arg : ""); + return xstrfmt("--%s%s%s%s", flags & OPT_UNSET ? "no-" : "", + opt->long_name, arg ? "=" : "", arg ? arg : ""); +} + +static enum parse_opt_result get_value(struct parse_opt_ctx_t *p, + const struct option *opt, + enum opt_parsed flags) +{ + const char *arg = NULL; + enum parse_opt_result result = do_get_value(p, opt, flags, &arg); + struct parse_opt_cmdmode_list *elem = p->cmdmode_list; + char *opt_name, *other_opt_name; + + for (; elem; elem = elem->next) { + if (*elem->value_ptr == elem->value) + continue; + + if (elem->opt && + (elem->opt->flags | opt->flags) & PARSE_OPT_CMDMODE) + break; + + elem->opt = opt; + elem->arg = arg; + elem->flags = flags; + elem->value = *elem->value_ptr; + } + + if (result || !elem) + return result; + + opt_name = optnamearg(opt, arg, flags); + other_opt_name = optnamearg(elem->opt, elem->arg, elem->flags); + error(_("%s is incompatible with %s"), opt_name, other_opt_name); + free(opt_name); + free(other_opt_name); + return -1; +} + static enum parse_opt_result parse_short_opt(struct parse_opt_ctx_t *p, const struct option *options) { - const struct option *all_opts = options; const struct option *numopt = NULL; for (; options->type != OPTION_END; options++) { if (options->short_name == *p->opt) { p->opt = p->opt[1] ? p->opt + 1 : NULL; - return get_value(p, options, all_opts, OPT_SHORT); + return get_value(p, options, OPT_SHORT); } /* @@ -318,7 +355,6 @@ static enum parse_opt_result parse_long_opt( struct parse_opt_ctx_t *p, const char *arg, const struct option *options) { - const struct option *all_opts = options; const char *arg_end = strchrnul(arg, '='); const struct option *abbrev_option = NULL, *ambiguous_option = NULL; enum opt_parsed abbrev_flags = OPT_LONG, ambiguous_flags = OPT_LONG; @@ -387,7 +423,7 @@ is_abbreviated: continue; p->opt = rest + 1; } - return get_value(p, options, all_opts, flags ^ opt_flags); + return get_value(p, options, flags ^ opt_flags); } if (disallow_abbreviated_options && (ambiguous_option || abbrev_option)) @@ -405,7 +441,7 @@ is_abbreviated: return PARSE_OPT_HELP; } if (abbrev_option) - return get_value(p, abbrev_option, all_opts, abbrev_flags); + return get_value(p, abbrev_option, abbrev_flags); return PARSE_OPT_UNKNOWN; } @@ -413,13 +449,11 @@ static enum parse_opt_result parse_nodash_opt(struct parse_opt_ctx_t *p, const char *arg, const struct option *options) { - const struct option *all_opts = options; - for (; options->type != OPTION_END; options++) { if (!(options->flags & PARSE_OPT_NODASH)) continue; if (options->short_name == arg[0] && arg[1] == '\0') - return get_value(p, options, all_opts, OPT_SHORT); + return get_value(p, options, OPT_SHORT); } return PARSE_OPT_ERROR; } @@ -574,6 +608,7 @@ static void parse_options_start_1(struct parse_opt_ctx_t *ctx, (flags & PARSE_OPT_KEEP_ARGV0)) BUG("Can't keep argv0 if you don't have it"); parse_options_check(options); + build_cmdmode_list(ctx, options); } void parse_options_start(struct parse_opt_ctx_t *ctx, @@ -1006,6 +1041,11 @@ int parse_options(int argc, const char **argv, precompose_argv_prefix(argc, argv, NULL); free_preprocessed_options(real_options); free(ctx.alias_groups); + for (struct parse_opt_cmdmode_list *elem = ctx.cmdmode_list; elem;) { + struct parse_opt_cmdmode_list *next = elem->next; + free(elem); + elem = next; + } return parse_options_end(&ctx); } diff --git a/parse-options.h b/parse-options.h index 4a66ec3bf5..bd62e20268 100644 --- a/parse-options.h +++ b/parse-options.h @@ -445,6 +445,8 @@ static inline void die_for_incompatible_opt3(int opt1, const char *opt1_name, /*----- incremental advanced APIs -----*/ +struct parse_opt_cmdmode_list; + /* * It's okay for the caller to consume argv/argc in the usual way. * Other fields of that structure are private to parse-options and should not @@ -459,6 +461,7 @@ struct parse_opt_ctx_t { unsigned has_subcommands; const char *prefix; const char **alias_groups; /* must be in groups of 3 elements! */ + struct parse_opt_cmdmode_list *cmdmode_list; }; void parse_options_start(struct parse_opt_ctx_t *ctx, diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index a4f6e24b0c..ded8116cc5 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -21,6 +21,19 @@ static struct { int unset; } length_cb; +static int mode34_callback(const struct option *opt, const char *arg, int unset) +{ + if (unset) + *(int *)opt->value = 0; + else if (!strcmp(arg, "3")) + *(int *)opt->value = 3; + else if (!strcmp(arg, "4")) + *(int *)opt->value = 4; + else + return error("invalid value for '%s': '%s'", "--mode34", arg); + return 0; +} + static int length_callback(const struct option *opt, const char *arg, int unset) { length_cb.called = 1; @@ -124,6 +137,9 @@ int cmd__parse_options(int argc, const char **argv) OPT_SET_INT(0, "set23", &integer, "set integer to 23", 23), OPT_CMDMODE(0, "mode1", &integer, "set integer to 1 (cmdmode option)", 1), OPT_CMDMODE(0, "mode2", &integer, "set integer to 2 (cmdmode option)", 2), + OPT_CALLBACK_F(0, "mode34", &integer, "(3|4)", + "set integer to 3 or 4 (cmdmode option)", + PARSE_OPT_CMDMODE, mode34_callback), OPT_CALLBACK('L', "length", &integer, "str", "get length of ", length_callback), OPT_FILENAME('F', "file", &file, "set file to "), diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index a0ad6192d6..06fb9e6457 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -28,6 +28,7 @@ usage: test-tool parse-options --[no-]set23 set integer to 23 --mode1 set integer to 1 (cmdmode option) --mode2 set integer to 2 (cmdmode option) + --[no-]mode34 (3|4) set integer to 3 or 4 (cmdmode option) -L, --[no-]length get length of -F, --[no-]file @@ -366,19 +367,41 @@ test_expect_success 'OPT_NEGBIT() works' ' ' test_expect_success 'OPT_CMDMODE() works' ' - test-tool parse-options --expect="integer: 1" --mode1 + test-tool parse-options --expect="integer: 1" --mode1 && + test-tool parse-options --expect="integer: 3" --mode34=3 ' -test_expect_success 'OPT_CMDMODE() detects incompatibility' ' +test_expect_success 'OPT_CMDMODE() detects incompatibility (1)' ' test_must_fail test-tool parse-options --mode1 --mode2 >output 2>output.err && test_must_be_empty output && - test_i18ngrep "incompatible with --mode" output.err + test_i18ngrep "mode1" output.err && + test_i18ngrep "mode2" output.err && + test_i18ngrep "is incompatible with" output.err ' -test_expect_success 'OPT_CMDMODE() detects incompatibility with something else' ' +test_expect_success 'OPT_CMDMODE() detects incompatibility (2)' ' test_must_fail test-tool parse-options --set23 --mode2 >output 2>output.err && test_must_be_empty output && - test_i18ngrep "incompatible with something else" output.err + test_i18ngrep "mode2" output.err && + test_i18ngrep "set23" output.err && + test_i18ngrep "is incompatible with" output.err +' + +test_expect_success 'OPT_CMDMODE() detects incompatibility (3)' ' + test_must_fail test-tool parse-options --mode2 --set23 >output 2>output.err && + test_must_be_empty output && + test_i18ngrep "mode2" output.err && + test_i18ngrep "set23" output.err && + test_i18ngrep "is incompatible with" output.err +' + +test_expect_success 'OPT_CMDMODE() detects incompatibility (4)' ' + test_must_fail test-tool parse-options --mode2 --mode34=3 \ + >output 2>output.err && + test_must_be_empty output && + test_i18ngrep "mode2" output.err && + test_i18ngrep "mode34.3" output.err && + test_i18ngrep "is incompatible with" output.err ' test_expect_success 'OPT_COUNTUP() with PARSE_OPT_NODASH works' '