Repository: pallets/click

Issue:
Creating an option with prompt always prompts even if the option isn't added to command call
Hi, I'm having an issue when I add a prompt or password prompt option to my command. It will prompt even if the command doesn't have that option specified when calling the command in terminal.

I used this cookiecutter template to generate the boilerplate code:
https://github.com/nvie/cookiecutter-python-cli

And the default configs generate a working command that i used to test this:

1. I added an password_prompt decorator in the main command at `my_tool/cli.py`:
```python
import click


@click.command()
@click.option('--as-cowboy', '-c', is_flag=True, help='Greet as a cowboy.')
@click.password_option('--password', '-p', help='Input a password')
@click.argument('name', default='world', required=False)
def main(name, as_cowboy):
    """My Tool does one thing, and one thing well."""
    greet = 'Howdy' if as_cowboy else 'Hello'
    click.echo('{0}, {1}.'.format(greet, name))
```
2. Installed in the project virtualenv with pip
```
pip install --editable .
```

3. Calling the my-tool command in virtualenv makes click prompt for the password:
```
$ my-tool
Password:
```

As it is an option I thought that it wouldn't prompt it it isn't passed when calling the command.

There is something that I'm missing?


Discussion:
That is by design I would guess,
the documentation example shows this behaviour as well:
http://click.pocoo.org/5/options/#password-prompts
Just below the section I referenced above comes the section about callbacks, those might be usefull to you:

http://click.pocoo.org/5/options/#callbacks-and-eager-options



related to #736 

Resolving patch:
diff --git a/src/click/core.py b/src/click/core.py
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -18,6 +18,7 @@
 from .formatting import join_options
 from .globals import pop_context
 from .globals import push_context
+from .parser import _flag_needs_value
 from .parser import OptionParser
 from .parser import split_opt
 from .termui import confirm
@@ -2148,6 +2149,9 @@ class Option(Parameter):
                    option name capitalized.
     :param confirmation_prompt: if set then the value will need to be confirmed
                                 if it was prompted for.
+    :param prompt_required: If set to ``False``, the user will be
+        prompted for input only when the option was specified as a flag
+        without a value.
     :param hide_input: if this is `True` then the input on the prompt will be
                        hidden from the user.  This is useful for password
                        input.
@@ -2177,6 +2181,7 @@ def __init__(
         show_default=False,
         prompt=False,
         confirmation_prompt=False,
+        prompt_required=True,
         hide_input=False,
         is_flag=None,
         flag_value=None,
@@ -2201,21 +2206,38 @@ def __init__(
             prompt_text = prompt
         self.prompt = prompt_text
         self.confirmation_prompt = confirmation_prompt
+        self.prompt_required = prompt_required
         self.hide_input = hide_input
         self.hidden = hidden
 
-        # Flags
+        # If prompt is enabled but not required, then the option can be
+        # used as a flag to indicate using prompt or flag_value.
+        self._flag_needs_value = self.prompt is not None and not self.prompt_required
+
         if is_flag is None:
             if flag_value is not None:
+                # Implicitly a flag because flag_value was set.
                 is_flag = True
+            elif self._flag_needs_value:
+                # Not a flag, but when used as a flag it shows a prompt.
+                is_flag = False
             else:
+                # Implicitly a flag because flag options were given.
                 is_flag = bool(self.secondary_opts)
+        elif is_flag is False and not self._flag_needs_value:
+            # Not a flag, and prompt is not enabled, can be used as a
+            # flag if flag_value is set.
+            self._flag_needs_value = flag_value is not None
+
         if is_flag and default_is_missing:
             self.default = False
+
         if flag_value is None:
             flag_value = not self.default
+
         self.is_flag = is_flag
         self.flag_value = flag_value
+
         if self.is_flag and isinstance(self.flag_value, bool) and type in [None, bool]:
             self.type = BOOL
             self.is_bool_flag = True
@@ -2486,11 +2508,23 @@ def value_from_envvar(self, ctx):
     def consume_value(self, ctx, opts):
         value, source = super().consume_value(ctx, opts)
 
+        # The parser will emit a sentinel value if the option can be
+        # given as a flag without a value. This is different from None
+        # to distinguish from the flag not being given at all.
+        if value is _flag_needs_value:
+            if self.prompt is not None and not ctx.resilient_parsing:
+                value = self.prompt_for_value(ctx)
+                source = ParameterSource.PROMPT
+            else:
+                value = self.flag_value
+                source = ParameterSource.COMMANDLINE
+
         # The value wasn't set, or used the param's default, prompt if
         # prompting is enabled.
-        if (
+        elif (
             source in {None, ParameterSource.DEFAULT}
             and self.prompt is not None
+            and (self.required or self.prompt_required)
             and not ctx.resilient_parsing
         ):
             value = self.prompt_for_value(ctx)
diff --git a/src/click/parser.py b/src/click/parser.py
--- a/src/click/parser.py
+++ b/src/click/parser.py
@@ -29,6 +29,11 @@
 from .exceptions import NoSuchOption
 from .exceptions import UsageError
 
+# Sentinel value that indicates an option was passed as a flag without a
+# value but is not a flag option. Option.consume_value uses this to
+# prompt or use the flag_value.
+_flag_needs_value = object()
+
 
 def _unpack_args(args, nargs_spec):
     """Given an iterable of arguments and an iterable of nargs specifications,
@@ -81,12 +86,6 @@ def _fetch(c):
     return tuple(rv), list(args)
 
 
-def _error_opt_args(nargs, opt):
-    if nargs == 1:
-        raise BadOptionUsage(opt, f"{opt} option requires an argument")
-    raise BadOptionUsage(opt, f"{opt} option requires {nargs} arguments")
-
-
 def split_opt(opt):
     first = opt[:1]
     if first.isalnum():
@@ -343,14 +342,7 @@ def _match_long_opt(self, opt, explicit_value, state):
             if explicit_value is not None:
                 state.rargs.insert(0, explicit_value)
 
-            nargs = option.nargs
-            if len(state.rargs) < nargs:
-                _error_opt_args(nargs, opt)
-            elif nargs == 1:
-                value = state.rargs.pop(0)
-            else:
-                value = tuple(state.rargs[:nargs])
-                del state.rargs[:nargs]
+            value = self._get_value_from_state(opt, option, state)
 
         elif explicit_value is not None:
             raise BadOptionUsage(opt, f"{opt} option does not take a value")
@@ -383,14 +375,7 @@ def _match_short_opt(self, arg, state):
                     state.rargs.insert(0, arg[i:])
                     stop = True
 
-                nargs = option.nargs
-                if len(state.rargs) < nargs:
-                    _error_opt_args(nargs, opt)
-                elif nargs == 1:
-                    value = state.rargs.pop(0)
-                else:
-                    value = tuple(state.rargs[:nargs])
-                    del state.rargs[:nargs]
+                value = self._get_value_from_state(opt, option, state)
 
             else:
                 value = None
@@ -407,6 +392,38 @@ def _match_short_opt(self, arg, state):
         if self.ignore_unknown_options and unknown_options:
             state.largs.append(f"{prefix}{''.join(unknown_options)}")
 
+    def _get_value_from_state(self, option_name, option, state):
+        nargs = option.nargs
+
+        if len(state.rargs) < nargs:
+            if option.obj._flag_needs_value:
+                # Option allows omitting the value.
+                value = _flag_needs_value
+            else:
+                n_str = "an argument" if nargs == 1 else f"{nargs} arguments"
+                raise BadOptionUsage(
+                    option_name, f"{option_name} option requires {n_str}."
+                )
+        elif nargs == 1:
+            next_rarg = state.rargs[0]
+
+            if (
+                option.obj._flag_needs_value
+                and isinstance(next_rarg, str)
+                and next_rarg[:1] in self._opt_prefixes
+                and len(next_rarg) > 1
+            ):
+                # The next arg looks like the start of an option, don't
+                # use it as the value if omitting the value is allowed.
+                value = _flag_needs_value
+            else:
+                value = state.rargs.pop(0)
+        else:
+            value = tuple(state.rargs[:nargs])
+            del state.rargs[:nargs]
+
+        return value
+
     def _process_opts(self, arg, state):
         explicit_value = None
         # Long option handling happens in two parts.  The first part is


Test patch:
diff --git a/tests/test_options.py b/tests/test_options.py
--- a/tests/test_options.py
+++ b/tests/test_options.py
@@ -658,3 +658,34 @@ def test_show_default_boolean_flag_value(runner):
     ctx = click.Context(click.Command("test"))
     message = opt.get_help_record(ctx)[1]
     assert "[default: False]" in message
+
+
+@pytest.mark.parametrize(
+    ("args", "expect"),
+    [
+        (None, (None, None, ())),
+        (["--opt"], ("flag", None, ())),
+        (["--opt", "-a", 42], ("flag", "42", ())),
+        (["--opt", "test", "-a", 42], ("test", "42", ())),
+        (["--opt=test", "-a", 42], ("test", "42", ())),
+        (["-o"], ("flag", None, ())),
+        (["-o", "-a", 42], ("flag", "42", ())),
+        (["-o", "test", "-a", 42], ("test", "42", ())),
+        (["-otest", "-a", 42], ("test", "42", ())),
+        (["a", "b", "c"], (None, None, ("a", "b", "c"))),
+        (["--opt", "a", "b", "c"], ("a", None, ("b", "c"))),
+        (["--opt", "test"], ("test", None, ())),
+        (["-otest", "a", "b", "c"], ("test", None, ("a", "b", "c"))),
+        (["--opt=test", "a", "b", "c"], ("test", None, ("a", "b", "c"))),
+    ],
+)
+def test_option_with_optional_value(runner, args, expect):
+    @click.command()
+    @click.option("-o", "--opt", is_flag=False, flag_value="flag")
+    @click.option("-a")
+    @click.argument("b", nargs=-1)
+    def cli(opt, a, b):
+        return opt, a, b
+
+    result = runner.invoke(cli, args, standalone_mode=False, catch_exceptions=False)
+    assert result.return_value == expect
diff --git a/tests/test_termui.py b/tests/test_termui.py
--- a/tests/test_termui.py
+++ b/tests/test_termui.py
@@ -385,3 +385,54 @@ def test_getchar_windows_exceptions(runner, monkeypatch, key_char, exc):
 def test_fast_edit(runner):
     result = click.edit("a\nb", editor="sed -i~ 's/$/Test/'")
     assert result == "aTest\nbTest\n"
+
+
+@pytest.mark.parametrize(
+    ("prompt_required", "required", "args", "expect"),
+    [
+        (True, False, None, "prompt"),
+        (True, False, ["-v"], "-v option requires an argument"),
+        (False, True, None, "prompt"),
+        (False, True, ["-v"], "prompt"),
+    ],
+)
+def test_prompt_required_with_required(runner, prompt_required, required, args, expect):
+    @click.command()
+    @click.option("-v", prompt=True, prompt_required=prompt_required, required=required)
+    def cli(v):
+        click.echo(str(v))
+
+    result = runner.invoke(cli, args, input="prompt")
+    assert expect in result.output
+
+
+@pytest.mark.parametrize(
+    ("args", "expect"),
+    [
+        # Flag not passed, don't prompt.
+        pytest.param(None, None, id="no flag"),
+        # Flag and value passed, don't prompt.
+        pytest.param(["-v", "value"], "value", id="short sep value"),
+        pytest.param(["--value", "value"], "value", id="long sep value"),
+        pytest.param(["-vvalue"], "value", id="short join value"),
+        pytest.param(["--value=value"], "value", id="long join value"),
+        # Flag without value passed, prompt.
+        pytest.param(["-v"], "prompt", id="short no value"),
+        pytest.param(["--value"], "prompt", id="long no value"),
+        # Don't use next option flag as value.
+        pytest.param(["-v", "-o", "42"], ("prompt", "42"), id="no value opt"),
+    ],
+)
+def test_prompt_required_false(runner, args, expect):
+    @click.command()
+    @click.option("-v", "--value", prompt=True, prompt_required=False)
+    @click.option("-o")
+    def cli(value, o):
+        if o is not None:
+            return value, o
+
+        return value
+
+    result = runner.invoke(cli, args=args, input="prompt", standalone_mode=False)
+    assert result.exception is None
+    assert result.return_value == expect