Repository: pallets/click

Issue:
Can't mix nargs=-1 with envvar
`@click.argument(name, nargs=2, envvar=NAME)`

Works as expected. If you set `NAME="a b"` and run your command `name` in the called function will be a tuple of `('a', 'b')`. 

`@click.argument(name, nargs=-1, envvar=NAME)`

on the other hand doesn't work as expected. If you call it in the same exact way, `name` will be set to the empty tuple.


Discussion:
Thanks!

Looks like there may be a couple bugs around this behavior.

my test code to reproduce

```python
@click.group()
def cli():
    pass

@cli.command(short_help='test')
@click.argument('src', nargs=-1, envvar="TEST")
def testing(src):
    click.echo("src")
    click.echo(src)
    click.echo("len %s" % len(src))
    click.echo("type %s" % type(src))
    for c,v in enumerate(src):
        click.echo('elemenet %s of src: %s ' % (c,v))
cli()
```

case above nargs=-1 gives an empty tuple

```
TEST="one two three 4" python repo.py testing
src
()
len 0
type <class 'tuple'>
```

if we run with nargs=2 shouldn't become a 4-tuple (the correct behavior here is a bit ambiguous)

```
TEST="one two three 4" python repo.py testing
src
('one', 'two', 'three', '4')
len 4
type <class 'tuple'>
elemenet 0 of src: one
elemenet 1 of src: two
elemenet 2 of src: three
elemenet 3 of src: 4
```

Resolving patch:
diff --git a/src/click/core.py b/src/click/core.py
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -1624,12 +1624,26 @@ def full_process_value(self, ctx, value):
 
         if value is None and not ctx.resilient_parsing:
             value = self.get_default(ctx)
+
             if value is not None:
                 ctx.set_parameter_source(self.name, ParameterSource.DEFAULT)
 
         if self.required and self.value_is_missing(value):
             raise MissingParameter(ctx=ctx, param=self)
 
+        # For bounded nargs (!= -1), validate the number of values.
+        if (
+            not ctx.resilient_parsing
+            and self.nargs > 1
+            and self.nargs != len(value)
+            and isinstance(value, (tuple, list))
+        ):
+            were = "was" if len(value) == 1 else "were"
+            ctx.fail(
+                f"Argument {self.name!r} takes {self.nargs} values but"
+                f" {len(value)} {were} given."
+            )
+
         return value
 
     def resolve_envvar_value(self, ctx):
diff --git a/src/click/parser.py b/src/click/parser.py
--- a/src/click/parser.py
+++ b/src/click/parser.py
@@ -183,6 +183,10 @@ def process(self, value, state):
                 raise BadArgumentUsage(
                     f"argument {self.dest} takes {self.nargs} values"
                 )
+
+        if self.nargs == -1 and self.obj.envvar is not None:
+            value = None
+
         state.opts[self.dest] = value
         state.order.append(self.obj)
 


Test patch:
diff --git a/tests/test_arguments.py b/tests/test_arguments.py
--- a/tests/test_arguments.py
+++ b/tests/test_arguments.py
@@ -18,7 +18,7 @@ def copy(src, dst):
     assert result.output.splitlines() == ["src=foo.txt|bar.txt", "dst=dir"]
 
 
-def test_nargs_default(runner):
+def test_argument_unbounded_nargs_cant_have_default(runner):
     with pytest.raises(TypeError, match="nargs=-1"):
 
         @click.command()
@@ -163,26 +163,31 @@ def inout(output):
     assert result.output == "Foo bar baz\n"
 
 
-def test_nargs_envvar(runner):
-    @click.command()
-    @click.option("--arg", nargs=2)
-    def cmd(arg):
-        click.echo("|".join(arg))
-
-    result = runner.invoke(
-        cmd, [], auto_envvar_prefix="TEST", env={"TEST_ARG": "foo bar"}
-    )
-    assert not result.exception
-    assert result.output == "foo|bar\n"
+@pytest.mark.parametrize(
+    ("nargs", "value", "code", "output"),
+    [
+        (2, "", 2, "Argument 'arg' takes 2 values but 0 were given."),
+        (2, "a", 2, "Argument 'arg' takes 2 values but 1 was given."),
+        (2, "a b", 0, "len 2"),
+        (2, "a b c", 2, "Argument 'arg' takes 2 values but 3 were given."),
+        (-1, "a b c", 0, "len 3"),
+        (-1, "", 0, "len 0"),
+    ],
+)
+def test_nargs_envvar(runner, nargs, value, code, output):
+    if nargs == -1:
+        param = click.argument("arg", envvar="X", nargs=nargs)
+    else:
+        param = click.option("--arg", envvar="X", nargs=nargs)
 
     @click.command()
-    @click.option("--arg", envvar="X", nargs=2)
+    @param
     def cmd(arg):
-        click.echo("|".join(arg))
+        click.echo(f"len {len(arg)}")
 
-    result = runner.invoke(cmd, [], env={"X": "foo bar"})
-    assert not result.exception
-    assert result.output == "foo|bar\n"
+    result = runner.invoke(cmd, env={"X": value})
+    assert result.exit_code == code
+    assert output in result.output
 
 
 def test_empty_nargs(runner):
@@ -303,3 +308,23 @@ def test_multiple_param_decls_not_allowed(runner):
         @click.argument("x", click.Choice(["a", "b"]))
         def copy(x):
             click.echo(x)
+
+
+@pytest.mark.parametrize(
+    ("value", "code", "output"),
+    [
+        ((), 2, "Argument 'arg' takes 2 values but 0 were given."),
+        (("a",), 2, "Argument 'arg' takes 2 values but 1 was given."),
+        (("a", "b"), 0, "len 2"),
+        (("a", "b", "c"), 2, "Argument 'arg' takes 2 values but 3 were given."),
+    ],
+)
+def test_nargs_default(runner, value, code, output):
+    @click.command()
+    @click.argument("arg", nargs=2, default=value)
+    def cmd(arg):
+        click.echo(f"len {len(arg)}")
+
+    result = runner.invoke(cmd)
+    assert result.exit_code == code
+    assert output in result.output