Repository: pallets/click

Issue:
strip "_command" suffix when generating command name
You often want a command to have the same name as a function it is wrapping. However, this will cause a name collision, if your command is going to call `init_data` its function can't also be called `init_data`. So instead it's written as `init_data_command`, and then the command name has to be specified manually `@command("init-data")`. Click should remove a `_command` suffix when generating the command name from the function name.

```python
def init_data(external=False):
    ...

@cli.command
@click.option("--external/--internal")
def init_data_command(external):
    init_data(external=external)

```

Currently this command would be named `init-data-command`. After the change, it would be `init-data`.


Discussion:
Hi @davidism, Am I right in assuming the expected behaviour would be that both `_command` and `_cmd` suffixes are dropped?

An example using `_cmd` as a suffix:

https://github.com/pallets/click/blob/fb4327216543d751e2654a0d3bf6ce3d31c435cc/examples/imagepipe/imagepipe.py#L74-L98

Resolving patch:
diff --git a/src/click/decorators.py b/src/click/decorators.py
--- a/src/click/decorators.py
+++ b/src/click/decorators.py
@@ -178,9 +178,10 @@ def command(
     callback.  This will also automatically attach all decorated
     :func:`option`\s and :func:`argument`\s as parameters to the command.
 
-    The name of the command defaults to the name of the function with
-    underscores replaced by dashes.  If you want to change that, you can
-    pass the intended name as the first argument.
+    The name of the command defaults to the name of the function, converted to
+    lowercase, with underscores ``_`` replaced by dashes ``-``, and the suffixes
+    ``_command``, ``_cmd``, ``_group``, and ``_grp`` are removed. For example,
+    ``init_data_command`` becomes ``init-data``.
 
     All keyword arguments are forwarded to the underlying command class.
     For the ``params`` argument, any decorated params are appended to
@@ -190,10 +191,13 @@ def command(
     that can be invoked as a command line utility or be attached to a
     command :class:`Group`.
 
-    :param name: the name of the command.  This defaults to the function
-                 name with underscores replaced by dashes.
-    :param cls: the command class to instantiate.  This defaults to
-                :class:`Command`.
+    :param name: The name of the command. Defaults to modifying the function's
+        name as described above.
+    :param cls: The command class to create. Defaults to :class:`Command`.
+
+    .. versionchanged:: 8.2
+        The suffixes ``_command``, ``_cmd``, ``_group``, and ``_grp`` are
+        removed when generating the name.
 
     .. versionchanged:: 8.1
         This decorator can be applied without parentheses.
@@ -236,12 +240,16 @@ def decorator(f: _AnyCallable) -> CmdType:
             assert cls is not None
             assert not callable(name)
 
-        cmd = cls(
-            name=name or f.__name__.lower().replace("_", "-"),
-            callback=f,
-            params=params,
-            **attrs,
-        )
+        if name is not None:
+            cmd_name = name
+        else:
+            cmd_name = f.__name__.lower().replace("_", "-")
+            cmd_left, sep, suffix = cmd_name.rpartition("-")
+
+            if sep and suffix in {"command", "cmd", "group", "grp"}:
+                cmd_name = cmd_left
+
+        cmd = cls(name=cmd_name, callback=f, params=params, **attrs)
         cmd.__doc__ = f.__doc__
         return cmd
 


Test patch:
diff --git a/tests/test_command_decorators.py b/tests/test_command_decorators.py
--- a/tests/test_command_decorators.py
+++ b/tests/test_command_decorators.py
@@ -1,3 +1,5 @@
+import pytest
+
 import click
 
 
@@ -69,3 +71,22 @@ def cli(a, b):
     assert cli.params[1].name == "b"
     result = runner.invoke(cli, ["1", "2"])
     assert result.output == "1 2\n"
+
+
+@pytest.mark.parametrize(
+    "name",
+    [
+        "init_data",
+        "init_data_command",
+        "init_data_cmd",
+        "init_data_group",
+        "init_data_grp",
+    ],
+)
+def test_generate_name(name: str) -> None:
+    def f():
+        pass
+
+    f.__name__ = name
+    f = click.command(f)
+    assert f.name == "init-data"
diff --git a/tests/test_commands.py b/tests/test_commands.py
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -249,7 +249,7 @@ def other_cmd(ctx, a, b, c):
     result = runner.invoke(cli, standalone_mode=False)
     # invoke should type cast default values, str becomes int, empty
     # multiple should be empty tuple instead of None
-    assert result.return_value == ("other-cmd", 42, 15, ())
+    assert result.return_value == ("other", 42, 15, ())
 
 
 def test_invoked_subcommand(runner):