Repository: pallets/click

Issue:
Allow `params` attribute in decorators
Currently it is not possible to pass `params` attribute to `@command()` and `@group()` decorators.
This would be really simple implementation, which can be seen in [this forked commit](https://github.com/kristianheljas/click/commit/4e908df69ab11eb333fa14bcf4f3b54b77810ef4)

Although, this allows to mix decorator and class patterns, there are a few advantages as well:

1. Share same option spec between (sub)commands (consider CRUD subcommands which all have `--force` option, except `read`)
It can be done with nested decorators, which hurt readability
2. Take advantage of IDE auto-completion when using `Option()` class instead of decorator ([PEP-612](https://www.python.org/dev/peps/pep-0612/) would resolve this, but it requires python >=3.10)

If this is something to be considered, I can follow up with a PR.

Example usage:

```python
import click

@click.group()
def crud():
    ...

write_args = [
    click.Option(['--force'], is_flag=True)
]

@crud.command(params=write_args)
def create():
    ...

@crud.command(params=write_args)
@click.option('--create', is_flag=True) # Should this be appended or prepended to `write_args`?
def update():
    ...

@crud.command(params=write_args)
def delete():
    ...

@crud.command()
def read():
    ...
```



Discussion:

Resolving patch:
diff --git a/src/click/decorators.py b/src/click/decorators.py
--- a/src/click/decorators.py
+++ b/src/click/decorators.py
@@ -153,6 +153,8 @@ def command(
     pass the intended name as the first argument.
 
     All keyword arguments are forwarded to the underlying command class.
+    For the ``params`` argument, any decorated params are appended to
+    the end of the list.
 
     Once decorated the function turns into a :class:`Command` instance
     that can be invoked as a command line utility or be attached to a
@@ -165,6 +167,10 @@ def command(
 
     .. versionchanged:: 8.1
         This decorator can be applied without parentheses.
+
+    .. versionchanged:: 8.1
+        The ``params`` argument can be used. Decorated params are
+        appended to the end of the list.
     """
     if cls is None:
         cls = Command
@@ -179,13 +185,16 @@ def decorator(f: t.Callable[..., t.Any]) -> Command:
         if isinstance(f, Command):
             raise TypeError("Attempted to convert a callback into a command twice.")
 
+        attr_params = attrs.pop("params", None)
+        params = attr_params if attr_params is not None else []
+
         try:
-            params = f.__click_params__  # type: ignore
+            decorator_params = f.__click_params__  # type: ignore
         except AttributeError:
-            params = []
+            pass
         else:
-            params.reverse()
             del f.__click_params__  # type: ignore
+            params.extend(reversed(decorator_params))
 
         if attrs.get("help") is None:
             attrs["help"] = f.__doc__


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
@@ -35,3 +35,17 @@ def cmd2():
     result = runner.invoke(grp, ["grp2", "cmd2"])
     assert result.exception is None
     assert result.output == "grp1\ngrp2\ncmd2\n"
+
+
+def test_params_argument(runner):
+    opt = click.Argument(["a"])
+
+    @click.command(params=[opt])
+    @click.argument("b")
+    def cli(a, b):
+        click.echo(f"{a} {b}")
+
+    assert cli.params[0].name == "a"
+    assert cli.params[1].name == "b"
+    result = runner.invoke(cli, ["1", "2"])
+    assert result.output == "1 2\n"