_bashcomplete.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. import copy
  2. import os
  3. import re
  4. from .core import Argument
  5. from .core import MultiCommand
  6. from .core import Option
  7. from .parser import split_arg_string
  8. from .types import Choice
  9. from .utils import echo
  10. try:
  11. from collections import abc
  12. except ImportError:
  13. import collections as abc
  14. WORDBREAK = "="
  15. # Note, only BASH version 4.4 and later have the nosort option.
  16. COMPLETION_SCRIPT_BASH = """
  17. %(complete_func)s() {
  18. local IFS=$'\n'
  19. COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\
  20. COMP_CWORD=$COMP_CWORD \\
  21. %(autocomplete_var)s=complete $1 ) )
  22. return 0
  23. }
  24. %(complete_func)setup() {
  25. local COMPLETION_OPTIONS=""
  26. local BASH_VERSION_ARR=(${BASH_VERSION//./ })
  27. # Only BASH version 4.4 and later have the nosort option.
  28. if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] \
  29. && [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then
  30. COMPLETION_OPTIONS="-o nosort"
  31. fi
  32. complete $COMPLETION_OPTIONS -F %(complete_func)s %(script_names)s
  33. }
  34. %(complete_func)setup
  35. """
  36. COMPLETION_SCRIPT_ZSH = """
  37. #compdef %(script_names)s
  38. %(complete_func)s() {
  39. local -a completions
  40. local -a completions_with_descriptions
  41. local -a response
  42. (( ! $+commands[%(script_names)s] )) && return 1
  43. response=("${(@f)$( env COMP_WORDS=\"${words[*]}\" \\
  44. COMP_CWORD=$((CURRENT-1)) \\
  45. %(autocomplete_var)s=\"complete_zsh\" \\
  46. %(script_names)s )}")
  47. for key descr in ${(kv)response}; do
  48. if [[ "$descr" == "_" ]]; then
  49. completions+=("$key")
  50. else
  51. completions_with_descriptions+=("$key":"$descr")
  52. fi
  53. done
  54. if [ -n "$completions_with_descriptions" ]; then
  55. _describe -V unsorted completions_with_descriptions -U
  56. fi
  57. if [ -n "$completions" ]; then
  58. compadd -U -V unsorted -a completions
  59. fi
  60. compstate[insert]="automenu"
  61. }
  62. compdef %(complete_func)s %(script_names)s
  63. """
  64. COMPLETION_SCRIPT_FISH = (
  65. "complete --no-files --command %(script_names)s --arguments"
  66. ' "(env %(autocomplete_var)s=complete_fish'
  67. " COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t)"
  68. ' %(script_names)s)"'
  69. )
  70. _completion_scripts = {
  71. "bash": COMPLETION_SCRIPT_BASH,
  72. "zsh": COMPLETION_SCRIPT_ZSH,
  73. "fish": COMPLETION_SCRIPT_FISH,
  74. }
  75. _invalid_ident_char_re = re.compile(r"[^a-zA-Z0-9_]")
  76. def get_completion_script(prog_name, complete_var, shell):
  77. cf_name = _invalid_ident_char_re.sub("", prog_name.replace("-", "_"))
  78. script = _completion_scripts.get(shell, COMPLETION_SCRIPT_BASH)
  79. return (
  80. script
  81. % {
  82. "complete_func": "_{}_completion".format(cf_name),
  83. "script_names": prog_name,
  84. "autocomplete_var": complete_var,
  85. }
  86. ).strip() + ";"
  87. def resolve_ctx(cli, prog_name, args):
  88. """Parse into a hierarchy of contexts. Contexts are connected
  89. through the parent variable.
  90. :param cli: command definition
  91. :param prog_name: the program that is running
  92. :param args: full list of args
  93. :return: the final context/command parsed
  94. """
  95. ctx = cli.make_context(prog_name, args, resilient_parsing=True)
  96. args = ctx.protected_args + ctx.args
  97. while args:
  98. if isinstance(ctx.command, MultiCommand):
  99. if not ctx.command.chain:
  100. cmd_name, cmd, args = ctx.command.resolve_command(ctx, args)
  101. if cmd is None:
  102. return ctx
  103. ctx = cmd.make_context(
  104. cmd_name, args, parent=ctx, resilient_parsing=True
  105. )
  106. args = ctx.protected_args + ctx.args
  107. else:
  108. # Walk chained subcommand contexts saving the last one.
  109. while args:
  110. cmd_name, cmd, args = ctx.command.resolve_command(ctx, args)
  111. if cmd is None:
  112. return ctx
  113. sub_ctx = cmd.make_context(
  114. cmd_name,
  115. args,
  116. parent=ctx,
  117. allow_extra_args=True,
  118. allow_interspersed_args=False,
  119. resilient_parsing=True,
  120. )
  121. args = sub_ctx.args
  122. ctx = sub_ctx
  123. args = sub_ctx.protected_args + sub_ctx.args
  124. else:
  125. break
  126. return ctx
  127. def start_of_option(param_str):
  128. """
  129. :param param_str: param_str to check
  130. :return: whether or not this is the start of an option declaration
  131. (i.e. starts "-" or "--")
  132. """
  133. return param_str and param_str[:1] == "-"
  134. def is_incomplete_option(all_args, cmd_param):
  135. """
  136. :param all_args: the full original list of args supplied
  137. :param cmd_param: the current command paramter
  138. :return: whether or not the last option declaration (i.e. starts
  139. "-" or "--") is incomplete and corresponds to this cmd_param. In
  140. other words whether this cmd_param option can still accept
  141. values
  142. """
  143. if not isinstance(cmd_param, Option):
  144. return False
  145. if cmd_param.is_flag:
  146. return False
  147. last_option = None
  148. for index, arg_str in enumerate(
  149. reversed([arg for arg in all_args if arg != WORDBREAK])
  150. ):
  151. if index + 1 > cmd_param.nargs:
  152. break
  153. if start_of_option(arg_str):
  154. last_option = arg_str
  155. return True if last_option and last_option in cmd_param.opts else False
  156. def is_incomplete_argument(current_params, cmd_param):
  157. """
  158. :param current_params: the current params and values for this
  159. argument as already entered
  160. :param cmd_param: the current command parameter
  161. :return: whether or not the last argument is incomplete and
  162. corresponds to this cmd_param. In other words whether or not the
  163. this cmd_param argument can still accept values
  164. """
  165. if not isinstance(cmd_param, Argument):
  166. return False
  167. current_param_values = current_params[cmd_param.name]
  168. if current_param_values is None:
  169. return True
  170. if cmd_param.nargs == -1:
  171. return True
  172. if (
  173. isinstance(current_param_values, abc.Iterable)
  174. and cmd_param.nargs > 1
  175. and len(current_param_values) < cmd_param.nargs
  176. ):
  177. return True
  178. return False
  179. def get_user_autocompletions(ctx, args, incomplete, cmd_param):
  180. """
  181. :param ctx: context associated with the parsed command
  182. :param args: full list of args
  183. :param incomplete: the incomplete text to autocomplete
  184. :param cmd_param: command definition
  185. :return: all the possible user-specified completions for the param
  186. """
  187. results = []
  188. if isinstance(cmd_param.type, Choice):
  189. # Choices don't support descriptions.
  190. results = [
  191. (c, None) for c in cmd_param.type.choices if str(c).startswith(incomplete)
  192. ]
  193. elif cmd_param.autocompletion is not None:
  194. dynamic_completions = cmd_param.autocompletion(
  195. ctx=ctx, args=args, incomplete=incomplete
  196. )
  197. results = [
  198. c if isinstance(c, tuple) else (c, None) for c in dynamic_completions
  199. ]
  200. return results
  201. def get_visible_commands_starting_with(ctx, starts_with):
  202. """
  203. :param ctx: context associated with the parsed command
  204. :starts_with: string that visible commands must start with.
  205. :return: all visible (not hidden) commands that start with starts_with.
  206. """
  207. for c in ctx.command.list_commands(ctx):
  208. if c.startswith(starts_with):
  209. command = ctx.command.get_command(ctx, c)
  210. if not command.hidden:
  211. yield command
  212. def add_subcommand_completions(ctx, incomplete, completions_out):
  213. # Add subcommand completions.
  214. if isinstance(ctx.command, MultiCommand):
  215. completions_out.extend(
  216. [
  217. (c.name, c.get_short_help_str())
  218. for c in get_visible_commands_starting_with(ctx, incomplete)
  219. ]
  220. )
  221. # Walk up the context list and add any other completion
  222. # possibilities from chained commands
  223. while ctx.parent is not None:
  224. ctx = ctx.parent
  225. if isinstance(ctx.command, MultiCommand) and ctx.command.chain:
  226. remaining_commands = [
  227. c
  228. for c in get_visible_commands_starting_with(ctx, incomplete)
  229. if c.name not in ctx.protected_args
  230. ]
  231. completions_out.extend(
  232. [(c.name, c.get_short_help_str()) for c in remaining_commands]
  233. )
  234. def get_choices(cli, prog_name, args, incomplete):
  235. """
  236. :param cli: command definition
  237. :param prog_name: the program that is running
  238. :param args: full list of args
  239. :param incomplete: the incomplete text to autocomplete
  240. :return: all the possible completions for the incomplete
  241. """
  242. all_args = copy.deepcopy(args)
  243. ctx = resolve_ctx(cli, prog_name, args)
  244. if ctx is None:
  245. return []
  246. has_double_dash = "--" in all_args
  247. # In newer versions of bash long opts with '='s are partitioned, but
  248. # it's easier to parse without the '='
  249. if start_of_option(incomplete) and WORDBREAK in incomplete:
  250. partition_incomplete = incomplete.partition(WORDBREAK)
  251. all_args.append(partition_incomplete[0])
  252. incomplete = partition_incomplete[2]
  253. elif incomplete == WORDBREAK:
  254. incomplete = ""
  255. completions = []
  256. if not has_double_dash and start_of_option(incomplete):
  257. # completions for partial options
  258. for param in ctx.command.params:
  259. if isinstance(param, Option) and not param.hidden:
  260. param_opts = [
  261. param_opt
  262. for param_opt in param.opts + param.secondary_opts
  263. if param_opt not in all_args or param.multiple
  264. ]
  265. completions.extend(
  266. [(o, param.help) for o in param_opts if o.startswith(incomplete)]
  267. )
  268. return completions
  269. # completion for option values from user supplied values
  270. for param in ctx.command.params:
  271. if is_incomplete_option(all_args, param):
  272. return get_user_autocompletions(ctx, all_args, incomplete, param)
  273. # completion for argument values from user supplied values
  274. for param in ctx.command.params:
  275. if is_incomplete_argument(ctx.params, param):
  276. return get_user_autocompletions(ctx, all_args, incomplete, param)
  277. add_subcommand_completions(ctx, incomplete, completions)
  278. # Sort before returning so that proper ordering can be enforced in custom types.
  279. return sorted(completions)
  280. def do_complete(cli, prog_name, include_descriptions):
  281. cwords = split_arg_string(os.environ["COMP_WORDS"])
  282. cword = int(os.environ["COMP_CWORD"])
  283. args = cwords[1:cword]
  284. try:
  285. incomplete = cwords[cword]
  286. except IndexError:
  287. incomplete = ""
  288. for item in get_choices(cli, prog_name, args, incomplete):
  289. echo(item[0])
  290. if include_descriptions:
  291. # ZSH has trouble dealing with empty array parameters when
  292. # returned from commands, use '_' to indicate no description
  293. # is present.
  294. echo(item[1] if item[1] else "_")
  295. return True
  296. def do_complete_fish(cli, prog_name):
  297. cwords = split_arg_string(os.environ["COMP_WORDS"])
  298. incomplete = os.environ["COMP_CWORD"]
  299. args = cwords[1:]
  300. for item in get_choices(cli, prog_name, args, incomplete):
  301. if item[1]:
  302. echo("{arg}\t{desc}".format(arg=item[0], desc=item[1]))
  303. else:
  304. echo(item[0])
  305. return True
  306. def bashcomplete(cli, prog_name, complete_var, complete_instr):
  307. if "_" in complete_instr:
  308. command, shell = complete_instr.split("_", 1)
  309. else:
  310. command = complete_instr
  311. shell = "bash"
  312. if command == "source":
  313. echo(get_completion_script(prog_name, complete_var, shell))
  314. return True
  315. elif command == "complete":
  316. if shell == "fish":
  317. return do_complete_fish(cli, prog_name)
  318. elif shell in {"bash", "zsh"}:
  319. return do_complete(cli, prog_name, shell == "zsh")
  320. return False