Пример #1
0
    def Matches(self, comp):
        # type: (Api) -> Iterator[Union[Iterator, Iterator[str]]]
        """
    Args:
      comp: Callback args from readline.  Readline uses set_completer_delims to
        tokenize the string.

    Returns a list of matches relative to readline's completion_delims.
    We have to post-process the output of various completers.
    """
        arena = self.parse_ctx.arena  # Used by inner functions

        # Pass the original line "out of band" to the completion callback.
        line_until_tab = comp.line[:comp.end]
        self.comp_ui_state.line_until_tab = line_until_tab

        self.parse_ctx.trail.Clear()
        line_reader = reader.StringLineReader(line_until_tab,
                                              self.parse_ctx.arena)
        c_parser = self.parse_ctx.MakeOshParser(line_reader,
                                                emit_comp_dummy=True)

        # We want the output from parse_ctx, so we don't use the return value.
        try:
            c_parser.ParseLogicalLine()
        except error.Parse as e:
            # e.g. 'ls | ' will not parse.  Now inspect the parser state!
            pass

        debug_f = self.debug_f
        trail = self.parse_ctx.trail
        if 1:
            trail.PrintDebugString(debug_f)

        #
        # First try completing the shell language itself.
        #

        # NOTE: We get Eof_Real in the command state, but not in the middle of a
        # BracedVarSub.  This is due to the difference between the CommandParser
        # and WordParser.
        tokens = trail.tokens
        last = -1
        if tokens[-1].id == Id.Eof_Real:
            last -= 1  # ignore it

        try:
            t1 = tokens[last]
        except IndexError:
            t1 = None
        try:
            t2 = tokens[last - 1]
        except IndexError:
            t2 = None

        debug_f.log('line: %r', comp.line)
        debug_f.log('rl_slice from byte %d to %d: %r', comp.begin, comp.end,
                    comp.line[comp.begin:comp.end])

        debug_f.log('t1 %s', t1)
        debug_f.log('t2 %s', t2)

        # Each of the 'yield' statements below returns a fully-completed line, to
        # appease the readline library.  The root cause of this dance: If there's
        # one candidate, readline is responsible for redrawing the input line.  OSH
        # only displays candidates and never redraws the input line.

        def _TokenStart(tok):
            # type: (Token) -> int
            span = arena.GetLineSpan(tok.span_id)
            return span.col

        if t2:  # We always have t1?
            # echo $
            if IsDollar(t2) and IsDummy(t1):
                self.comp_ui_state.display_pos = _TokenStart(t2) + 1  # 1 for $
                for name in self.mem.VarNames():
                    yield line_until_tab + name  # no need to quote var names
                return

            # echo ${
            if t2.id == Id.Left_DollarBrace and IsDummy(t1):
                self.comp_ui_state.display_pos = _TokenStart(
                    t2) + 2  # 2 for ${
                for name in self.mem.VarNames():
                    yield line_until_tab + name  # no need to quote var names
                return

            # echo $P
            if t2.id == Id.VSub_DollarName and IsDummy(t1):
                # Example: ${undef:-$P
                # readline splits at ':' so we have to prepend '-$' to every completed
                # variable name.
                self.comp_ui_state.display_pos = _TokenStart(t2) + 1  # 1 for $
                to_complete = t2.val[1:]
                n = len(to_complete)
                for name in self.mem.VarNames():
                    if name.startswith(to_complete):
                        yield line_until_tab + name[
                            n:]  # no need to quote var names
                return

            # echo ${P
            if t2.id == Id.VSub_Name and IsDummy(t1):
                self.comp_ui_state.display_pos = _TokenStart(t2)  # no offset
                to_complete = t2.val
                n = len(to_complete)
                for name in self.mem.VarNames():
                    if name.startswith(to_complete):
                        yield line_until_tab + name[
                            n:]  # no need to quote var names
                return

            # echo $(( VAR
            if t2.id == Id.Lit_ArithVarLike and IsDummy(t1):
                self.comp_ui_state.display_pos = _TokenStart(t2)  # no offset
                to_complete = t2.val
                n = len(to_complete)
                for name in self.mem.VarNames():
                    if name.startswith(to_complete):
                        yield line_until_tab + name[
                            n:]  # no need to quote var names
                return

        if trail.words:
            # echo ~<TAB>
            # echo ~a<TAB> $(home dirs)
            # This must be done at a word level, and TildeDetectAll() does NOT help
            # here, because they don't have trailing slashes yet!  We can't do it on
            # tokens, because otherwise f~a will complete.  Looking at word_part is
            # EXACTLY what we want.
            parts = trail.words[-1].parts
            if (len(parts) == 2 and parts[0].tag_() == word_part_e.Literal
                    and parts[1].tag_() == word_part_e.Literal
                    and parts[0].id == Id.Lit_TildeLike
                    and parts[1].id == Id.Lit_CompDummy):
                t2 = parts[0]

                # +1 for ~
                self.comp_ui_state.display_pos = _TokenStart(parts[0]) + 1

                to_complete = t2.val[1:]
                n = len(to_complete)
                for u in pwd.getpwall():  # catch errors?
                    name = u.pw_name
                    if name.startswith(to_complete):
                        yield line_until_tab + ShellQuoteB(name[n:]) + '/'
                return

        # echo hi > f<TAB>   (complete redirect arg)
        if trail.redirects:
            r = trail.redirects[-1]
            # Only complete 'echo >', but not 'echo >&' or 'cat <<'
            # TODO: Don't complete <<< 'h'
            if (r.arg.tag_() == redir_param_e.Word
                    and consts.RedirArgType(r.op.id) == redir_arg_type_e.Path):
                arg_word = r.arg
                if WordEndsWithCompDummy(arg_word):
                    debug_f.log('Completing redirect arg')

                    try:
                        val = self.word_ev.EvalWordToString(r.arg)
                    except error.FatalRuntime as e:
                        debug_f.log('Error evaluating redirect word: %s', e)
                        return
                    if val.tag_() != value_e.Str:
                        debug_f.log("Didn't get a string from redir arg")
                        return

                    span_id = word_.LeftMostSpanForWord(arg_word)
                    span = arena.GetLineSpan(span_id)

                    self.comp_ui_state.display_pos = span.col

                    comp.Update(
                        to_complete=val.s)  # FileSystemAction uses only this
                    n = len(val.s)
                    action = FileSystemAction(add_slash=True)
                    for name in action.Matches(comp):
                        yield line_until_tab + ShellQuoteB(name[n:])
                    return

        #
        # We're not completing the shell language.  Delegate to user-defined
        # completion for external tools.
        #

        # Set below, and set on retries.
        base_opts = None
        user_spec = None

        # Used on retries.
        partial_argv = []
        num_partial = -1
        first = None

        if trail.words:
            # Now check if we're completing a word!
            if WordEndsWithCompDummy(trail.words[-1]):
                debug_f.log('Completing words')
                #
                # It didn't look like we need to complete var names, tilde, redirects,
                # etc.  Now try partial_argv, which may involve invoking PLUGINS.

                # needed to complete paths with ~
                words2 = word_.TildeDetectAll(trail.words)
                if 0:
                    debug_f.log('After tilde detection')
                    for w in words2:
                        print(w, file=debug_f)

                if 0:
                    debug_f.log('words2:')
                    for w2 in words2:
                        debug_f.log(' %s', w2)

                for w in words2:
                    try:
                        # TODO:
                        # - Should we call EvalWordSequence?  But turn globbing off?  It
                        # can do splitting and such.
                        # - We could have a variant to eval TildeSub to ~ ?
                        val = self.word_ev.EvalWordToString(w)
                    except error.FatalRuntime:
                        # Why would it fail?
                        continue
                    if val.tag_() == value_e.Str:
                        partial_argv.append(val.s)
                    else:
                        pass

                debug_f.log('partial_argv: %s', partial_argv)
                num_partial = len(partial_argv)

                first = partial_argv[0]
                alias_first = None
                debug_f.log('alias_words: %s', trail.alias_words)

                if trail.alias_words:
                    w = trail.alias_words[0]
                    try:
                        val = self.word_ev.EvalWordToString(w)
                    except error.FatalRuntime:
                        pass
                    alias_first = val.s
                    debug_f.log('alias_first: %s', alias_first)

                if num_partial == 0:  # should never happen because of Lit_CompDummy
                    raise AssertionError()
                elif num_partial == 1:
                    base_opts, user_spec = self.comp_lookup.GetFirstSpec()

                    # Display/replace since the beginning of the first word.  Note: this
                    # is non-zero in the case of
                    # echo $(gr   and
                    # echo `gr

                    span_id = word_.LeftMostSpanForWord(trail.words[0])
                    span = arena.GetLineSpan(span_id)
                    self.comp_ui_state.display_pos = span.col
                    self.debug_f.log('** DISPLAY_POS = %d',
                                     self.comp_ui_state.display_pos)

                else:
                    base_opts, user_spec = self.comp_lookup.GetSpecForName(
                        first)
                    if not user_spec and alias_first:
                        base_opts, user_spec = self.comp_lookup.GetSpecForName(
                            alias_first)
                        if user_spec:
                            # Pass the aliased command to the user-defined function, and use
                            # it for retries.
                            first = alias_first
                    if not user_spec:
                        base_opts, user_spec = self.comp_lookup.GetFallback()

                    # Display since the beginning
                    span_id = word_.LeftMostSpanForWord(trail.words[-1])
                    span = arena.GetLineSpan(span_id)
                    self.comp_ui_state.display_pos = span.col
                    self.debug_f.log('words[-1]: %r', trail.words[-1])
                    self.debug_f.log('display_pos %d',
                                     self.comp_ui_state.display_pos)

                # Update the API for user-defined functions.
                index = len(
                    partial_argv) - 1  # COMP_CWORD is -1 when it's empty
                prev = '' if index == 0 else partial_argv[index - 1]
                comp.Update(first=first,
                            to_complete=partial_argv[-1],
                            prev=prev,
                            index=index,
                            partial_argv=partial_argv)

        # This happens in the case of [[ and ((, or a syntax error like 'echo < >'.
        if not user_spec:
            debug_f.log("Didn't find anything to complete")
            return

        # Reset it back to what was registered.  User-defined functions can mutate
        # it.
        dynamic_opts = {}
        self.compopt_state.dynamic_opts = dynamic_opts
        self.compopt_state.currently_completing = True
        try:
            done = False
            while not done:
                try:
                    for candidate in self._PostProcess(base_opts, dynamic_opts,
                                                       user_spec, comp):
                        yield candidate
                except _RetryCompletion as e:
                    debug_f.log('Got 124, trying again ...')

                    # Get another user_spec.  The ShellFuncAction may have 'sourced' code
                    # and run 'complete' to mutate comp_lookup, and we want to get that
                    # new entry.
                    if num_partial == 0:
                        raise AssertionError()
                    elif num_partial == 1:
                        base_opts, user_spec = self.comp_lookup.GetFirstSpec()
                    else:
                        # (already processed alias_first)
                        base_opts, user_spec = self.comp_lookup.GetSpecForName(
                            first)
                        if not user_spec:
                            base_opts, user_spec = self.comp_lookup.GetFallback(
                            )
                else:
                    done = True  # exhausted candidates without getting a retry
        finally:
            self.compopt_state.currently_completing = False
Пример #2
0
    def _PostProcess(
            self,
            base_opts,  # type: Dict
            dynamic_opts,  # type: Dict
            user_spec,  # type: UserSpec
            comp,  # type: Api
    ):
        # type: (...) -> Iterator[Union[Iterator, Iterator[str]]]
        """
    Add trailing spaces / slashes to completion candidates, and time them.

    NOTE: This post-processing MUST go here, and not in UserSpec, because it's
    in READLINE in bash.  compgen doesn't see it.
    """
        self.debug_f.log('Completing %r ... (Ctrl-C to cancel)', comp.line)
        start_time = time.time()

        # TODO: dedupe candidates?  You can get two 'echo' in bash, which is dumb.

        i = 0
        for candidate, is_fs_action in user_spec.Matches(comp):
            # SUBTLE: dynamic_opts is part of compopt_state, which ShellFuncAction
            # can mutate!  So we don't want to pull this out of the loop.
            #
            # TODO: The candidates from each actions shouldn't be flattened.
            # for action in user_spec.Actions():
            #   if action.IsFileSystem():  # this returns is_dir too
            #
            #   action.Run()  # might set dynamic opts
            #   opt_nospace = base_opts...
            #   if 'nospace' in dynamic_opts:
            #     opt_nosspace = dynamic_opts['nospace']
            #   for candidate in action.Matches():
            #     add space or /
            #     and do escaping too
            #
            # Or maybe you can request them on demand?  Most actions are EAGER.
            # While the ShellacAction is LAZY?  And you should be able to cancel it!

            # NOTE: User-defined plugins (and the -P flag) can REWRITE what the user
            # already typed.  So
            #
            # $ echo 'dir with spaces'/f<TAB>
            #
            # can be rewritten to:
            #
            # $ echo dir\ with\ spaces/foo
            line_until_tab = self.comp_ui_state.line_until_tab
            line_until_word = line_until_tab[:self.comp_ui_state.display_pos]

            opt_filenames = base_opts.get('filenames', False)
            if 'filenames' in dynamic_opts:
                opt_filenames = dynamic_opts['filenames']

            # compopt -o filenames is for user-defined actions.  Or any
            # FileSystemAction needs it.
            if is_fs_action or opt_filenames:
                if path_stat.isdir(candidate):  # TODO: test coverage
                    yield line_until_word + ShellQuoteB(candidate) + '/'
                    continue

            opt_nospace = base_opts.get('nospace', False)
            if 'nospace' in dynamic_opts:
                opt_nospace = dynamic_opts['nospace']

            sp = '' if opt_nospace else ' '
            yield line_until_word + ShellQuoteB(candidate) + sp

            # NOTE: Can't use %.2f in production build!
            i += 1
            elapsed_ms = (time.time() - start_time) * 1000.0
            plural = '' if i == 1 else 'es'

            # TODO: Show this in the UI if it takes too long!
            if 0:
                self.debug_f.log(
                    '... %d match%s for %r in %d ms (Ctrl-C to cancel)', i,
                    plural, comp.line, elapsed_ms)

        elapsed_ms = (time.time() - start_time) * 1000.0
        plural = '' if i == 1 else 'es'
        self.debug_f.log('Found %d match%s for %r in %d ms', i, plural,
                         comp.line, elapsed_ms)