summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarkus Unterwaditzer <markus@unterwaditzer.net>2017-04-16 12:43:52 +0200
committerGitHub <noreply@github.com>2017-04-16 12:43:52 +0200
commit5f2ff4faf627dc9d0925cffb7d9b8f3162fbdaad (patch)
treedfefda64336c6a761bd14ce7dd1014fd24f4e430
parent430c50a3b6dca4e23936389513d2099b0af865cf (diff)
parent32fb787bbd4acf84983b5c49f87cfc76c7699d44 (diff)
downloadclick-5f2ff4faf627dc9d0925cffb7d9b8f3162fbdaad.tar.gz
Merge pull request #755 from stopthatcow/feature/bash_autocompletion_6.6
Dynamic bash autocompletion
-rw-r--r--CHANGES5
-rw-r--r--click/_bashcomplete.py23
-rw-r--r--click/core.py4
-rw-r--r--docs/bashcomplete.rst30
-rw-r--r--examples/bashcompletion/README12
-rw-r--r--examples/bashcompletion/bashcompletion.py31
-rw-r--r--examples/bashcompletion/setup.py15
-rw-r--r--tests/test_bashcomplete.py22
8 files changed, 127 insertions, 15 deletions
diff --git a/CHANGES b/CHANGES
index c24e42e..f06bb31 100644
--- a/CHANGES
+++ b/CHANGES
@@ -7,12 +7,13 @@ Version 7.0
-----------
(upcoming release with new features, release date to be decided)
-
+- Added support for dynamic bash completion from a user-supplied callback.
+ 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
type=click.Choice in a click.option. The choices are listed within
- parentthesis like 'Choose fruit (apple, orange): '.
+ parenthesis like 'Choose fruit (apple, orange): '.
- The exception objects now store unicode properly.
- Added the ability to hide commands and options from help.
- Added Float Range in Types.
diff --git a/click/_bashcomplete.py b/click/_bashcomplete.py
index bd1c278..10a9dc2 100644
--- a/click/_bashcomplete.py
+++ b/click/_bashcomplete.py
@@ -8,7 +8,6 @@ from .parser import split_arg_string
from .core import MultiCommand, Option, Argument
from .types import Choice
-
WORDBREAK = '='
COMPLETION_SCRIPT = '''
@@ -97,6 +96,22 @@ def is_incomplete_argument(current_params, cmd_param):
return True
return False
+def get_user_autocompletions(ctx, args, incomplete, cmd_param):
+ """
+ :param ctx: context associated with the parsed command
+ :param args: full list of args
+ :param incomplete: the incomplete text to autocomplete
+ :param cmd_param: command definition
+ :return: all the possible user-specified completions for the param
+ """
+ if isinstance(cmd_param.type, Choice):
+ return cmd_param.type.choices
+ elif cmd_param.autocompletion is not None:
+ return cmd_param.autocompletion(ctx=ctx,
+ args=args,
+ incomplete=incomplete)
+ else:
+ return []
def get_choices(cli, prog_name, args, incomplete):
"""
@@ -134,16 +149,14 @@ def get_choices(cli, prog_name, args, incomplete):
# completion for option values by choices
for cmd_param in ctx.command.params:
if isinstance(cmd_param, Option) and is_incomplete_option(all_args, cmd_param):
- if isinstance(cmd_param.type, Choice):
- choices.extend(cmd_param.type.choices)
+ choices.extend(get_user_autocompletions(ctx, all_args, incomplete, cmd_param))
found_param = True
break
if not found_param:
# completion for argument values by choices
for cmd_param in ctx.command.params:
if isinstance(cmd_param, Argument) and is_incomplete_argument(ctx.params, cmd_param):
- if isinstance(cmd_param.type, Choice):
- choices.extend(cmd_param.type.choices)
+ choices.extend(get_user_autocompletions(ctx, all_args, incomplete, cmd_param))
found_param = True
break
diff --git a/click/core.py b/click/core.py
index 18c1766..5e8d0ee 100644
--- a/click/core.py
+++ b/click/core.py
@@ -1266,7 +1266,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,
+ autocompletion=None):
self.name, self.opts, self.secondary_opts = \
self._parse_decls(param_decls or (), expose_value)
@@ -1289,6 +1290,7 @@ class Parameter(object):
self.is_eager = is_eager
self.metavar = metavar
self.envvar = envvar
+ self.autocompletion = autocompletion
@property
def human_readable_name(self):
diff --git a/docs/bashcomplete.rst b/docs/bashcomplete.rst
index 9dd9a99..37d10e6 100644
--- a/docs/bashcomplete.rst
+++ b/docs/bashcomplete.rst
@@ -16,9 +16,6 @@ how to do that, see :ref:`setuptools-integration`. Also, Click currently
only supports completion for Bash. Zsh support is available through Zsh's
bash completion compatibility mode.
-Currently, Bash completion is an internal feature that is not customizable.
-This might be relaxed in future versions.
-
What it Completes
-----------------
@@ -32,6 +29,33 @@ least a dash has been provided. Example::
$ repo clone -<TAB><TAB>
--deep --help --rev --shallow -r
+Additionally, custom suggestions can be provided for arguments and options with
+the ``autocompletion`` parameter. ``autocompletion`` should a callback function
+that returns a list of strings. This is useful when the suggestions need to be
+dynamically generated at bash completion time. The callback function will be
+passed 3 keyword arguments:
+
+- ``ctx`` - The current click context.
+- ``args`` - The list of arguments passed in.
+- ``incomplete`` - The partial word that is being completed, as a string. May
+ be an empty string ``''`` if no characters have been entered yet.
+
+Here is an example of using a callback function to generate dynamic suggestions:
+
+.. click:example::
+
+ import os
+
+ def get_env_vars(ctx, incomplete, cwords, cword):
+ return os.environ.keys()
+
+ @click.command()
+ @click.argument("envvar", type=click.STRING, autocompletion=get_env_vars)
+ def cmd1(envvar):
+ click.echo('Environment variable: %s' % envvar)
+ click.echo('Value: %s' % os.environ[envvar])
+
+
Activation
----------
diff --git a/examples/bashcompletion/README b/examples/bashcompletion/README
new file mode 100644
index 0000000..f8a0d51
--- /dev/null
+++ b/examples/bashcompletion/README
@@ -0,0 +1,12 @@
+$ bashcompletion
+
+ bashcompletion is a simple example of an application that
+ tries to autocomplete commands, arguments and options.
+
+ This example requires Click 2.0 or higher.
+
+Usage:
+
+ $ pip install --editable .
+ $ eval "$(_BASHCOMPLETION_COMPLETE=source bashcompletion)"
+ $ bashcompletion --help
diff --git a/examples/bashcompletion/bashcompletion.py b/examples/bashcompletion/bashcompletion.py
new file mode 100644
index 0000000..8aaf174
--- /dev/null
+++ b/examples/bashcompletion/bashcompletion.py
@@ -0,0 +1,31 @@
+import click
+import os
+
+@click.group()
+def cli():
+ pass
+
+def get_env_vars(ctx, args, incomplete):
+ return os.environ.keys()
+
+@cli.command()
+@click.argument("envvar", type=click.STRING, autocompletion=get_env_vars)
+def cmd1(envvar):
+ click.echo('Environment variable: %s' % envvar)
+ click.echo('Value: %s' % os.environ[envvar])
+
+@click.group()
+def group():
+ pass
+
+def list_users(ctx, args, incomplete):
+ # Here you can generate completions dynamically
+ users = ['bob', 'alice']
+ return users
+
+@group.command()
+@click.argument("user", type=click.STRING, autocompletion=list_users)
+def subcmd(user):
+ click.echo('Chosen user is %s' % user)
+
+cli.add_command(group)
diff --git a/examples/bashcompletion/setup.py b/examples/bashcompletion/setup.py
new file mode 100644
index 0000000..ad20081
--- /dev/null
+++ b/examples/bashcompletion/setup.py
@@ -0,0 +1,15 @@
+from setuptools import setup
+
+setup(
+ name='click-example-bashcompletion',
+ version='1.0',
+ py_modules=['bashcompletion'],
+ include_package_data=True,
+ install_requires=[
+ 'click',
+ ],
+ entry_points='''
+ [console_scripts]
+ bashcompletion=bashcompletion:cli
+ ''',
+)
diff --git a/tests/test_bashcomplete.py b/tests/test_bashcomplete.py
index 42d2255..f8e91b4 100644
--- a/tests/test_bashcomplete.py
+++ b/tests/test_bashcomplete.py
@@ -83,9 +83,18 @@ def test_long_chain():
def bsub(bsub_opt):
pass
+ COLORS = ['red', 'green', 'blue']
+ def get_colors(ctx, args, incomplete):
+ for c in COLORS:
+ yield c
+
+ CSUB_OPT_CHOICES = ['foo', 'bar']
+ CSUB_CHOICES = ['bar', 'baz']
@bsub.command('csub')
- @click.option('--csub-opt')
- def csub(csub_opt):
+ @click.option('--csub-opt', type=click.Choice(CSUB_OPT_CHOICES))
+ @click.option('--csub', type=click.Choice(CSUB_CHOICES))
+ @click.argument('color', autocompletion=get_colors)
+ def csub(csub_opt, color):
pass
assert list(get_choices(cli, 'lol', [], '-')) == ['--cli-opt']
@@ -94,8 +103,13 @@ def test_long_chain():
assert list(get_choices(cli, 'lol', ['asub'], '')) == ['bsub']
assert list(get_choices(cli, 'lol', ['asub', 'bsub'], '-')) == ['--bsub-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'], '')) == []
+ assert list(get_choices(cli, 'lol', ['asub', 'bsub', 'csub'], '-')) == ['--csub-opt', '--csub']
+ assert list(get_choices(cli, 'lol', ['asub', 'bsub', 'csub', '--csub-opt'], '')) == CSUB_OPT_CHOICES
+ assert list(get_choices(cli, 'lol', ['asub', 'bsub', 'csub'], '--csub')) == ['--csub-opt', '--csub']
+ assert list(get_choices(cli, 'lol', ['asub', 'bsub', 'csub', '--csub'], '')) == CSUB_CHOICES
+ assert list(get_choices(cli, 'lol', ['asub', 'bsub', 'csub', '--csub-opt'], 'f')) == ['foo']
+ assert list(get_choices(cli, 'lol', ['asub', 'bsub', 'csub'], '')) == COLORS
+ assert list(get_choices(cli, 'lol', ['asub', 'bsub', 'csub'], 'b')) == ['blue']
def test_argument_choice():