| 1 | """
|
| 2 | history.py: A LIBRARY for history evaluation.
|
| 3 |
|
| 4 | UI details should go in core/ui.py.
|
| 5 | """
|
| 6 | from __future__ import print_function
|
| 7 |
|
| 8 | from _devbuild.gen.id_kind_asdl import Id
|
| 9 | from core import error
|
| 10 | from core import util
|
| 11 | #from mycpp.mylib import log
|
| 12 | from frontend import location
|
| 13 | from frontend import match
|
| 14 | from frontend import reader
|
| 15 |
|
| 16 | from typing import List, Optional, TYPE_CHECKING
|
| 17 | if TYPE_CHECKING:
|
| 18 | from frontend.parse_lib import ParseContext
|
| 19 | from frontend.py_readline import Readline
|
| 20 | from core.util import _DebugFile
|
| 21 |
|
| 22 |
|
| 23 | class Evaluator(object):
|
| 24 | """Expand ! commands within the command line.
|
| 25 |
|
| 26 | This necessarily happens BEFORE lexing.
|
| 27 |
|
| 28 | NOTE: This should also be used in completion, and it COULD be used in history
|
| 29 | -p, if we want to support that.
|
| 30 | """
|
| 31 |
|
| 32 | def __init__(self, readline, parse_ctx, debug_f):
|
| 33 | # type: (Optional[Readline], ParseContext, _DebugFile) -> None
|
| 34 | self.readline = readline
|
| 35 | self.parse_ctx = parse_ctx
|
| 36 | self.debug_f = debug_f
|
| 37 |
|
| 38 | def Eval(self, line):
|
| 39 | # type: (str) -> str
|
| 40 | """Returns an expanded line."""
|
| 41 |
|
| 42 | if not self.readline:
|
| 43 | return line
|
| 44 |
|
| 45 | tokens = match.HistoryTokens(line)
|
| 46 | #self.debug_f.log('tokens %r', tokens)
|
| 47 |
|
| 48 | # Common case: no history expansion.
|
| 49 | # mycpp: rewrite of all()
|
| 50 | ok = True
|
| 51 | for (id_, _) in tokens:
|
| 52 | if id_ != Id.History_Other:
|
| 53 | ok = False
|
| 54 | break
|
| 55 |
|
| 56 | if ok:
|
| 57 | return line
|
| 58 |
|
| 59 | history_len = self.readline.get_current_history_length()
|
| 60 | if history_len <= 0: # no commands to expand
|
| 61 | return line
|
| 62 |
|
| 63 | self.debug_f.writeln('history length = %d' % history_len)
|
| 64 |
|
| 65 | parts = [] # type: List[str]
|
| 66 | for id_, val in tokens:
|
| 67 | if id_ == Id.History_Other:
|
| 68 | out = val
|
| 69 |
|
| 70 | elif id_ == Id.History_Op:
|
| 71 | # all operations get a part of the previous line
|
| 72 | prev = self.readline.get_history_item(history_len)
|
| 73 |
|
| 74 | ch = val[1]
|
| 75 | if ch == '!': # !!
|
| 76 | out = prev
|
| 77 | else:
|
| 78 | self.parse_ctx.trail.Clear() # not strictly necessary?
|
| 79 | line_reader = reader.StringLineReader(
|
| 80 | prev, self.parse_ctx.arena)
|
| 81 | c_parser = self.parse_ctx.MakeOshParser(line_reader)
|
| 82 | try:
|
| 83 | c_parser.ParseLogicalLine()
|
| 84 | except error.Parse as e:
|
| 85 | # Invalid command in history. bash uses a separate, approximate
|
| 86 | # history lexer which allows invalid commands, and will retrieve
|
| 87 | # parts of them. I guess we should too!
|
| 88 | self.debug_f.writeln(
|
| 89 | "Couldn't parse historical command %r: %s" %
|
| 90 | (prev, e.UserErrorString()))
|
| 91 |
|
| 92 | # NOTE: We're using the trail rather than the return value of
|
| 93 | # ParseLogicalLine() because it handles cases like
|
| 94 | #
|
| 95 | # $ for i in 1 2 3; do sleep ${i}; done
|
| 96 | # $ echo !$
|
| 97 | # which should expand to 'echo ${i}'
|
| 98 | #
|
| 99 | # Although the approximate bash parser returns 'done'.
|
| 100 | # TODO: The trail isn't particularly well-defined, so maybe this
|
| 101 | # isn't a great idea.
|
| 102 |
|
| 103 | words = self.parse_ctx.trail.words
|
| 104 | #self.debug_f.log('TRAIL words: %d', len(words))
|
| 105 |
|
| 106 | if ch == '^':
|
| 107 | try:
|
| 108 | w = words[1]
|
| 109 | except IndexError:
|
| 110 | raise util.HistoryError("No first word in %r" %
|
| 111 | prev)
|
| 112 | tok1 = location.LeftTokenForWord(w)
|
| 113 | tok2 = location.RightTokenForWord(w)
|
| 114 |
|
| 115 | elif ch == '$':
|
| 116 | try:
|
| 117 | w = words[-1]
|
| 118 | except IndexError:
|
| 119 | raise util.HistoryError("No last word in %r" %
|
| 120 | prev)
|
| 121 |
|
| 122 | tok1 = location.LeftTokenForWord(w)
|
| 123 | tok2 = location.RightTokenForWord(w)
|
| 124 |
|
| 125 | elif ch == '*':
|
| 126 | try:
|
| 127 | w1 = words[1]
|
| 128 | w2 = words[-1]
|
| 129 | except IndexError:
|
| 130 | raise util.HistoryError(
|
| 131 | "Couldn't find words in %r" % prev)
|
| 132 |
|
| 133 | tok1 = location.LeftTokenForWord(w1)
|
| 134 | tok2 = location.RightTokenForWord(w2)
|
| 135 |
|
| 136 | else:
|
| 137 | raise AssertionError(ch)
|
| 138 |
|
| 139 | begin = tok1.col
|
| 140 | end = tok2.col + tok2.length
|
| 141 |
|
| 142 | out = prev[begin:end]
|
| 143 |
|
| 144 | elif id_ == Id.History_Num:
|
| 145 | # regex ensures this. Maybe have - on the front.
|
| 146 | index = int(val[1:])
|
| 147 | if index < 0:
|
| 148 | num = history_len + 1 + index
|
| 149 | else:
|
| 150 | num = index
|
| 151 |
|
| 152 | out = self.readline.get_history_item(num)
|
| 153 | if out is None: # out of range
|
| 154 | raise util.HistoryError('%s: not found' % val)
|
| 155 |
|
| 156 | elif id_ == Id.History_Search:
|
| 157 | # Remove the required space at the end and save it. A simple hack than
|
| 158 | # the one bash has.
|
| 159 | last_char = val[-1]
|
| 160 | val = val[:-1]
|
| 161 |
|
| 162 | # Search backward
|
| 163 | prefix = None # type: Optional[str]
|
| 164 | substring = ''
|
| 165 | if val[1] == '?':
|
| 166 | substring = val[2:]
|
| 167 | else:
|
| 168 | prefix = val[1:]
|
| 169 |
|
| 170 | out = None
|
| 171 | for i in xrange(history_len, 1, -1):
|
| 172 | cmd = self.readline.get_history_item(i)
|
| 173 | if prefix is not None and cmd.startswith(prefix):
|
| 174 | out = cmd
|
| 175 | if len(substring) and substring in cmd:
|
| 176 | out = cmd
|
| 177 | if out is not None:
|
| 178 | # mycpp: rewrite of +=
|
| 179 | out = out + last_char # restore required space
|
| 180 | break
|
| 181 |
|
| 182 | if out is None:
|
| 183 | raise util.HistoryError('%r found no results' % val)
|
| 184 |
|
| 185 | else:
|
| 186 | raise AssertionError(id_)
|
| 187 |
|
| 188 | parts.append(out)
|
| 189 |
|
| 190 | line = ''.join(parts)
|
| 191 | # show what we expanded to
|
| 192 | print('! %s' % line)
|
| 193 | return line
|