1# 2# Copyright (c) 2014 by Armin Ronacher. 3# Copyright (C) 2016 Codethink Limited 4# 5# This program is free software; you can redistribute it and/or 6# modify it under the terms of the GNU Lesser General Public 7# License as published by the Free Software Foundation; either 8# version 2 of the License, or (at your option) any later version. 9# 10# This library is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13# Lesser General Public License for more details. 14# 15# You should have received a copy of the GNU Lesser General Public 16# License along with this library. If not, see <http://www.gnu.org/licenses/>. 17# 18# This module was forked from the python click library, Included 19# original copyright notice from the Click library and following disclaimer 20# as per their LICENSE requirements. 21# 22# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33# 34import collections 35import copy 36import os 37 38import click 39from click.core import MultiCommand, Option, Argument 40from click.parser import split_arg_string 41 42WORDBREAK = '=' 43 44COMPLETION_SCRIPT = ''' 45%(complete_func)s() { 46 local IFS=$'\n' 47 COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\ 48 COMP_CWORD=$COMP_CWORD \\ 49 %(autocomplete_var)s=complete $1 ) ) 50 return 0 51} 52 53complete -F %(complete_func)s -o nospace %(script_names)s 54''' 55 56 57# An exception for our custom completion handler to 58# indicate that it does not want to handle completion 59# for this parameter 60# 61class CompleteUnhandled(Exception): 62 pass 63 64 65def complete_path(path_type, incomplete, base_directory='.'): 66 """Helper method for implementing the completions() method 67 for File and Path parameter types. 68 """ 69 70 # Try listing the files in the relative or absolute path 71 # specified in `incomplete` minus the last path component, 72 # otherwise list files starting from the current working directory. 73 entries = [] 74 base_path = '' 75 76 # This is getting a bit messy 77 listed_base_directory = False 78 79 if os.path.sep in incomplete: 80 split = incomplete.rsplit(os.path.sep, 1) 81 base_path = split[0] 82 83 # If there was nothing on the left of the last separator, 84 # we are completing files in the filesystem root 85 base_path = os.path.join(base_directory, base_path) 86 else: 87 incomplete_base_path = os.path.join(base_directory, incomplete) 88 if os.path.isdir(incomplete_base_path): 89 base_path = incomplete_base_path 90 91 try: 92 if base_path: 93 if os.path.isdir(base_path): 94 entries = [os.path.join(base_path, e) for e in os.listdir(base_path)] 95 else: 96 entries = os.listdir(base_directory) 97 listed_base_directory = True 98 except OSError: 99 # If for any reason the os reports an error from os.listdir(), just 100 # ignore this and avoid a stack trace 101 pass 102 103 base_directory_slash = base_directory 104 if not base_directory_slash.endswith(os.sep): 105 base_directory_slash += os.sep 106 base_directory_len = len(base_directory_slash) 107 108 def entry_is_dir(entry): 109 if listed_base_directory: 110 entry = os.path.join(base_directory, entry) 111 return os.path.isdir(entry) 112 113 def fix_path(path): 114 115 # Append slashes to any entries which are directories, or 116 # spaces for other files since they cannot be further completed 117 if entry_is_dir(path) and not path.endswith(os.sep): 118 path = path + os.sep 119 else: 120 path = path + " " 121 122 # Remove the artificial leading path portion which 123 # may have been prepended for search purposes. 124 if path.startswith(base_directory_slash): 125 path = path[base_directory_len:] 126 127 return path 128 129 return [ 130 # Return an appropriate path for each entry 131 fix_path(e) for e in sorted(entries) 132 133 # Filter out non directory elements when searching for a directory, 134 # the opposite is fine, however. 135 if not (path_type == 'Directory' and not entry_is_dir(e)) 136 ] 137 138 139# Instead of delegating completions to the param type, 140# hard code all of buildstream's completions here. 141# 142# This whole module should be removed in favor of more 143# generic code in click once this issue is resolved: 144# https://github.com/pallets/click/issues/780 145# 146def get_param_type_completion(param_type, incomplete): 147 148 if isinstance(param_type, click.Choice): 149 return [c + " " for c in param_type.choices] 150 elif isinstance(param_type, click.File): 151 return complete_path("File", incomplete) 152 elif isinstance(param_type, click.Path): 153 return complete_path(param_type.path_type, incomplete) 154 155 return [] 156 157 158def resolve_ctx(cli, prog_name, args): 159 """ 160 Parse into a hierarchy of contexts. Contexts are connected through the parent variable. 161 :param cli: command definition 162 :param prog_name: the program that is running 163 :param args: full list of args typed before the incomplete arg 164 :return: the final context/command parsed 165 """ 166 ctx = cli.make_context(prog_name, args, resilient_parsing=True) 167 args_remaining = ctx.protected_args + ctx.args 168 while ctx is not None and args_remaining: 169 if isinstance(ctx.command, MultiCommand): 170 cmd = ctx.command.get_command(ctx, args_remaining[0]) 171 if cmd is None: 172 return None 173 ctx = cmd.make_context(args_remaining[0], args_remaining[1:], parent=ctx, resilient_parsing=True) 174 args_remaining = ctx.protected_args + ctx.args 175 else: 176 ctx = ctx.parent 177 178 return ctx 179 180 181def start_of_option(param_str): 182 """ 183 :param param_str: param_str to check 184 :return: whether or not this is the start of an option declaration (i.e. starts "-" or "--") 185 """ 186 return param_str and param_str[:1] == '-' 187 188 189def is_incomplete_option(all_args, cmd_param): 190 """ 191 :param all_args: the full original list of args supplied 192 :param cmd_param: the current command paramter 193 :return: whether or not the last option declaration (i.e. starts "-" or "--") is incomplete and 194 corresponds to this cmd_param. In other words whether this cmd_param option can still accept 195 values 196 """ 197 if cmd_param.is_flag: 198 return False 199 last_option = None 200 for index, arg_str in enumerate(reversed([arg for arg in all_args if arg != WORDBREAK])): 201 if index + 1 > cmd_param.nargs: 202 break 203 if start_of_option(arg_str): 204 last_option = arg_str 205 206 return True if last_option and last_option in cmd_param.opts else False 207 208 209def is_incomplete_argument(current_params, cmd_param): 210 """ 211 :param current_params: the current params and values for this argument as already entered 212 :param cmd_param: the current command parameter 213 :return: whether or not the last argument is incomplete and corresponds to this cmd_param. In 214 other words whether or not the this cmd_param argument can still accept values 215 """ 216 current_param_values = current_params[cmd_param.name] 217 if current_param_values is None: 218 return True 219 if cmd_param.nargs == -1: 220 return True 221 if isinstance(current_param_values, collections.Iterable) \ 222 and cmd_param.nargs > 1 and len(current_param_values) < cmd_param.nargs: 223 return True 224 return False 225 226 227def get_user_autocompletions(args, incomplete, cmd, cmd_param, override): 228 """ 229 :param args: full list of args typed before the incomplete arg 230 :param incomplete: the incomplete text of the arg to autocomplete 231 :param cmd_param: command definition 232 :param override: a callable (cmd_param, args, incomplete) that will be 233 called to override default completion based on parameter type. Should raise 234 'CompleteUnhandled' if it could not find a completion. 235 :return: all the possible user-specified completions for the param 236 """ 237 238 # Use the type specific default completions unless it was overridden 239 try: 240 return override(cmd=cmd, 241 cmd_param=cmd_param, 242 args=args, 243 incomplete=incomplete) 244 except CompleteUnhandled: 245 return get_param_type_completion(cmd_param.type, incomplete) or [] 246 247 248def get_choices(cli, prog_name, args, incomplete, override): 249 """ 250 :param cli: command definition 251 :param prog_name: the program that is running 252 :param args: full list of args typed before the incomplete arg 253 :param incomplete: the incomplete text of the arg to autocomplete 254 :param override: a callable (cmd_param, args, incomplete) that will be 255 called to override default completion based on parameter type. Should raise 256 'CompleteUnhandled' if it could not find a completion. 257 :return: all the possible completions for the incomplete 258 """ 259 all_args = copy.deepcopy(args) 260 261 ctx = resolve_ctx(cli, prog_name, args) 262 if ctx is None: 263 return 264 265 # In newer versions of bash long opts with '='s are partitioned, but it's easier to parse 266 # without the '=' 267 if start_of_option(incomplete) and WORDBREAK in incomplete: 268 partition_incomplete = incomplete.partition(WORDBREAK) 269 all_args.append(partition_incomplete[0]) 270 incomplete = partition_incomplete[2] 271 elif incomplete == WORDBREAK: 272 incomplete = '' 273 274 choices = [] 275 found_param = False 276 if start_of_option(incomplete): 277 # completions for options 278 for param in ctx.command.params: 279 if isinstance(param, Option): 280 choices.extend([param_opt + " " for param_opt in param.opts + param.secondary_opts 281 if param_opt not in all_args or param.multiple]) 282 found_param = True 283 if not found_param: 284 # completion for option values by choices 285 for cmd_param in ctx.command.params: 286 if isinstance(cmd_param, Option) and is_incomplete_option(all_args, cmd_param): 287 choices.extend(get_user_autocompletions(all_args, incomplete, ctx.command, cmd_param, override)) 288 found_param = True 289 break 290 if not found_param: 291 # completion for argument values by choices 292 for cmd_param in ctx.command.params: 293 if isinstance(cmd_param, Argument) and is_incomplete_argument(ctx.params, cmd_param): 294 choices.extend(get_user_autocompletions(all_args, incomplete, ctx.command, cmd_param, override)) 295 found_param = True 296 break 297 298 if not found_param and isinstance(ctx.command, MultiCommand): 299 # completion for any subcommands 300 choices.extend([cmd + " " for cmd in ctx.command.list_commands(ctx)]) 301 302 if not start_of_option(incomplete) and ctx.parent is not None \ 303 and isinstance(ctx.parent.command, MultiCommand) and ctx.parent.command.chain: 304 # completion for chained commands 305 remaining_comands = set(ctx.parent.command.list_commands(ctx.parent)) - set(ctx.parent.protected_args) 306 choices.extend([cmd + " " for cmd in remaining_comands]) 307 308 for item in choices: 309 if item.startswith(incomplete): 310 yield item 311 312 313def do_complete(cli, prog_name, override): 314 cwords = split_arg_string(os.environ['COMP_WORDS']) 315 cword = int(os.environ['COMP_CWORD']) 316 args = cwords[1:cword] 317 try: 318 incomplete = cwords[cword] 319 except IndexError: 320 incomplete = '' 321 322 for item in get_choices(cli, prog_name, args, incomplete, override): 323 click.echo(item) 324 325 326# Main function called from main.py at startup here 327# 328def main_bashcomplete(cmd, prog_name, override): 329 """Internal handler for the bash completion support.""" 330 331 if '_BST_COMPLETION' in os.environ: 332 do_complete(cmd, prog_name, override) 333 return True 334 335 return False 336