| 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
 |