diff --git a/click/_bashcomplete.py b/click/_bashcomplete.py index d9d26d28b..9d60fa2bb 100644 --- a/click/_bashcomplete.py +++ b/click/_bashcomplete.py @@ -40,18 +40,30 @@ def resolve_ctx(cli, prog_name, args): def get_choices(cli, prog_name, args, incomplete): + # resolve_ctx will change args in place + # need to grab the last completed one here and now + last_complete = [a for a in (args if args else ['']) if a != '='][-1] + if incomplete == '=': + incomplete = '' + ctx = resolve_ctx(cli, prog_name, args) if ctx is None: return choices = [] + + if last_complete: + for param in ctx.command.params: + if last_complete in param.opts: + choices.extend(param.get_completion()) + break if incomplete and not incomplete[:1].isalnum(): for param in ctx.command.params: if not isinstance(param, Option): continue choices.extend(param.opts) choices.extend(param.secondary_opts) - elif isinstance(ctx.command, MultiCommand): + if isinstance(ctx.command, MultiCommand): choices.extend(ctx.command.list_commands(ctx)) for item in choices: diff --git a/click/core.py b/click/core.py index cb43d537f..b87fa31de 100644 --- a/click/core.py +++ b/click/core.py @@ -1260,7 +1260,8 @@ class Parameter(object): def __init__(self, param_decls=None, type=None, required=False, default=None, callback=None, nargs=None, metavar=None, - expose_value=True, is_eager=False, envvar=None): + expose_value=True, is_eager=False, envvar=None, + completer=None, choices=None): self.name, self.opts, self.secondary_opts = \ self._parse_decls(param_decls or (), expose_value) @@ -1283,6 +1284,8 @@ def __init__(self, param_decls=None, type=None, required=False, self.is_eager = is_eager self.metavar = metavar self.envvar = envvar + self.completer = completer + self.choices = choices @property def human_readable_name(self): @@ -1415,6 +1418,15 @@ def get_help_record(self, ctx): def get_usage_pieces(self, ctx): return [] + def get_completion(self): + if self.completer: + for thing in self.completer(): + yield str(thing) + if self.choices: + for thing in self.choices: + yield str(thing) + + class Option(Parameter): """Options are usually optional values on the command line and diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_bashcomplete.py b/tests/test_bashcomplete.py index fea096c10..01ca084ef 100644 --- a/tests/test_bashcomplete.py +++ b/tests/test_bashcomplete.py @@ -60,3 +60,31 @@ def csub(csub_opt): assert list(get_choices(cli, 'lol', ['asub', 'bsub'], '')) == ['csub'] assert list(get_choices(cli, 'lol', ['asub', 'bsub', 'csub'], '-')) == ['--csub-opt'] assert list(get_choices(cli, 'lol', ['asub', 'bsub', 'csub'], '')) == [] + + +def test_completer(): + + def custom_completer(): + yield 'custom' + for i in [0, 1, 2]: + yield i + yield 'bye' + + @click.command() + @click.option('--local-opt', completer=custom_completer) + def cli(local_opt): + pass + + assert list(get_choices(cli, 'lol', [], '-')) == ['--local-opt'] + assert list(get_choices(cli, 'lol', ['--local-opt'], '')) == ['custom', '0', '1', '2', 'bye'] + + +def test_choices(): + + @click.command() + @click.option('--local-opt', choices=['foo', 'bar', 'spam', 'eggs']) + def cli(local_opt): + pass + + assert list(get_choices(cli, 'lol', [], '-')) == ['--local-opt'] + assert list(get_choices(cli, 'lol', ['--local-opt'], '')) == ['foo', 'bar', 'spam', 'eggs']