Repository: pallets/click

Issue:
New upcoming hidden option in Click.Option is not hidden from bash completion
Thanks for wonderful package.

I don't know whether this is intended behavior or not but I was just trying out new upcoming release 7.0 for hidden option and autocompletion to Click.Option and hidden option hides option from help message but it shows up in bash completion.
However it will be good if we can hide it from bash completion also, so that hidden option is actually hidden from everywhere.


Discussion:
@stopthatcow 
Should be a simple fix. I will take a look this evening.
So there is a choice to be made here about behavior. If a param is hidden I agree that tab completions shouldn't reveal the presence of this parameter if it is partially typed in but once a user types it in fully should click allow autocompletion of the parameter's possible values? I lean toward yes, because once a user has typed in the parameter they wish to use it and it should no longer act as "hidden". The same goes for commands and sub commands that are hidden. If a command group is marked as hidden it should not show up un completion results when partially typed in but once typed in all commands under it should complete as normal. I have a prototyped branch working with this behavior, will finish it up tomorrow, rebase and submit a PR.
Yes, only stop completion of the token itself.

Resolving patch:
diff --git a/click/_bashcomplete.py b/click/_bashcomplete.py
--- a/click/_bashcomplete.py
+++ b/click/_bashcomplete.py
@@ -23,7 +23,8 @@
 %(complete_func)setup() {
     local COMPLETION_OPTIONS=""
     local BASH_VERSION_ARR=(${BASH_VERSION//./ })
-    if [ ${BASH_VERSION_ARR[0]} -ge 4 ] && [ ${BASH_VERSION_ARR[1]} -ge 4 ];then
+    # Only BASH version 4.4 and later have the nosort option.
+    if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] && [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then
         COMPLETION_OPTIONS="-o nosort"
     fi
 
@@ -176,7 +177,7 @@ def get_user_autocompletions(ctx, args, incomplete, cmd_param):
     if isinstance(cmd_param.type, Choice):
         # Choices don't support descriptions.
         results = [(c, None)
-                   for c in cmd_param.type.choices if c.startswith(incomplete)]
+                   for c in cmd_param.type.choices if str(c).startswith(incomplete)]
     elif cmd_param.autocompletion is not None:
         dynamic_completions = cmd_param.autocompletion(ctx=ctx,
                                                        args=args,
@@ -186,20 +187,32 @@ def get_user_autocompletions(ctx, args, incomplete, cmd_param):
     return results
 
 
+def get_visible_commands_starting_with(ctx, starts_with):
+    """
+    :param ctx: context associated with the parsed command
+    :starts_with: string that visible commands must start with.
+    :return: all visible (not hidden) commands that start with starts_with.
+    """
+    for c in ctx.command.list_commands(ctx):
+        if c.startswith(starts_with):
+            command = ctx.command.get_command(ctx, c)
+            if not command.hidden:
+                yield command
+
+
 def add_subcommand_completions(ctx, incomplete, completions_out):
     # Add subcommand completions.
     if isinstance(ctx.command, MultiCommand):
         completions_out.extend(
-            [(c, ctx.command.get_command(ctx, c).get_short_help_str()) for c in ctx.command.list_commands(ctx) if c.startswith(incomplete)])
+            [(c.name, c.get_short_help_str()) for c in get_visible_commands_starting_with(ctx, incomplete)])
 
     # Walk up the context list and add any other completion possibilities from chained commands
     while ctx.parent is not None:
         ctx = ctx.parent
         if isinstance(ctx.command, MultiCommand) and ctx.command.chain:
-            remaining_commands = sorted(
-                set(ctx.command.list_commands(ctx)) - set(ctx.protected_args))
-            completions_out.extend(
-                [(c, ctx.command.get_command(ctx, c).get_short_help_str()) for c in remaining_commands if c.startswith(incomplete)])
+            remaining_commands = [c for c in get_visible_commands_starting_with(ctx, incomplete)
+                                  if c.name not in ctx.protected_args]
+            completions_out.extend([(c.name, c.get_short_help_str()) for c in remaining_commands])
 
 
 def get_choices(cli, prog_name, args, incomplete):
@@ -229,11 +242,10 @@ def get_choices(cli, prog_name, args, incomplete):
     if start_of_option(incomplete):
         # completions for partial options
         for param in ctx.command.params:
-            if isinstance(param, Option):
+            if isinstance(param, Option) and not param.hidden:
                 param_opts = [param_opt for param_opt in param.opts +
                               param.secondary_opts if param_opt not in all_args or param.multiple]
-                completions.extend(
-                    [(o, param.help) for o in param_opts if o.startswith(incomplete)])
+                completions.extend([(o, param.help) for o in param_opts if o.startswith(incomplete)])
         return completions
     # completion for option values from user supplied values
     for param in ctx.command.params:


Test patch:
diff --git a/tests/test_bashcomplete.py b/tests/test_bashcomplete.py
--- a/tests/test_bashcomplete.py
+++ b/tests/test_bashcomplete.py
@@ -371,3 +371,39 @@ def esub():
     assert choices_without_help(cli, ['sub'], 'c') == ['csub']
     assert choices_without_help(cli, ['sub', 'csub'], '') == ['dsub', 'esub']
     assert choices_without_help(cli, ['sub', 'csub', 'dsub'], '') == ['esub']
+
+
+def test_hidden():
+    @click.group()
+    @click.option('--name', hidden=True)
+    @click.option('--choices', type=click.Choice([1, 2]), hidden=True)
+    def cli(name):
+        pass
+
+    @cli.group(hidden=True)
+    def hgroup():
+        pass
+
+    @hgroup.group()
+    def hgroupsub():
+        pass
+
+    @cli.command()
+    def asub():
+        pass
+
+    @cli.command(hidden=True)
+    @click.option('--hname')
+    def hsub():
+        pass
+
+    assert choices_without_help(cli, [], '--n') == []
+    assert choices_without_help(cli, [], '--c') == []
+    # If the user exactly types out the hidden param, complete its options.
+    assert choices_without_help(cli, ['--choices'], '') == [1, 2]
+    assert choices_without_help(cli, [], '') == ['asub']
+    assert choices_without_help(cli, [], '') == ['asub']
+    assert choices_without_help(cli, [], 'h') == []
+    # If the user exactly types out the hidden command, complete its subcommands.
+    assert choices_without_help(cli, ['hgroup'], '') == ['hgroupsub']
+    assert choices_without_help(cli, ['hsub'], '--h') == ['--hname']