Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File and path completion again #1403

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 23 additions & 17 deletions click/_bashcomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from .utils import echo
from .parser import split_arg_string
from .core import MultiCommand, Option, Argument
from .types import Choice

try:
from collections import abc
Expand All @@ -24,18 +23,19 @@
return 0
}

%(complete_func)setup() {
%(complete_func)s_setup() {
local COMPLETION_OPTIONS=""
local BASH_VERSION_ARR=(${BASH_VERSION//./ })
# 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"
COMPLETION_OPTIONS="-o nosort "
fi
COMPLETION_OPTIONS=$COMPLETION_OPTIONS"-o nospace"

complete $COMPLETION_OPTIONS -F %(complete_func)s %(script_names)s
}

%(complete_func)setup
%(complete_func)s_setup
'''

COMPLETION_SCRIPT_ZSH = '''
Expand Down Expand Up @@ -182,16 +182,16 @@ def get_user_autocompletions(ctx, args, incomplete, cmd_param):
:return: all the possible user-specified completions for the param
"""
results = []
if isinstance(cmd_param.type, Choice):
# Choices don't support descriptions.
results = [(c, None)
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,
incomplete=incomplete)
results = [c if isinstance(c, tuple) else (c, None)
for c in dynamic_completions]
if cmd_param.autocompletion is not None:
completions = cmd_param.autocompletion(
ctx=ctx,
args=args,
incomplete=incomplete
)
else:
completions = cmd_param.type.completions(incomplete)

results = [c if isinstance(c, tuple) else (c, None) for c in completions]
return results


Expand All @@ -212,15 +212,21 @@ def add_subcommand_completions(ctx, incomplete, completions_out):
# Add subcommand completions.
if isinstance(ctx.command, MultiCommand):
completions_out.extend(
[(c.name, c.get_short_help_str()) for c in get_visible_commands_starting_with(ctx, 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 = [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])
completions_out.extend(
[(c.name + " ", c.get_short_help_str()) for c in remaining_commands]
)


def get_choices(cli, prog_name, args, incomplete):
Expand Down Expand Up @@ -251,7 +257,7 @@ def get_choices(cli, prog_name, args, incomplete):
# completions for partial options
for param in ctx.command.params:
if isinstance(param, Option) and not param.hidden:
param_opts = [param_opt for param_opt in param.opts +
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)])
return completions
Expand Down
67 changes: 67 additions & 0 deletions click/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ def fail(self, message, param=None, ctx=None):
"""Helper method to fail with an invalid value message."""
raise BadParameter(message, ctx=ctx, param=param)

def completions(self, incomplete):
"""Report possible completions for this parameter.

This takes the currently incomplete word for this parameter,
which may be an empty string if no characters have been entered yet.

This method should return a list of words that are suitable completions
for the parameter.

Suggested completions which cannot be completed further, should
be completed with a space appended. This allows support for
partial path like completions which may be searched in a database
or filesystem.
"""
return []


class CompositeParamType(ParamType):
is_composite = True
Expand Down Expand Up @@ -179,6 +195,9 @@ def convert(self, value, param, ctx):
self.fail('invalid choice: %s. (choose from %s)' %
(value, ', '.join(self.choices)), param, ctx)

def completions(self, incomplete):
return [c + " " for c in self.choices if c.startswith(incomplete)]

def __repr__(self):
return 'Choice(%r)' % list(self.choices)

Expand Down Expand Up @@ -354,6 +373,9 @@ def convert(self, value, param, ctx):
return False
self.fail('%s is not a valid boolean' % value, param, ctx)

def completions(self, incomplete):
return ["yes ", "no "]

def __repr__(self):
return 'BOOL'

Expand All @@ -374,6 +396,45 @@ def __repr__(self):
return 'UUID'


def _complete_path(path_type, incomplete):
"""Helper method for implementing the completions() method
for File and Path parameter types.
"""
# Resolve shell variables and user/home shortcuts.
incomplete = os.path.expandvars(os.path.expanduser(incomplete))

# Try listing the files in the relative or absolute path
# specified in `incomplete` minus the last path component,
# otherwise list files starting from the current working directory.
dirname = os.path.dirname(incomplete)
base_path = dirname or '.'

entries = []
try:
entries = [os.path.join(dirname, e) for e in os.listdir(base_path)]
except OSError:
# If for any reason the os reports an error from os.listdir(), just
# ignore this and avoid a stack trace
pass

# Only keep completions that match the provided partial completion.
entries = [entry for entry in entries if entry.startswith(incomplete)]

# If we're searching for a directory, keep only directories.
entries = [
entry for entry in entries
if path_type != "Directory" or os.path.isdir(entry)
]

# Append a slash for directories, or a space for files.
entries = [
entry + (os.path.sep if os.path.isdir(entry) else " ")
for entry in entries
]

return sorted(entries)


class File(ParamType):
"""Declares a parameter to be a file for reading or writing. The file
is automatically closed once the context tears down (after the command
Expand Down Expand Up @@ -454,6 +515,9 @@ def convert(self, value, param, ctx):
get_streerror(e),
), param, ctx)

def completions(self, incomplete):
return _complete_path("File", incomplete)


class Path(ParamType):
"""The path type is similar to the :class:`File` type but it performs
Expand Down Expand Up @@ -559,6 +623,9 @@ def convert(self, value, param, ctx):

return self.coerce_path_result(rv)

def completions(self, incomplete):
return _complete_path(self.path_type, incomplete)


class Tuple(CompositeParamType):
"""The default behavior of Click is to apply a type on a value directly.
Expand Down
9 changes: 6 additions & 3 deletions docs/bashcomplete.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,19 @@ passed 3 keyword arguments:
- ``incomplete`` - The partial word that is being completed, as a string. May
be an empty string ``''`` if no characters have been entered yet.

The returned strings should have spaces appended to them in the case that
they cannot be further completed, omitting the space allows the completion
to be invoked multiple times, which can be useful for completing path-like
strings by searching a database or filesystem.

Here is an example of using a callback function to generate dynamic suggestions:

.. click:example::

import os

def get_env_vars(ctx, args, incomplete):
return [k for k in os.environ.keys() if incomplete in k]
return [k + ' ' for k in os.environ.keys() if incomplete in k]

@click.command()
@click.argument("envvar", type=click.STRING, autocompletion=get_env_vars)
Expand Down Expand Up @@ -127,5 +132,3 @@ For zsh:
And then you would put this into your .bashrc or .zshrc instead::

. /path/to/foo-bar-complete.sh


8 changes: 6 additions & 2 deletions examples/bashcompletion/bashcompletion.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def get_env_vars(ctx, args, incomplete):
# Completions returned as strings do not have a description displayed.
for key in os.environ.keys():
if incomplete in key:
yield key
yield key + ' '


@cli.command(help='A command to print environment variables')
Expand All @@ -33,7 +33,11 @@ def list_users(ctx, args, incomplete):
('alice', 'baker'),
('jerry', 'candlestick maker')]
# Ths will allow completion matches based on matches within the description string too!
return [user for user in users if incomplete in user[0] or incomplete in user[1]]
return [
user + ' '
for user in users
if incomplete in user[0] or incomplete in user[1]
]


@group.command(help='Choose a user')
Expand Down
Loading