Merge branch 'rs/parse-options-negation-help'

"git cmd -h" learned to signal which options can be negated by
listing such options like "--[no-]opt".

* rs/parse-options-negation-help:
  parse-options: simplify usage_padding()
  parse-options: no --[no-]no-...
  parse-options: factor out usage_indent() and usage_padding()
  parse-options: show negatability of options in short help
  t1502: test option negation
  t1502: move optionspec help output to a file
  t1502, docs: disallow --no-help
  subtree: disallow --no-{help,quiet,debug,branch,message}
maint
Junio C Hamano 2023-08-25 10:37:37 -07:00
commit 6d159f5757
10 changed files with 198 additions and 125 deletions

View File

@ -398,7 +398,7 @@ some-command [<options>] <args>...


some-command does foo and bar! some-command does foo and bar!
-- --
h,help show the help h,help! show the help


foo some nifty option --foo foo some nifty option --foo
bar= some cool option --bar with an argument bar= some cool option --bar with an argument
@ -424,10 +424,10 @@ usage: some-command [<options>] <args>...
some-command does foo and bar! some-command does foo and bar!


-h, --help show the help -h, --help show the help
--foo some nifty option --foo --[no-]foo some nifty option --foo
--bar ... some cool option --bar with an argument --[no-]bar ... some cool option --bar with an argument
--baz <arg> another cool option --baz with a named argument --[no-]baz <arg> another cool option --baz with a named argument
--qux[=<path>] qux may take a path argument but has meaning by itself --[no-]qux[=<path>] qux may take a path argument but has meaning by itself


An option group Header An option group Header
-C[...] option C with an optional argument -C[...] option C with an optional argument

View File

@ -33,19 +33,19 @@ git subtree split --prefix=<prefix> [<commit>]
git subtree pull --prefix=<prefix> <repository> <ref> git subtree pull --prefix=<prefix> <repository> <ref>
git subtree push --prefix=<prefix> <repository> <refspec> git subtree push --prefix=<prefix> <repository> <refspec>
-- --
h,help show the help h,help! show the help
q,quiet quiet q,quiet! quiet
d,debug show debug messages d,debug! show debug messages
P,prefix= the name of the subdir to split out P,prefix= the name of the subdir to split out
options for 'split' (also: 'push') options for 'split' (also: 'push')
annotate= add a prefix to commit message of new commits annotate= add a prefix to commit message of new commits
b,branch= create a new branch from the split subtree b,branch!= create a new branch from the split subtree
ignore-joins ignore prior --rejoin commits ignore-joins ignore prior --rejoin commits
onto= try connecting new tree to an existing one onto= try connecting new tree to an existing one
rejoin merge the new branch back into HEAD rejoin merge the new branch back into HEAD
options for 'add' and 'merge' (also: 'pull', 'split --rejoin', and 'push --rejoin') options for 'add' and 'merge' (also: 'pull', 'split --rejoin', and 'push --rejoin')
squash merge subtree changes as a single commit squash merge subtree changes as a single commit
m,message= use the given message as the commit message for the merge commit m,message!= use the given message as the commit message for the merge commit
" "


indent=0 indent=0

View File

@ -71,7 +71,7 @@ test_expect_success 'shows short help text for -h' '
test_expect_code 129 git subtree -h >out 2>err && test_expect_code 129 git subtree -h >out 2>err &&
test_must_be_empty err && test_must_be_empty err &&
grep -e "^ *or: git subtree pull" out && grep -e "^ *or: git subtree pull" out &&
grep -e --annotate out grep -F -e "--[no-]annotate" out
' '


# #

View File

@ -1023,14 +1023,37 @@ static int usage_argh(const struct option *opts, FILE *outfile)
return utf8_fprintf(outfile, s, opts->argh ? _(opts->argh) : _("...")); return utf8_fprintf(outfile, s, opts->argh ? _(opts->argh) : _("..."));
} }


#define USAGE_OPTS_WIDTH 24 static int usage_indent(FILE *outfile)
#define USAGE_GAP 2 {
return fprintf(outfile, " ");
}

#define USAGE_OPTS_WIDTH 26

static void usage_padding(FILE *outfile, size_t pos)
{
if (pos < USAGE_OPTS_WIDTH)
fprintf(outfile, "%*s", USAGE_OPTS_WIDTH - (int)pos, "");
else
fprintf(outfile, "\n%*s", USAGE_OPTS_WIDTH, "");
}

static const struct option *find_option_by_long_name(const struct option *opts,
const char *long_name)
{
for (; opts->type != OPTION_END; opts++) {
if (opts->long_name && !strcmp(opts->long_name, long_name))
return opts;
}
return NULL;
}


static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t *ctx, static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t *ctx,
const char * const *usagestr, const char * const *usagestr,
const struct option *opts, const struct option *opts,
int full, int err) int full, int err)
{ {
const struct option *all_opts = opts;
FILE *outfile = err ? stderr : stdout; FILE *outfile = err ? stderr : stdout;
int need_newline; int need_newline;


@ -1111,8 +1134,8 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t


for (; opts->type != OPTION_END; opts++) { for (; opts->type != OPTION_END; opts++) {
size_t pos; size_t pos;
int pad;
const char *cp, *np; const char *cp, *np;
const char *positive_name = NULL;


if (opts->type == OPTION_SUBCOMMAND) if (opts->type == OPTION_SUBCOMMAND)
continue; continue;
@ -1131,7 +1154,7 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t
need_newline = 0; need_newline = 0;
} }


pos = fprintf(outfile, " "); pos = usage_indent(outfile);
if (opts->short_name) { if (opts->short_name) {
if (opts->flags & PARSE_OPT_NODASH) if (opts->flags & PARSE_OPT_NODASH)
pos += fprintf(outfile, "%c", opts->short_name); pos += fprintf(outfile, "%c", opts->short_name);
@ -1140,8 +1163,15 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t
} }
if (opts->long_name && opts->short_name) if (opts->long_name && opts->short_name)
pos += fprintf(outfile, ", "); pos += fprintf(outfile, ", ");
if (opts->long_name) if (opts->long_name) {
pos += fprintf(outfile, "--%s", opts->long_name); const char *long_name = opts->long_name;
if ((opts->flags & PARSE_OPT_NONEG) ||
skip_prefix(long_name, "no-", &positive_name))
pos += fprintf(outfile, "--%s", long_name);
else
pos += fprintf(outfile, "--[no-]%s", long_name);
}

if (opts->type == OPTION_NUMBER) if (opts->type == OPTION_NUMBER)
pos += utf8_fprintf(outfile, _("-NUM")); pos += utf8_fprintf(outfile, _("-NUM"));


@ -1149,16 +1179,8 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t
!(opts->flags & PARSE_OPT_NOARG)) !(opts->flags & PARSE_OPT_NOARG))
pos += usage_argh(opts, outfile); pos += usage_argh(opts, outfile);


if (pos == USAGE_OPTS_WIDTH + 1)
pad = -1;
else if (pos <= USAGE_OPTS_WIDTH)
pad = USAGE_OPTS_WIDTH - pos;
else {
fputc('\n', outfile);
pad = USAGE_OPTS_WIDTH;
}
if (opts->type == OPTION_ALIAS) { if (opts->type == OPTION_ALIAS) {
fprintf(outfile, "%*s", pad + USAGE_GAP, ""); usage_padding(outfile, pos);
fprintf_ln(outfile, _("alias of --%s"), fprintf_ln(outfile, _("alias of --%s"),
(const char *)opts->value); (const char *)opts->value);
continue; continue;
@ -1166,12 +1188,21 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t


for (cp = _(opts->help); *cp; cp = np) { for (cp = _(opts->help); *cp; cp = np) {
np = strchrnul(cp, '\n'); np = strchrnul(cp, '\n');
fprintf(outfile, usage_padding(outfile, pos);
"%*s%.*s\n", pad + USAGE_GAP, "", fprintf(outfile, "%.*s\n", (int)(np - cp), cp);
(int)(np - cp), cp);
if (*np) if (*np)
np++; np++;
pad = USAGE_OPTS_WIDTH; pos = 0;
}

if (positive_name) {
if (find_option_by_long_name(all_opts, positive_name))
continue;
pos = usage_indent(outfile);
pos += fprintf(outfile, "--%s", positive_name);
usage_padding(outfile, pos);
fprintf_ln(outfile, _("opposite of --no-%s"),
positive_name);
} }
} }
fputc('\n', outfile); fputc('\n', outfile);

View File

@ -13,30 +13,35 @@ usage: test-tool parse-options <options>


A helper function for the parse-options API. A helper function for the parse-options API.


--yes get a boolean --[no-]yes get a boolean
-D, --no-doubt begins with 'no-' -D, --no-doubt begins with 'no-'
--doubt opposite of --no-doubt
-B, --no-fear be brave -B, --no-fear be brave
-b, --boolean increment by one -b, --[no-]boolean increment by one
-4, --or4 bitwise-or boolean with ...0100 -4, --[no-]or4 bitwise-or boolean with ...0100
--neg-or4 same as --no-or4 --[no-]neg-or4 same as --no-or4


-i, --integer <n> get a integer -i, --[no-]integer <n>
get a integer
-j <n> get a integer, too -j <n> get a integer, too
-m, --magnitude <n> get a magnitude -m, --magnitude <n> get a magnitude
--set23 set integer to 23 --[no-]set23 set integer to 23
--mode1 set integer to 1 (cmdmode option) --mode1 set integer to 1 (cmdmode option)
--mode2 set integer to 2 (cmdmode option) --mode2 set integer to 2 (cmdmode option)
-L, --length <str> get length of <str> -L, --[no-]length <str>
-F, --file <file> set file to <file> get length of <str>
-F, --[no-]file <file>
set file to <file>


String options String options
-s, --string <string> get a string -s, --[no-]string <string>
--string2 <str> get another string get a string
--st <st> get another string (pervert ordering) --[no-]string2 <str> get another string
--[no-]st <st> get another string (pervert ordering)
-o <str> get another string -o <str> get another string
--longhelp help text of this entry --longhelp help text of this entry
spans multiple lines spans multiple lines
--list <str> add str to list --[no-]list <str> add str to list


Magic arguments Magic arguments
-NUM set integer to NUM -NUM set integer to NUM
@ -45,16 +50,17 @@ Magic arguments
--no-ambiguous negative ambiguity --no-ambiguous negative ambiguity


Standard options Standard options
--abbrev[=<n>] use <n> digits to display object names --[no-]abbrev[=<n>] use <n> digits to display object names
-v, --verbose be verbose -v, --[no-]verbose be verbose
-n, --dry-run dry run -n, --[no-]dry-run dry run
-q, --quiet be quiet -q, --[no-]quiet be quiet
--expect <string> expected output in the variable dump --[no-]expect <string>
expected output in the variable dump


Alias Alias
-A, --alias-source <string> -A, --[no-]alias-source <string>
get a string get a string
-Z, --alias-target <string> -Z, --[no-]alias-target <string>
alias of --alias-source alias of --alias-source


EOF EOF

View File

@ -3,13 +3,29 @@
test_description='test git rev-parse --parseopt' test_description='test git rev-parse --parseopt'
. ./test-lib.sh . ./test-lib.sh


check_invalid_long_option () {
spec="$1"
opt="$2"
test_expect_success "test --parseopt invalid switch $opt help output for $spec" '
{
cat <<-\EOF &&
error: unknown option `'${opt#--}\''
EOF
sed -e 1d -e \$d <"$TEST_DIRECTORY/t1502/$spec.help"
} >expect &&
test_expect_code 129 git rev-parse --parseopt -- $opt \
2>output <"$TEST_DIRECTORY/t1502/$spec" &&
test_cmp expect output
'
}

test_expect_success 'setup optionspec' ' test_expect_success 'setup optionspec' '
sed -e "s/^|//" >optionspec <<\EOF sed -e "s/^|//" >optionspec <<\EOF
|some-command [options] <args>... |some-command [options] <args>...
| |
|some-command does foo and bar! |some-command does foo and bar!
|-- |--
|h,help show the help |h,help! show the help
| |
|foo some nifty option --foo |foo some nifty option --foo
|bar= some cool option --bar with an argument |bar= some cool option --bar with an argument
@ -58,44 +74,8 @@ EOF
' '


test_expect_success 'test --parseopt help output' ' test_expect_success 'test --parseopt help output' '
sed -e "s/^|//" >expect <<\END_EXPECT &&
|cat <<\EOF
|usage: some-command [options] <args>...
|
| some-command does foo and bar!
|
| -h, --help show the help
| --foo some nifty option --foo
| --bar ... some cool option --bar with an argument
| -b, --baz a short and long option
|
|An option group Header
| -C[...] option C with an optional argument
| -d, --data[=...] short and long option with an optional argument
|
|Argument hints
| -B <arg> short option required argument
| --bar2 <arg> long option required argument
| -e, --fuz <with-space>
| short and long option required argument
| -s[<some>] short option optional argument
| --long[=<data>] long option optional argument
| -g, --fluf[=<path>] short and long option optional argument
| --longest <very-long-argument-hint>
| a very long argument hint
| --pair <key=value> with an equals sign in the hint
| --aswitch help te=t contains? fl*g characters!`
| --bswitch <hint> hint has trailing tab character
| --cswitch switch has trailing tab character
| --short-hint <a> with a one symbol hint
|
|Extras
| --extra1 line above used to cause a segfault but no longer does
|
|EOF
END_EXPECT
test_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec && test_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec &&
test_cmp expect output test_cmp "$TEST_DIRECTORY/t1502/optionspec.help" output
' '


test_expect_success 'test --parseopt help output no switches' ' test_expect_success 'test --parseopt help output no switches' '
@ -131,7 +111,7 @@ test_expect_success 'test --parseopt help-all output hidden switches' '
| |
| some-command does foo and bar! | some-command does foo and bar!
| |
| --hidden1 A hidden switch | --[no-]hidden1 A hidden switch
| |
|EOF |EOF
END_EXPECT END_EXPECT
@ -140,41 +120,12 @@ END_EXPECT
' '


test_expect_success 'test --parseopt invalid switch help output' ' test_expect_success 'test --parseopt invalid switch help output' '
sed -e "s/^|//" >expect <<\END_EXPECT && {
|error: unknown option `does-not-exist'\'' cat <<-\EOF &&
|usage: some-command [options] <args>... error: unknown option `does-not-exist'\''
| EOF
| some-command does foo and bar! sed -e 1d -e \$d <"$TEST_DIRECTORY/t1502/optionspec.help"
| } >expect &&
| -h, --help show the help
| --foo some nifty option --foo
| --bar ... some cool option --bar with an argument
| -b, --baz a short and long option
|
|An option group Header
| -C[...] option C with an optional argument
| -d, --data[=...] short and long option with an optional argument
|
|Argument hints
| -B <arg> short option required argument
| --bar2 <arg> long option required argument
| -e, --fuz <with-space>
| short and long option required argument
| -s[<some>] short option optional argument
| --long[=<data>] long option optional argument
| -g, --fluf[=<path>] short and long option optional argument
| --longest <very-long-argument-hint>
| a very long argument hint
| --pair <key=value> with an equals sign in the hint
| --aswitch help te=t contains? fl*g characters!`
| --bswitch <hint> hint has trailing tab character
| --cswitch switch has trailing tab character
| --short-hint <a> with a one symbol hint
|
|Extras
| --extra1 line above used to cause a segfault but no longer does
|
END_EXPECT
test_expect_code 129 git rev-parse --parseopt -- --does-not-exist 1>/dev/null 2>output < optionspec && test_expect_code 129 git rev-parse --parseopt -- --does-not-exist 1>/dev/null 2>output < optionspec &&
test_cmp expect output test_cmp expect output
' '
@ -288,7 +239,7 @@ test_expect_success 'test --parseopt help output: "wrapped" options normal "or:"
| [--another-option] | [--another-option]
|cmd [--yet-another-option] |cmd [--yet-another-option]
|-- |--
|h,help show the help |h,help! show the help
EOF EOF


sed -e "s/^|//" >expect <<-\END_EXPECT && sed -e "s/^|//" >expect <<-\END_EXPECT &&
@ -322,7 +273,7 @@ test_expect_success 'test --parseopt help output: multi-line blurb after empty l
|line |line
|blurb |blurb
|-- |--
|h,help show the help |h,help! show the help
EOF EOF


sed -e "s/^|//" >expect <<-\END_EXPECT && sed -e "s/^|//" >expect <<-\END_EXPECT &&
@ -343,4 +294,32 @@ test_expect_success 'test --parseopt help output: multi-line blurb after empty l
test_cmp expect actual test_cmp expect actual
' '


test_expect_success 'test --parseopt help output for optionspec-neg' '
test_expect_code 129 git rev-parse --parseopt -- \
-h >output <"$TEST_DIRECTORY/t1502/optionspec-neg" &&
test_cmp "$TEST_DIRECTORY/t1502/optionspec-neg.help" output
'

test_expect_success 'test --parseopt valid options for optionspec-neg' '
cat >expect <<-\EOF &&
set -- --foo --no-foo --no-bar --positive-only --no-negative --
EOF
git rev-parse --parseopt -- <"$TEST_DIRECTORY/t1502/optionspec-neg" >output \
--foo --no-foo --no-bar --positive-only --no-negative &&
test_cmp expect output
'

test_expect_success 'test --parseopt positivated option for optionspec-neg' '
cat >expect <<-\EOF &&
set -- --no-no-bar --no-no-bar --
EOF
git rev-parse --parseopt -- <"$TEST_DIRECTORY/t1502/optionspec-neg" >output \
--no-no-bar --bar &&
test_cmp expect output
'

check_invalid_long_option optionspec-neg --no-positive-only
check_invalid_long_option optionspec-neg --negative
check_invalid_long_option optionspec-neg --no-no-negative

test_done test_done

1
t/t1502/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* -whitespace

8
t/t1502/optionspec-neg Normal file
View File

@ -0,0 +1,8 @@
some-command [options] <args>...

some-command does foo and bar!
--
foo can be negated
no-bar can be positivated
positive-only! cannot be negated
no-negative! cannot be positivated

View File

@ -0,0 +1,12 @@
cat <<\EOF
usage: some-command [options] <args>...

some-command does foo and bar!

--[no-]foo can be negated
--no-bar can be positivated
--bar opposite of --no-bar
--positive-only cannot be negated
--no-negative cannot be positivated

EOF

36
t/t1502/optionspec.help Executable file
View File

@ -0,0 +1,36 @@
cat <<\EOF
usage: some-command [options] <args>...

some-command does foo and bar!

-h, --help show the help
--[no-]foo some nifty option --foo
--[no-]bar ... some cool option --bar with an argument
-b, --[no-]baz a short and long option

An option group Header
-C[...] option C with an optional argument
-d, --[no-]data[=...] short and long option with an optional argument

Argument hints
-B <arg> short option required argument
--[no-]bar2 <arg> long option required argument
-e, --[no-]fuz <with-space>
short and long option required argument
-s[<some>] short option optional argument
--[no-]long[=<data>] long option optional argument
-g, --[no-]fluf[=<path>]
short and long option optional argument
--[no-]longest <very-long-argument-hint>
a very long argument hint
--[no-]pair <key=value>
with an equals sign in the hint
--[no-]aswitch help te=t contains? fl*g characters!`
--[no-]bswitch <hint> hint has trailing tab character
--[no-]cswitch switch has trailing tab character
--[no-]short-hint <a> with a one symbol hint

Extras
--[no-]extra1 line above used to cause a segfault but no longer does

EOF