# Copyright 2012-2021, Andrey Kislyuk and argcomplete contributors. # Licensed under the Apache License. See https://github.com/kislyuk/argcomplete for more info. import argparse import contextlib import os import sys from . import completers from . import my_shlex as shlex from .completers import FilesCompleter, SuppressCompleter from .my_argparse import IntrospectiveArgumentParser, action_is_greedy, action_is_open, action_is_satisfied from .shell_integration import shellcode # noqa _DEBUG = "_ARC_DEBUG" in os.environ debug_stream = sys.stderr def debug(*args): if _DEBUG: print(file=debug_stream, *args) BASH_FILE_COMPLETION_FALLBACK = 79 BASH_DIR_COMPLETION_FALLBACK = 80 safe_actions = ( argparse._StoreAction, argparse._StoreConstAction, argparse._StoreTrueAction, argparse._StoreFalseAction, argparse._AppendAction, argparse._AppendConstAction, argparse._CountAction, ) @contextlib.contextmanager def mute_stdout(): stdout = sys.stdout sys.stdout = open(os.devnull, "w") try: yield finally: sys.stdout = stdout @contextlib.contextmanager def mute_stderr(): stderr = sys.stderr sys.stderr = open(os.devnull, "w") try: yield finally: sys.stderr.close() sys.stderr = stderr class ArgcompleteException(Exception): pass def split_line(line, point=None): if point is None: point = len(line) line = line[:point] lexer = shlex.shlex(line, posix=True) lexer.whitespace_split = True lexer.wordbreaks = os.environ.get("_ARGCOMPLETE_COMP_WORDBREAKS", "") words = [] def split_word(word): # TODO: make this less ugly point_in_word = len(word) + point - lexer.instream.tell() if isinstance(lexer.state, (str, bytes)) and lexer.state in lexer.whitespace: point_in_word += 1 if point_in_word > len(word): debug("In trailing whitespace") words.append(word) word = "" prefix, suffix = word[:point_in_word], word[point_in_word:] prequote = "" # posix if lexer.state is not None and lexer.state in lexer.quotes: prequote = lexer.state # non-posix # if len(prefix) > 0 and prefix[0] in lexer.quotes: # prequote, prefix = prefix[0], prefix[1:] return prequote, prefix, suffix, words, lexer.last_wordbreak_pos while True: try: word = lexer.get_token() if word == lexer.eof: # TODO: check if this is ever unsafe # raise ArgcompleteException("Unexpected end of input") return "", "", "", words, None if lexer.instream.tell() >= point: debug("word", word, "split, lexer state: '{s}'".format(s=lexer.state)) return split_word(word) words.append(word) except ValueError: debug("word", lexer.token, "split (lexer stopped, state: '{s}')".format(s=lexer.state)) if lexer.instream.tell() >= point: return split_word(lexer.token) else: msg = ( "Unexpected internal state. " "Please report this bug at https://github.com/kislyuk/argcomplete/issues." ) raise ArgcompleteException(msg) def default_validator(completion, prefix): return completion.startswith(prefix) class CompletionFinder(object): """ Inherit from this class if you wish to override any of the stages below. Otherwise, use ``argcomplete.autocomplete()`` directly (it's a convenience instance of this class). It has the same signature as :meth:`CompletionFinder.__call__()`. """ def __init__( self, argument_parser=None, always_complete_options=True, exclude=None, validator=None, print_suppressed=False, default_completer=FilesCompleter(), append_space=None, ): self._parser = argument_parser self.always_complete_options = always_complete_options self.exclude = exclude if validator is None: validator = default_validator self.validator = validator self.print_suppressed = print_suppressed self.completing = False self._display_completions = {} self.default_completer = default_completer if append_space is None: append_space = os.environ.get("_ARGCOMPLETE_SUPPRESS_SPACE") != "1" self.append_space = append_space def __call__( self, argument_parser, always_complete_options=True, exit_method=os._exit, output_stream=None, exclude=None, validator=None, print_suppressed=False, append_space=None, default_completer=FilesCompleter(), ): """ :param argument_parser: The argument parser to autocomplete on :type argument_parser: :class:`argparse.ArgumentParser` :param always_complete_options: Controls the autocompletion of option strings if an option string opening character (normally ``-``) has not been entered. If ``True`` (default), both short (``-x``) and long (``--x``) option strings will be suggested. If ``False``, no option strings will be suggested. If ``long``, long options and short options with no long variant will be suggested. If ``short``, short options and long options with no short variant will be suggested. :type always_complete_options: boolean or string :param exit_method: Method used to stop the program after printing completions. Defaults to :meth:`os._exit`. If you want to perform a normal exit that calls exit handlers, use :meth:`sys.exit`. :type exit_method: callable :param exclude: List of strings representing options to be omitted from autocompletion :type exclude: iterable :param validator: Function to filter all completions through before returning (called with two string arguments, completion and prefix; return value is evaluated as a boolean) :type validator: callable :param print_suppressed: Whether or not to autocomplete options that have the ``help=argparse.SUPPRESS`` keyword argument set. :type print_suppressed: boolean :param append_space: Whether to append a space to unique matches. The default is ``True``. :type append_space: boolean .. note:: If you are not subclassing CompletionFinder to override its behaviors, use ``argcomplete.autocomplete()`` directly. It has the same signature as this method. Produces tab completions for ``argument_parser``. See module docs for more info. Argcomplete only executes actions if their class is known not to have side effects. Custom action classes can be added to argcomplete.safe_actions, if their values are wanted in the ``parsed_args`` completer argument, or their execution is otherwise desirable. """ self.__init__( argument_parser, always_complete_options=always_complete_options, exclude=exclude, validator=validator, print_suppressed=print_suppressed, append_space=append_space, default_completer=default_completer, ) if "_ARGCOMPLETE" not in os.environ: # not an argument completion invocation return global debug_stream try: debug_stream = os.fdopen(9, "w") except Exception: debug_stream = sys.stderr debug() if output_stream is None: filename = os.environ.get("_ARGCOMPLETE_STDOUT_FILENAME") if filename is not None: debug("Using output file {}".format(filename)) output_stream = open(filename, "w") if output_stream is None: try: output_stream = os.fdopen(8, "w") except Exception: debug("Unable to open fd 8 for writing, quitting") exit_method(1) # print("", stream=debug_stream) # for v in "COMP_CWORD COMP_LINE COMP_POINT COMP_TYPE COMP_KEY _ARGCOMPLETE_COMP_WORDBREAKS COMP_WORDS".split(): # print(v, os.environ[v], stream=debug_stream) ifs = os.environ.get("_ARGCOMPLETE_IFS", "\013") if len(ifs) != 1: debug("Invalid value for IFS, quitting [{v}]".format(v=ifs)) exit_method(1) dfs = os.environ.get("_ARGCOMPLETE_DFS") if dfs and len(dfs) != 1: debug("Invalid value for DFS, quitting [{v}]".format(v=dfs)) exit_method(1) comp_line = os.environ["COMP_LINE"] comp_point = int(os.environ["COMP_POINT"]) cword_prequote, cword_prefix, cword_suffix, comp_words, last_wordbreak_pos = split_line(comp_line, comp_point) # _ARGCOMPLETE is set by the shell script to tell us where comp_words # should start, based on what we're completing. # 1: