OILS / osh / history.py View on Github | oilshell.org

193 lines, 119 significant
1"""
2history.py: A LIBRARY for history evaluation.
3
4UI details should go in core/ui.py.
5"""
6from __future__ import print_function
7
8from _devbuild.gen.id_kind_asdl import Id
9from core import error
10from core import util
11#from mycpp.mylib import log
12from frontend import location
13from frontend import match
14from frontend import reader
15
16from typing import List, Optional, TYPE_CHECKING
17if TYPE_CHECKING:
18 from frontend.parse_lib import ParseContext
19 from frontend.py_readline import Readline
20 from core.util import _DebugFile
21
22
23class 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