OILS / core / main_loop.py View on Github | oilshell.org

402 lines, 225 significant
1"""main_loop.py.
2
3Variants:
4 main_loop.Interactive() calls ParseInteractiveLine() and ExecuteAndCatch()
5 main_loop.Batch() calls ParseLogicalLine() and ExecuteAndCatch()
6 main_loop.Headless() calls Batch() like eval and source.
7 We want 'echo 1\necho 2\n' to work, so we
8 don't bother with "the PS2 problem".
9 main_loop.ParseWholeFile() calls ParseLogicalLine(). Used by osh -n.
10"""
11from __future__ import print_function
12
13from _devbuild.gen import arg_types
14from _devbuild.gen.syntax_asdl import (command, command_t, parse_result,
15 parse_result_e)
16from core import error
17from core import process
18from core import ui
19from core import util
20from frontend import reader
21from osh import cmd_eval
22from mycpp import mylib
23from mycpp.mylib import log, print_stderr, probe, tagswitch
24
25import fanos
26import posix_ as posix
27
28from typing import cast, Any, List, TYPE_CHECKING
29if TYPE_CHECKING:
30 from core.comp_ui import _IDisplay
31 from core.ui import ErrorFormatter
32 from frontend import parse_lib
33 from osh.cmd_parse import CommandParser
34 from osh.cmd_eval import CommandEvaluator
35 from osh.prompt import UserPlugin
36
37_ = log
38
39
40class ctx_Descriptors(object):
41 """Save and restore descriptor state for the headless EVAL command."""
42
43 def __init__(self, fds):
44 # type: (List[int]) -> None
45
46 self.saved0 = process.SaveFd(0)
47 self.saved1 = process.SaveFd(1)
48 self.saved2 = process.SaveFd(2)
49
50 #ShowDescriptorState('BEFORE')
51 posix.dup2(fds[0], 0)
52 posix.dup2(fds[1], 1)
53 posix.dup2(fds[2], 2)
54
55 self.fds = fds
56
57 def __enter__(self):
58 # type: () -> None
59 pass
60
61 def __exit__(self, type, value, traceback):
62 # type: (Any, Any, Any) -> None
63
64 # Restore
65 posix.dup2(self.saved0, 0)
66 posix.dup2(self.saved1, 1)
67 posix.dup2(self.saved2, 2)
68
69 # Restoration done, so close
70 posix.close(self.saved0)
71 posix.close(self.saved1)
72 posix.close(self.saved2)
73
74 # And close descriptors we were passed
75 posix.close(self.fds[0])
76 posix.close(self.fds[1])
77 posix.close(self.fds[2])
78
79
80def fanos_log(msg):
81 # type: (str) -> None
82 print_stderr('[FANOS] %s' % msg)
83
84
85def ShowDescriptorState(label):
86 # type: (str) -> None
87 if mylib.PYTHON:
88 import os # Our posix fork doesn't have os.system
89 import time
90 time.sleep(0.01) # prevent interleaving
91
92 pid = posix.getpid()
93 print_stderr(label + ' (PID %d)' % pid)
94
95 os.system('ls -l /proc/%d/fd >&2' % pid)
96
97 time.sleep(0.01) # prevent interleaving
98
99
100class Headless(object):
101 """Main loop for headless mode."""
102
103 def __init__(self, cmd_ev, parse_ctx, errfmt):
104 # type: (CommandEvaluator, parse_lib.ParseContext, ErrorFormatter) -> None
105 self.cmd_ev = cmd_ev
106 self.parse_ctx = parse_ctx
107 self.errfmt = errfmt
108
109 def Loop(self):
110 # type: () -> int
111 try:
112 return self._Loop()
113 except ValueError as e:
114 fanos.send(1, 'ERROR %s' % e)
115 return 1
116
117 def EVAL(self, arg):
118 # type: (str) -> str
119
120 # This logic is similar to the 'eval' builtin in osh/builtin_meta.
121
122 # Note: we're not using the InteractiveLineReader, so there's no history
123 # expansion. It would be nice if there was a way for the client to use
124 # that.
125 line_reader = reader.StringLineReader(arg, self.parse_ctx.arena)
126 c_parser = self.parse_ctx.MakeOshParser(line_reader)
127
128 # Status is unused; $_ can be queried by the headless client
129 unused_status = Batch(self.cmd_ev, c_parser, self.errfmt, 0)
130
131 return '' # result is always 'OK ' since there was no protocol error
132
133 def _Loop(self):
134 # type: () -> int
135 fanos_log(
136 'Connect stdin and stdout to one end of socketpair() and send control messages. osh writes debug messages (like this one) to stderr.'
137 )
138
139 fd_out = [] # type: List[int]
140 while True:
141 try:
142 blob = fanos.recv(0, fd_out)
143 except ValueError as e:
144 fanos_log('protocol error: %s' % e)
145 raise # higher level handles it
146
147 if blob is None:
148 fanos_log('EOF received')
149 break
150
151 fanos_log('received blob %r' % blob)
152 if ' ' in blob:
153 bs = blob.split(' ', 1)
154 command = bs[0]
155 arg = bs[1]
156 else:
157 command = blob
158 arg = ''
159
160 if command == 'GETPID':
161 reply = str(posix.getpid())
162
163 elif command == 'EVAL':
164 #fanos_log('arg %r', arg)
165
166 if len(fd_out) != 3:
167 raise ValueError('Expected 3 file descriptors')
168
169 for fd in fd_out:
170 fanos_log('received descriptor %d' % fd)
171
172 with ctx_Descriptors(fd_out):
173 reply = self.EVAL(arg)
174
175 #ShowDescriptorState('RESTORED')
176
177 # Note: lang == 'osh' or lang == 'ysh' puts this in different modes.
178 # Do we also need 'complete --osh' and 'complete --ysh' ?
179 elif command == 'PARSE':
180 # Just parse
181 reply = 'TODO:PARSE'
182
183 else:
184 fanos_log('Invalid command %r' % command)
185 raise ValueError('Invalid command %r' % command)
186
187 fanos.send(1, b'OK %s' % reply)
188 del fd_out[:] # reset for next iteration
189
190 return 0
191
192
193def Interactive(
194 flag, # type: arg_types.main
195 cmd_ev, # type: CommandEvaluator
196 c_parser, # type: CommandParser
197 display, # type: _IDisplay
198 prompt_plugin, # type: UserPlugin
199 waiter, # type: process.Waiter
200 errfmt, # type: ErrorFormatter
201):
202 # type: (...) -> int
203 status = 0
204 done = False
205 while not done:
206 mylib.MaybeCollect() # manual GC point
207
208 # - This loop has a an odd structure because we want to do cleanup
209 # after every 'break'. (The ones without 'done = True' were
210 # 'continue')
211 # - display.EraseLines() needs to be called BEFORE displaying anything, so
212 # it appears in all branches.
213
214 while True: # ONLY EXECUTES ONCE
215 quit = False
216 prompt_plugin.Run()
217 try:
218 # may raise HistoryError or ParseError
219 result = c_parser.ParseInteractiveLine()
220 UP_result = result
221 with tagswitch(result) as case:
222 if case(parse_result_e.EmptyLine):
223 display.EraseLines()
224 # POSIX shell behavior: waitpid(-1) and show job "Done"
225 # messages
226 waiter.PollNotifications()
227 quit = True
228 elif case(parse_result_e.Eof):
229 display.EraseLines()
230 done = True
231 quit = True
232 elif case(parse_result_e.Node):
233 result = cast(parse_result.Node, UP_result)
234 node = result.cmd
235 else:
236 raise AssertionError()
237
238 except util.HistoryError as e: # e.g. expansion failed
239 # Where this happens:
240 # for i in 1 2 3; do
241 # !invalid
242 # done
243 display.EraseLines()
244 print(e.UserErrorString())
245 quit = True
246 except error.Parse as e:
247 display.EraseLines()
248 errfmt.PrettyPrintError(e)
249 status = 2
250 cmd_ev.mem.SetLastStatus(status)
251 quit = True
252 except KeyboardInterrupt: # thrown by InteractiveLineReader._GetLine()
253 # Here we must print a newline BEFORE EraseLines()
254 print('^C')
255 display.EraseLines()
256 # http://www.tldp.org/LDP/abs/html/exitcodes.html
257 # bash gives 130, dash gives 0, zsh gives 1.
258 # Unless we SET cmd_ev.last_status, scripts see it, so don't bother now.
259 quit = True
260
261 if quit:
262 break
263
264 display.EraseLines() # Clear candidates right before executing
265
266 # to debug the slightly different interactive prasing
267 if cmd_ev.exec_opts.noexec():
268 ui.PrintAst(node, flag)
269 break
270
271 try:
272 is_return, _ = cmd_ev.ExecuteAndCatch(node)
273 except KeyboardInterrupt: # issue 467, Ctrl-C during $(sleep 1)
274 is_return = False
275 display.EraseLines()
276 status = 130 # 128 + 2
277 cmd_ev.mem.SetLastStatus(status)
278 break
279
280 status = cmd_ev.LastStatus()
281
282 waiter.PollNotifications()
283
284 if is_return:
285 done = True
286 break
287
288 break # QUIT LOOP after one iteration.
289
290 # After every "logical line", no lines will be referenced by the Arena.
291 # Tokens in the LST still point to many lines, but lines with only comment
292 # or whitespace won't be reachable, so the GC will free them.
293 c_parser.arena.DiscardLines()
294
295 cmd_ev.RunPendingTraps() # Run trap handlers even if we get just ENTER
296
297 # Cleanup after every command (or failed command).
298
299 # Reset internal newline state.
300 c_parser.Reset()
301 c_parser.ResetInputObjects()
302
303 display.Reset() # clears dupes and number of lines last displayed
304
305 # TODO: Replace this with a shell hook? with 'trap', or it could be just
306 # like command_not_found. The hook can be 'echo $?' or something more
307 # complicated, i.e. with timestamps.
308 if flag.print_status:
309 print('STATUS\t%r' % status)
310
311 return status
312
313
314def Batch(cmd_ev, c_parser, errfmt, cmd_flags=0):
315 # type: (CommandEvaluator, CommandParser, ui.ErrorFormatter, int) -> int
316 """Loop for batch execution.
317
318 Returns:
319 int status, e.g. 2 on parse error
320
321 Can this be combined with interactive loop? Differences:
322
323 - Handling of parse errors.
324 - Have to detect here docs at the end?
325
326 Not a problem:
327 - Get rid of --print-status and --show-ast for now
328 - Get rid of EOF difference
329
330 TODO:
331 - Do source / eval need this?
332 - 'source' needs to parse incrementally so that aliases are respected
333 - I doubt 'eval' does! You can test it.
334 - In contrast, 'trap' should parse up front?
335 - What about $() ?
336 """
337 status = 0
338 while True:
339 probe('main_loop', 'Batch_parse_enter')
340 try:
341 node = c_parser.ParseLogicalLine() # can raise ParseError
342 if node is None: # EOF
343 c_parser.CheckForPendingHereDocs() # can raise ParseError
344 break
345 except error.Parse as e:
346 errfmt.PrettyPrintError(e)
347 status = 2
348 break
349
350 # After every "logical line", no lines will be referenced by the Arena.
351 # Tokens in the LST still point to many lines, but lines with only comment
352 # or whitespace won't be reachable, so the GC will free them.
353 c_parser.arena.DiscardLines()
354
355 # Only optimize if we're on the last line like -c "echo hi" etc.
356 if (cmd_flags & cmd_eval.IsMainProgram and
357 c_parser.line_reader.LastLineHint()):
358 cmd_flags |= cmd_eval.Optimize
359
360 probe('main_loop', 'Batch_parse_exit')
361
362 probe('main_loop', 'Batch_execute_enter')
363 # can't optimize this because we haven't seen the end yet
364 is_return, is_fatal = cmd_ev.ExecuteAndCatch(node, cmd_flags=cmd_flags)
365 status = cmd_ev.LastStatus()
366 # e.g. 'return' in middle of script, or divide by zero
367 if is_return or is_fatal:
368 break
369 probe('main_loop', 'Batch_execute_exit')
370
371 probe('main_loop', 'Batch_collect_enter')
372 mylib.MaybeCollect() # manual GC point
373 probe('main_loop', 'Batch_collect_exit')
374
375 return status
376
377
378def ParseWholeFile(c_parser):
379 # type: (CommandParser) -> command_t
380 """Parse an entire shell script.
381
382 This uses the same logic as Batch(). Used by:
383 - osh -n
384 - oshc translate
385 - Used by 'trap' to store code. But 'source' and 'eval' use Batch().
386
387 Note: it does NOT call DiscardLines
388 """
389 children = [] # type: List[command_t]
390 while True:
391 node = c_parser.ParseLogicalLine() # can raise ParseError
392 if node is None: # EOF
393 c_parser.CheckForPendingHereDocs() # can raise ParseError
394 break
395 children.append(node)
396
397 mylib.MaybeCollect() # manual GC point
398
399 if len(children) == 1:
400 return children[0]
401 else:
402 return command.CommandList(children)