Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Version 8.4.2

Unreleased

- A :class:`Group` with ``invoke_without_command=True`` marks its subcommand as
optional in the usage help, showing ``[COMMAND]`` instead of ``COMMAND``.
:issue:`3059` :pr:`3507`


Version 8.4.1
-------------
Expand Down
8 changes: 8 additions & 0 deletions docs/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,14 @@ The default placeholder variable ([meta variable](https://en.wikipedia.org/wiki/

```

```{admonition} Optional elements are bracketed
:class: note

The usage line follows a consistent convention: an element is optional when it is enclosed in square brackets and required when it appears outside them. A required argument appears as `FOO` and an optional one as `[FOO]`, with a variadic argument adding a trailing `...` (so `[FOO]...`). A single pair of brackets can group several tokens, so a chained group renders an extra command and its arguments as one optional, repeatable unit: `[COMMAND2 [ARGS]...]...`. A {class}`Group` that can run without naming a subcommand (`invoke_without_command=True`) also brackets the leading command, showing `[COMMAND]` instead of `COMMAND`. The `[OPTIONS]` placeholder is always bracketed, since adding options is itself optional.

This mirrors the conventional utility-argument syntax for optional arguments and operands ([POSIX §12.1](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html), [`man-pages(7)`](https://man7.org/linux/man-pages/man7/man-pages.7.html)), where a bracketed expression followed by `...` denotes zero or more occurrences. A subcommand name is a positional operand, so Click brackets it under the same rule.
```

## Help Parameter Customization

Help parameters are automatically added by Click for any command. The default is `--help` but can be overridden by the context setting {attr}`~Context.help_option_names`. Click also performs automatic conflict resolution on the default help parameter, so if a command itself implements a parameter named `help` then the default help will not be run.
Expand Down
9 changes: 8 additions & 1 deletion src/click/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1622,8 +1622,15 @@ def __init__(
self.invoke_without_command = invoke_without_command

if subcommand_metavar is None:
# When the group can run without a subcommand, the leading command
# token is optional, so wrap it in brackets to reflect that.
if chain:
subcommand_metavar = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."
if invoke_without_command:
subcommand_metavar = "[COMMAND1] [ARGS]... [COMMAND2 [ARGS]...]..."
else:
subcommand_metavar = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."
elif invoke_without_command:
subcommand_metavar = "[COMMAND] [ARGS]..."
else:
subcommand_metavar = "COMMAND [ARGS]..."

Expand Down
26 changes: 26 additions & 0 deletions tests/test_arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,32 @@ def cli(f):
assert "[F!]" in result.output


@pytest.mark.parametrize(
("kwargs", "expected"),
[
({}, "FOO"),
({"required": True}, "FOO"),
({"required": False}, "[FOO]"),
({"default": "x"}, "[FOO]"),
({"nargs": -1}, "[FOO]..."),
({"nargs": -1, "required": True}, "FOO..."),
({"nargs": 2}, "FOO..."),
({"nargs": 2, "required": False}, "[FOO]..."),
],
)
def test_argument_metavar_marks_optional(runner, kwargs, expected):
"""An argument is bracketed in the usage line only when it is optional."""

@click.command()
@click.argument("foo", **kwargs)
def cli(foo):
pass

result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0
assert result.output.splitlines()[0] == f"Usage: cli [OPTIONS] {expected}"


@pytest.mark.parametrize("deprecated", [True, "USE B INSTEAD"])
def test_deprecated_warning(runner, deprecated):
@click.command()
Expand Down
27 changes: 27 additions & 0 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,33 @@ def sync():
assert result.output == "no subcommand, use default\nin subcommand\n"


@pytest.mark.parametrize(
("chain", "invoke_without_command", "metavar"),
[
(False, False, "COMMAND [ARGS]..."),
(False, True, "[COMMAND] [ARGS]..."),
(True, False, "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."),
(True, True, "[COMMAND1] [ARGS]... [COMMAND2 [ARGS]...]..."),
],
)
def test_subcommand_metavar_marks_optional(
runner, chain, invoke_without_command, metavar
):
"""The leading subcommand token is bracketed only when it is optional."""

@click.group(chain=chain, invoke_without_command=invoke_without_command)
def cli():
pass

@cli.command()
def sub():
pass

result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0
assert result.output.splitlines()[0] == f"Usage: cli [OPTIONS] {metavar}"


def test_aliased_command_canonical_name(runner):
class AliasedGroup(click.Group):
def get_command(self, ctx, cmd_name):
Expand Down
Loading