Repository: pallets/click

Issue:
"Missing argument" message uses arg name, not metavar
If one defines the metavar different from the argument name, like in:
```
@adapter_group.command('list')
@click.argument('CPC-NAME', type=str, metavar='CPC')
@click.pass_obj
def adapter_list(cmd_obj, cpc_name):
```
the usage line in the help text correctly uses the metavar:
```
Usage: zhmc adapter list [OPTIONS] CPC
```
but the error message when invoked without arguments incorrectly uses the argument name:
```
Error: Missing argument "CPC-NAME".
```

I suggest that the metavar is used consistently in all display to the user.
"Missing argument" message uses arg name, not metavar
If one defines the metavar different from the argument name, like in:
```
@adapter_group.command('list')
@click.argument('CPC-NAME', type=str, metavar='CPC')
@click.pass_obj
def adapter_list(cmd_obj, cpc_name):
```
the usage line in the help text correctly uses the metavar:
```
Usage: zhmc adapter list [OPTIONS] CPC
```
but the error message when invoked without arguments incorrectly uses the argument name:
```
Error: Missing argument "CPC-NAME".
```

I suggest that the metavar is used consistently in all display to the user.


Discussion:
This is generated by a bit of a head scratcher, in the [formatting code](https://github.com/pallets/click/blob/30fc626263db82e9f339ca8810d207ab089d5575/click/exceptions.py#L117-L145) for a `MissingParameter` error.

If I'm reading this right, it's trying to be generic in its handling of Options and Arguments, so it takes `param.opts` as a relevant "name" for the parameter, and slash delimits all of the available names.
That gets nice help like `Missing option "-f/--foo"`, and explains the issue here.

`Argument.get_usage_pieces` looks at the metavar correctly. Without tracing this out thoroughly, it seems like this is the key difference.

---

Opinion Zone:
The real thing that's wrong here is that `Argument` has a non-empty `opts` value. I've seen this elsewhere in the code and it seems wrong -- the name doesn't help either, as it implies that it's a good detector of Option vs. Argument.
Fixing that would be a breaking change as completion and other advanced usage may depend upon this value.

---

There are a bunch of ways of fixing this.
Probably the least invasive is to make `MissingParameter` formatting read `param.get_usage_pieces` and see if there's anything there. However, there's so much layering going on here, I don't really want to act without some kind of input from the maintainers.
Plus, although pragmatic, that fix is icky and may be abusing the meaning of `get_usage_pieces`.

Something that is common throughout is that a lot of the code handles Options and Argument equivalently. I'm not sure how much people are defining their own param types, and adding an attribute "option-style" vs. "argument-style" would probably resolve a lot of this confusion.
Oh, idea which is not icky or messy and resolves a DRY violation: add a `get_error_hint()` method to Parameters, and fix `MissingParameter` and `BadParameter` to use it.
I'll do that and open a PR now.
This is generated by a bit of a head scratcher, in the [formatting code](https://github.com/pallets/click/blob/30fc626263db82e9f339ca8810d207ab089d5575/click/exceptions.py#L117-L145) for a `MissingParameter` error.

If I'm reading this right, it's trying to be generic in its handling of Options and Arguments, so it takes `param.opts` as a relevant "name" for the parameter, and slash delimits all of the available names.
That gets nice help like `Missing option "-f/--foo"`, and explains the issue here.

`Argument.get_usage_pieces` looks at the metavar correctly. Without tracing this out thoroughly, it seems like this is the key difference.

---

Opinion Zone:
The real thing that's wrong here is that `Argument` has a non-empty `opts` value. I've seen this elsewhere in the code and it seems wrong -- the name doesn't help either, as it implies that it's a good detector of Option vs. Argument.
Fixing that would be a breaking change as completion and other advanced usage may depend upon this value.

---

There are a bunch of ways of fixing this.
Probably the least invasive is to make `MissingParameter` formatting read `param.get_usage_pieces` and see if there's anything there. However, there's so much layering going on here, I don't really want to act without some kind of input from the maintainers.
Plus, although pragmatic, that fix is icky and may be abusing the meaning of `get_usage_pieces`.

Something that is common throughout is that a lot of the code handles Options and Argument equivalently. I'm not sure how much people are defining their own param types, and adding an attribute "option-style" vs. "argument-style" would probably resolve a lot of this confusion.
Oh, idea which is not icky or messy and resolves a DRY violation: add a `get_error_hint()` method to Parameters, and fix `MissingParameter` and `BadParameter` to use it.
I'll do that and open a PR now.

Resolving patch:
diff --git a/click/core.py b/click/core.py
--- a/click/core.py
+++ b/click/core.py
@@ -1431,6 +1431,13 @@ def get_help_record(self, ctx):
     def get_usage_pieces(self, ctx):
         return []
 
+    def get_error_hint(self, ctx):
+        """Get a stringified version of the param for use in error messages to
+        indicate which param caused the error.
+        """
+        hint_list = self.opts or [self.human_readable_name]
+        return ' / '.join('"%s"' % x for x in hint_list)
+
 
 class Option(Parameter):
     """Options are usually optional values on the command line and
@@ -1755,6 +1762,9 @@ def _parse_decls(self, decls, expose_value):
     def get_usage_pieces(self, ctx):
         return [self.make_metavar()]
 
+    def get_error_hint(self, ctx):
+        return '"%s"' % self.make_metavar()
+
     def add_to_parser(self, parser, ctx):
         parser.add_argument(dest=self.name, nargs=self.nargs,
                             obj=self)
diff --git a/click/exceptions.py b/click/exceptions.py
--- a/click/exceptions.py
+++ b/click/exceptions.py
@@ -2,6 +2,12 @@
 from .utils import echo
 
 
+def _join_param_hints(param_hint):
+    if isinstance(param_hint, (tuple, list)):
+        return ' / '.join('"%s"' % x for x in param_hint)
+    return param_hint
+
+
 class ClickException(Exception):
     """An exception that Click can handle and show to the user."""
 
@@ -92,11 +98,11 @@ def format_message(self):
         if self.param_hint is not None:
             param_hint = self.param_hint
         elif self.param is not None:
-            param_hint = self.param.opts or [self.param.human_readable_name]
+            param_hint = self.param.get_error_hint(self.ctx)
         else:
             return 'Invalid value: %s' % self.message
-        if isinstance(param_hint, (tuple, list)):
-            param_hint = ' / '.join('"%s"' % x for x in param_hint)
+        param_hint = _join_param_hints(param_hint)
+
         return 'Invalid value for %s: %s' % (param_hint, self.message)
 
 
@@ -121,11 +127,10 @@ def format_message(self):
         if self.param_hint is not None:
             param_hint = self.param_hint
         elif self.param is not None:
-            param_hint = self.param.opts or [self.param.human_readable_name]
+            param_hint = self.param.get_error_hint(self.ctx)
         else:
             param_hint = None
-        if isinstance(param_hint, (tuple, list)):
-            param_hint = ' / '.join('"%s"' % x for x in param_hint)
+        param_hint = _join_param_hints(param_hint)
 
         param_type = self.param_type
         if param_type is None and self.param is not None:


Test patch:
diff --git a/tests/test_arguments.py b/tests/test_arguments.py
--- a/tests/test_arguments.py
+++ b/tests/test_arguments.py
@@ -188,7 +188,7 @@ def cmd2(arg):
 
     result = runner.invoke(cmd2, [])
     assert result.exit_code == 2
-    assert 'Missing argument "arg"' in result.output
+    assert 'Missing argument "ARG..."' in result.output
 
 
 def test_missing_arg(runner):
@@ -199,7 +199,7 @@ def cmd(arg):
 
     result = runner.invoke(cmd, [])
     assert result.exit_code == 2
-    assert 'Missing argument "arg".' in result.output
+    assert 'Missing argument "ARG".' in result.output
 
 
 def test_implicit_non_required(runner):
diff --git a/tests/test_formatting.py b/tests/test_formatting.py
--- a/tests/test_formatting.py
+++ b/tests/test_formatting.py
@@ -159,7 +159,43 @@ def cmd(arg):
         'Usage: cmd [OPTIONS] ARG',
         'Try "cmd --help" for help.',
         '',
-        'Error: Missing argument "arg".'
+        'Error: Missing argument "ARG".'
+    ]
+
+
+def test_formatting_usage_error_metavar_missing_arg(runner):
+    """
+    :author: @r-m-n
+    Including attribution to #612
+    """
+    @click.command()
+    @click.argument('arg', metavar='metavar')
+    def cmd(arg):
+        pass
+
+    result = runner.invoke(cmd, [])
+    assert result.exit_code == 2
+    assert result.output.splitlines() == [
+        'Usage: cmd [OPTIONS] metavar',
+        'Try "cmd --help" for help.',
+        '',
+        'Error: Missing argument "metavar".'
+    ]
+
+
+def test_formatting_usage_error_metavar_bad_arg(runner):
+    @click.command()
+    @click.argument('arg', type=click.INT, metavar='metavar')
+    def cmd(arg):
+        pass
+
+    result = runner.invoke(cmd, ['3.14'])
+    assert result.exit_code == 2
+    assert result.output.splitlines() == [
+        'Usage: cmd [OPTIONS] metavar',
+        'Try "cmd --help" for help.',
+        '',
+        'Error: Invalid value for "metavar": 3.14 is not a valid integer'
     ]
 
 
@@ -179,7 +215,7 @@ def foo(bar):
         'Usage: cmd foo [OPTIONS] BAR',
         'Try "cmd foo --help" for help.',
         '',
-        'Error: Missing argument "bar".'
+        'Error: Missing argument "BAR".'
     ]
 
 
@@ -194,7 +230,7 @@ def cmd(arg):
     assert result.output.splitlines() == [
         'Usage: cmd [OPTIONS] ARG',
         '',
-        'Error: Missing argument "arg".'
+        'Error: Missing argument "ARG".'
     ]
 
 
@@ -210,5 +246,5 @@ def cmd(arg):
         'Usage: cmd [OPTIONS] ARG',
         'Try "cmd --man" for help.',
         '',
-        'Error: Missing argument "arg".'
+        'Error: Missing argument "ARG".'
     ]