Skip to content

Commit 7a01b44

Browse files
committed
Merge branch 'rs/opt-parse-long-fixups'
The parse-options code that deals with abbreviated long option names have been cleaned up. Reviewed-by: Josh Steadmon <[email protected]> cf. <[email protected]> * rs/opt-parse-long-fixups: parse-options: rearrange long_name matching code parse-options: normalize arg and long_name before comparison parse-options: detect ambiguous self-negation parse-options: factor out register_abbrev() and struct parsed_option parse-options: set arg of abbreviated option lazily parse-options: recognize abbreviated negated option with arg
2 parents 3bd955d + 28a9247 commit 7a01b44

File tree

3 files changed

+100
-64
lines changed

3 files changed

+100
-64
lines changed

parse-options.c

Lines changed: 73 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -350,98 +350,107 @@ static int is_alias(struct parse_opt_ctx_t *ctx,
350350
return 0;
351351
}
352352

353+
struct parsed_option {
354+
const struct option *option;
355+
enum opt_parsed flags;
356+
};
357+
358+
static void register_abbrev(struct parse_opt_ctx_t *p,
359+
const struct option *option, enum opt_parsed flags,
360+
struct parsed_option *abbrev,
361+
struct parsed_option *ambiguous)
362+
{
363+
if (p->flags & PARSE_OPT_KEEP_UNKNOWN_OPT)
364+
return;
365+
if (abbrev->option &&
366+
!(abbrev->flags == flags && is_alias(p, abbrev->option, option))) {
367+
/*
368+
* If this is abbreviated, it is
369+
* ambiguous. So when there is no
370+
* exact match later, we need to
371+
* error out.
372+
*/
373+
ambiguous->option = abbrev->option;
374+
ambiguous->flags = abbrev->flags;
375+
}
376+
abbrev->option = option;
377+
abbrev->flags = flags;
378+
}
379+
353380
static enum parse_opt_result parse_long_opt(
354381
struct parse_opt_ctx_t *p, const char *arg,
355382
const struct option *options)
356383
{
357384
const char *arg_end = strchrnul(arg, '=');
358-
const struct option *abbrev_option = NULL, *ambiguous_option = NULL;
359-
enum opt_parsed abbrev_flags = OPT_LONG, ambiguous_flags = OPT_LONG;
360-
int allow_abbrev = !(p->flags & PARSE_OPT_KEEP_UNKNOWN_OPT);
385+
const char *arg_start = arg;
386+
enum opt_parsed flags = OPT_LONG;
387+
int arg_starts_with_no_no = 0;
388+
struct parsed_option abbrev = { .option = NULL, .flags = OPT_LONG };
389+
struct parsed_option ambiguous = { .option = NULL, .flags = OPT_LONG };
390+
391+
if (skip_prefix(arg_start, "no-", &arg_start)) {
392+
if (skip_prefix(arg_start, "no-", &arg_start))
393+
arg_starts_with_no_no = 1;
394+
else
395+
flags |= OPT_UNSET;
396+
}
361397

362398
for (; options->type != OPTION_END; options++) {
363399
const char *rest, *long_name = options->long_name;
364-
enum opt_parsed flags = OPT_LONG, opt_flags = OPT_LONG;
400+
enum opt_parsed opt_flags = OPT_LONG;
401+
int allow_unset = !(options->flags & PARSE_OPT_NONEG);
365402

366403
if (options->type == OPTION_SUBCOMMAND)
367404
continue;
368405
if (!long_name)
369406
continue;
370407

371-
if (!starts_with(arg, "no-") &&
372-
!(options->flags & PARSE_OPT_NONEG) &&
373-
skip_prefix(long_name, "no-", &long_name))
408+
if (skip_prefix(long_name, "no-", &long_name))
374409
opt_flags |= OPT_UNSET;
410+
else if (arg_starts_with_no_no)
411+
continue;
375412

376-
if (!skip_prefix(arg, long_name, &rest))
377-
rest = NULL;
378-
if (!rest) {
379-
/* abbreviated? */
380-
if (allow_abbrev &&
381-
!strncmp(long_name, arg, arg_end - arg)) {
382-
is_abbreviated:
383-
if (abbrev_option &&
384-
!is_alias(p, abbrev_option, options)) {
385-
/*
386-
* If this is abbreviated, it is
387-
* ambiguous. So when there is no
388-
* exact match later, we need to
389-
* error out.
390-
*/
391-
ambiguous_option = abbrev_option;
392-
ambiguous_flags = abbrev_flags;
393-
}
394-
if (!(flags & OPT_UNSET) && *arg_end)
395-
p->opt = arg_end + 1;
396-
abbrev_option = options;
397-
abbrev_flags = flags ^ opt_flags;
398-
continue;
399-
}
400-
/* negation allowed? */
401-
if (options->flags & PARSE_OPT_NONEG)
402-
continue;
403-
/* negated and abbreviated very much? */
404-
if (allow_abbrev && starts_with("no-", arg)) {
405-
flags |= OPT_UNSET;
406-
goto is_abbreviated;
407-
}
408-
/* negated? */
409-
if (!starts_with(arg, "no-"))
410-
continue;
411-
flags |= OPT_UNSET;
412-
if (!skip_prefix(arg + 3, long_name, &rest)) {
413-
/* abbreviated and negated? */
414-
if (allow_abbrev &&
415-
starts_with(long_name, arg + 3))
416-
goto is_abbreviated;
417-
else
418-
continue;
419-
}
420-
}
421-
if (*rest) {
422-
if (*rest != '=')
413+
if (((flags ^ opt_flags) & OPT_UNSET) && !allow_unset)
414+
continue;
415+
416+
if (skip_prefix(arg_start, long_name, &rest)) {
417+
if (*rest == '=')
418+
p->opt = rest + 1;
419+
else if (*rest)
423420
continue;
424-
p->opt = rest + 1;
421+
return get_value(p, options, flags ^ opt_flags);
425422
}
426-
return get_value(p, options, flags ^ opt_flags);
423+
424+
/* abbreviated? */
425+
if (!strncmp(long_name, arg_start, arg_end - arg_start))
426+
register_abbrev(p, options, flags ^ opt_flags,
427+
&abbrev, &ambiguous);
428+
429+
/* negated and abbreviated very much? */
430+
if (allow_unset && starts_with("no-", arg))
431+
register_abbrev(p, options, OPT_UNSET ^ opt_flags,
432+
&abbrev, &ambiguous);
427433
}
428434

429-
if (disallow_abbreviated_options && (ambiguous_option || abbrev_option))
435+
if (disallow_abbreviated_options && (ambiguous.option || abbrev.option))
430436
die("disallowed abbreviated or ambiguous option '%.*s'",
431437
(int)(arg_end - arg), arg);
432438

433-
if (ambiguous_option) {
439+
if (ambiguous.option) {
434440
error(_("ambiguous option: %s "
435441
"(could be --%s%s or --%s%s)"),
436442
arg,
437-
(ambiguous_flags & OPT_UNSET) ? "no-" : "",
438-
ambiguous_option->long_name,
439-
(abbrev_flags & OPT_UNSET) ? "no-" : "",
440-
abbrev_option->long_name);
443+
(ambiguous.flags & OPT_UNSET) ? "no-" : "",
444+
ambiguous.option->long_name,
445+
(abbrev.flags & OPT_UNSET) ? "no-" : "",
446+
abbrev.option->long_name);
441447
return PARSE_OPT_HELP;
442448
}
443-
if (abbrev_option)
444-
return get_value(p, abbrev_option, abbrev_flags);
449+
if (abbrev.option) {
450+
if (*arg_end)
451+
p->opt = arg_end + 1;
452+
return get_value(p, abbrev.option, abbrev.flags);
453+
}
445454
return PARSE_OPT_UNKNOWN;
446455
}
447456

t/t0040-parse-options.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,22 @@ test_expect_success 'superfluous value provided: boolean' '
210210
test_cmp expect actual
211211
'
212212

213+
test_expect_success 'superfluous value provided: boolean, abbreviated' '
214+
cat >expect <<-\EOF &&
215+
error: option `yes'\'' takes no value
216+
EOF
217+
test_expect_code 129 env GIT_TEST_DISALLOW_ABBREVIATED_OPTIONS=false \
218+
test-tool parse-options --ye=hi 2>actual &&
219+
test_cmp expect actual &&
220+
221+
cat >expect <<-\EOF &&
222+
error: option `no-yes'\'' takes no value
223+
EOF
224+
test_expect_code 129 env GIT_TEST_DISALLOW_ABBREVIATED_OPTIONS=false \
225+
test-tool parse-options --no-ye=hi 2>actual &&
226+
test_cmp expect actual
227+
'
228+
213229
test_expect_success 'superfluous value provided: cmdmode' '
214230
cat >expect <<-\EOF &&
215231
error: option `mode1'\'' takes no value

t/t1502-rev-parse-parseopt.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,4 +322,15 @@ check_invalid_long_option optionspec-neg --no-positive-only
322322
check_invalid_long_option optionspec-neg --negative
323323
check_invalid_long_option optionspec-neg --no-no-negative
324324

325+
test_expect_success 'ambiguous: --no matches both --noble and --no-noble' '
326+
cat >spec <<-\EOF &&
327+
some-command [options]
328+
--
329+
noble The feudal switch.
330+
EOF
331+
test_expect_code 129 env GIT_TEST_DISALLOW_ABBREVIATED_OPTIONS=false \
332+
git rev-parse --parseopt -- <spec 2>err --no &&
333+
grep "error: ambiguous option: no (could be --noble or --no-noble)" err
334+
'
335+
325336
test_done

0 commit comments

Comments
 (0)