diff options
-rw-r--r-- | CHANGES | 5 | ||||
-rw-r--r-- | click/_bashcomplete.py | 19 | ||||
-rw-r--r-- | click/core.py | 11 | ||||
-rw-r--r-- | docs/bashcomplete.rst | 2 | ||||
-rw-r--r-- | tests/test_bashcomplete.py | 26 |
5 files changed, 55 insertions, 8 deletions
@@ -9,7 +9,7 @@ Version 7.0 (upcoming release with new features, release date to be decided) - Added support for bash completions containing spaces. See #773. - Added support for dynamic bash completion from a user-supplied callback. - See #755 + See #755. - Added support for bash completion of type=click.Choice for Options and Arguments. See #535. - The user is now presented with the available choices if prompt=True and @@ -23,6 +23,8 @@ Version 7.0 - ``launch`` now works properly under Cygwin. See #650. - `CliRunner.invoke` now may receive `args` as a string representing a Unix shell command. See #664. +- Fix bug that caused bashcompletion to give inproper completions on + chained commands. See #774. Version 6.8 ----------- @@ -33,6 +35,7 @@ Version 6.8 #728. - Fix bug in test runner when calling ``sys.exit`` with ``None``. See #739. - Fix crash on Windows console, see #744. +- Fix bashcompletion on chained commands. See #754. Version 6.7 ----------- diff --git a/click/_bashcomplete.py b/click/_bashcomplete.py index 7ea9683..536b5d7 100644 --- a/click/_bashcomplete.py +++ b/click/_bashcomplete.py @@ -43,14 +43,18 @@ def resolve_ctx(cli, prog_name, args): :return: the final context/command parsed """ ctx = cli.make_context(prog_name, args, resilient_parsing=True) - while ctx.protected_args + ctx.args and isinstance(ctx.command, MultiCommand): - a = ctx.protected_args + ctx.args - cmd = ctx.command.get_command(ctx, a[0]) + args_remaining = ctx.protected_args + ctx.args + while ctx is not None and args_remaining: + if isinstance(ctx.command, MultiCommand): + cmd = ctx.command.get_command(ctx, args_remaining[0]) if cmd is None: return None - ctx = cmd.make_context(a[0], a[1:], parent=ctx, resilient_parsing=True) - return ctx + ctx = cmd.make_context(args_remaining[0], args_remaining[1:], parent=ctx, resilient_parsing=True) + args_remaining = ctx.protected_args + ctx.args + else: + ctx = ctx.parent + return ctx def start_of_option(param_str): """ @@ -165,6 +169,11 @@ def get_choices(cli, prog_name, args, incomplete): # completion for any subcommands choices.extend(ctx.command.list_commands(ctx)) + if not start_of_option(incomplete) and ctx.parent is not None and isinstance(ctx.parent.command, MultiCommand) and ctx.parent.command.chain: + # completion for chained commands + remaining_comands = set(ctx.parent.command.list_commands(ctx.parent))-set(ctx.parent.protected_args) + choices.extend(remaining_comands) + for item in choices: if item.startswith(incomplete): yield item diff --git a/click/core.py b/click/core.py index 5e8d0ee..0741528 100644 --- a/click/core.py +++ b/click/core.py @@ -25,6 +25,15 @@ SUBCOMMAND_METAVAR = 'COMMAND [ARGS]...' SUBCOMMANDS_METAVAR = 'COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...' +def fast_exit(code): + """Exit without garbage collection, this speeds up exit by about 10ms for + things like bash completion. + """ + sys.stdout.flush() + sys.stderr.flush() + os._exit(code) + + def _bashcomplete(cmd, prog_name, complete_var=None): """Internal handler for the bash completion support.""" if complete_var is None: @@ -35,7 +44,7 @@ def _bashcomplete(cmd, prog_name, complete_var=None): from ._bashcomplete import bashcomplete if bashcomplete(cmd, prog_name, complete_var, complete_instr): - sys.exit(1) + fast_exit(1) def _check_multicommand(base_command, cmd_name, cmd, register=False): diff --git a/docs/bashcomplete.rst b/docs/bashcomplete.rst index 37d10e6..e2e2d49 100644 --- a/docs/bashcomplete.rst +++ b/docs/bashcomplete.rst @@ -46,7 +46,7 @@ Here is an example of using a callback function to generate dynamic suggestions: import os - def get_env_vars(ctx, incomplete, cwords, cword): + def get_env_vars(ctx, args, incomplete): return os.environ.keys() @click.command() diff --git a/tests/test_bashcomplete.py b/tests/test_bashcomplete.py index f8e91b4..268e046 100644 --- a/tests/test_bashcomplete.py +++ b/tests/test_bashcomplete.py @@ -112,6 +112,32 @@ def test_long_chain(): assert list(get_choices(cli, 'lol', ['asub', 'bsub', 'csub'], 'b')) == ['blue'] +def test_chaining(): + @click.group('cli', chain=True) + @click.option('--cli-opt') + def cli(cli_opt): + pass + + @cli.command('asub') + @click.option('--asub-opt') + def asub(asub_opt): + pass + + @cli.command('bsub') + @click.option('--bsub-opt') + @click.argument('arg', type=click.Choice(['arg1', 'arg2'])) + def bsub(bsub_opt, arg): + pass + + assert list(get_choices(cli, 'lol', [], '-')) == ['--cli-opt'] + assert list(get_choices(cli, 'lol', [], '')) == ['asub', 'bsub'] + assert list(get_choices(cli, 'lol', ['asub'], '-')) == ['--asub-opt'] + assert list(get_choices(cli, 'lol', ['asub'], '')) == ['bsub'] + assert list(get_choices(cli, 'lol', ['bsub'], '')) == ['arg1', 'arg2', 'asub'] + assert list(get_choices(cli, 'lol', ['asub', '--asub-opt', '5', 'bsub'], '-')) == ['--bsub-opt'] + assert list(get_choices(cli, 'lol', ['asub', 'bsub'], '-')) == ['--bsub-opt'] + + def test_argument_choice(): @click.command() @click.argument('arg1', required=False, type=click.Choice(['arg11', 'arg12'])) |