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
|