OILS
/
osh
/
history.py
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" % prev)
|
120 |
|
121 |
tok1 = location.LeftTokenForWord(w)
|
122 |
tok2 = location.RightTokenForWord(w)
|
123 |
|
124 |
elif ch == '*':
|
125 |
try:
|
126 |
w1 = words[1]
|
127 |
w2 = words[-1]
|
128 |
except IndexError:
|
129 |
raise util.HistoryError(
|
130 |
"Couldn't find words in %r" % prev)
|
131 |
|
132 |
tok1 = location.LeftTokenForWord(w1)
|
133 |
tok2 = location.RightTokenForWord(w2)
|
134 |
|
135 |
else:
|
136 |
raise AssertionError(ch)
|
137 |
|
138 |
begin = tok1.col
|
139 |
end = tok2.col + tok2.length
|
140 |
|
141 |
out = prev[begin:end]
|
142 |
|
143 |
elif id_ == Id.History_Num:
|
144 |
index = int(
|
145 |
val[1:]) # regex ensures this. Maybe have - on the front.
|
146 |
if index < 0:
|
147 |
num = history_len + 1 + index
|
148 |
else:
|
149 |
num = index
|
150 |
|
151 |
out = self.readline.get_history_item(num)
|
152 |
if out is None: # out of range
|
153 |
raise util.HistoryError('%s: not found' % val)
|
154 |
|
155 |
elif id_ == Id.History_Search:
|
156 |
# Remove the required space at the end and save it. A simple hack than
|
157 |
# the one bash has.
|
158 |
last_char = val[-1]
|
159 |
val = val[:-1]
|
160 |
|
161 |
# Search backward
|
162 |
prefix = None # type: Optional[str]
|
163 |
substring = ''
|
164 |
if val[1] == '?':
|
165 |
substring = val[2:]
|
166 |
else:
|
167 |
prefix = val[1:]
|
168 |
|
169 |
out = None
|
170 |
for i in xrange(history_len, 1, -1):
|
171 |
cmd = self.readline.get_history_item(i)
|
172 |
if prefix is not None and cmd.startswith(prefix):
|
173 |
out = cmd
|
174 |
if len(substring) and substring in cmd:
|
175 |
out = cmd
|
176 |
if out is not None:
|
177 |
# mycpp: rewrite of +=
|
178 |
out = out + last_char # restore required space
|
179 |
break
|
180 |
|
181 |
if out is None:
|
182 |
raise util.HistoryError('%r found no results' % val)
|
183 |
|
184 |
else:
|
185 |
raise AssertionError(id_)
|
186 |
|
187 |
parts.append(out)
|
188 |
|
189 |
line = ''.join(parts)
|
190 |
# show what we expanded to
|
191 |
print('! %s' % line)
|
192 |
return line
|