OILS / core / vm.py View on Github | oils.pub

526 lines, 294 significant
1"""vm.py: Library for executing shell."""
2from __future__ import print_function
3
4from _devbuild.gen.id_kind_asdl import Id, Id_t, Id_str
5from _devbuild.gen.runtime_asdl import (CommandStatus, StatusArray, flow_e,
6 flow_t)
7from _devbuild.gen.syntax_asdl import Token, loc, loc_t
8from _devbuild.gen.value_asdl import value, value_e, value_t, Obj
9from core import dev
10from core import error
11from core.error import e_die
12from core import pyos
13from core import pyutil
14from display import ui
15from mycpp.mylib import log, tagswitch
16
17from typing import List, Dict, Tuple, Optional, Any, cast, TYPE_CHECKING
18if TYPE_CHECKING:
19 from _devbuild.gen.runtime_asdl import cmd_value, RedirValue
20 from _devbuild.gen.syntax_asdl import (command, command_t, CommandSub)
21 from builtin import hay_ysh
22 from core import optview
23 from core import state
24 from frontend import typed_args
25 from osh import sh_expr_eval
26 from osh.sh_expr_eval import ArithEvaluator
27 from osh.sh_expr_eval import BoolEvaluator
28 from osh import word_eval
29 from osh import cmd_eval
30 from osh import prompt
31 from ysh import expr_eval
32
33_ = log
34
35
36class ControlFlow(Exception):
37 """Internal exception for control flow.
38
39 Used by CommandEvaluator and 'source' builtin
40
41 break and continue are caught by loops, return is caught by functions.
42 """
43 pass
44
45
46class IntControlFlow(Exception):
47
48 def __init__(self, keyword_id, keyword_str, keyword_loc, arg):
49 # type: (Id_t, str, loc_t, int) -> None
50 """
51 Args:
52 token: the keyword token
53 arg: exit code to 'return', or number of levels to break/continue
54 """
55 self.keyword_id = keyword_id
56 self.keyword_str = keyword_str
57 self.keyword_loc = keyword_loc
58 self.arg = arg
59
60 def Keyword(self):
61 # type: () -> str
62 return self.keyword_str
63
64 def Location(self):
65 # type: () -> loc_t
66 return self.keyword_loc
67
68 def IsReturn(self):
69 # type: () -> bool
70 return self.keyword_id == Id.ControlFlow_Return
71
72 def IsBreak(self):
73 # type: () -> bool
74 return self.keyword_id == Id.ControlFlow_Break
75
76 def IsContinue(self):
77 # type: () -> bool
78 return self.keyword_id == Id.ControlFlow_Continue
79
80 def StatusCode(self):
81 # type: () -> int
82 assert self.IsReturn()
83 # All shells except dash do this truncation.
84 # turn 257 into 1, and -1 into 255.
85 return self.arg & 0xff
86
87 def HandleLoop(self):
88 # type: () -> flow_t
89 """Mutates this exception and returns what the caller should do."""
90
91 if self.IsBreak():
92 self.arg -= 1
93 if self.arg == 0:
94 return flow_e.Break # caller should break out of loop
95
96 elif self.IsContinue():
97 self.arg -= 1
98 if self.arg == 0:
99 return flow_e.Nothing # do nothing to continue
100
101 # return / break 2 / continue 2 need to pop up more
102 return flow_e.Raise
103
104 def __repr__(self):
105 # type: () -> str
106 return '<IntControlFlow %s %s>' % (Id_str(self.keyword_id), self.arg)
107
108
109class ValueControlFlow(Exception):
110
111 def __init__(self, token, value):
112 # type: (Token, value_t) -> None
113 """
114 Args:
115 token: the keyword token
116 value: value_t to 'return' from a function
117 """
118 self.token = token
119 self.value = value
120
121 def __repr__(self):
122 # type: () -> str
123 return '<ValueControlFlow %s %s>' % (self.token, self.value)
124
125
126def InitUnsafeArith(mem, word_ev, unsafe_arith):
127 # type: (state.Mem, word_eval.NormalWordEvaluator, sh_expr_eval.UnsafeArith) -> None
128 """Wire up circular dependencies for UnsafeArith."""
129 mem.unsafe_arith = unsafe_arith # for 'declare -n' nameref expansion of a[i]
130 word_ev.unsafe_arith = unsafe_arith # for ${!ref} expansion of a[i]
131
132
133def InitCircularDeps(
134 arith_ev, # type: ArithEvaluator
135 bool_ev, # type: BoolEvaluator
136 expr_ev, # type: expr_eval.ExprEvaluator
137 word_ev, # type: word_eval.NormalWordEvaluator
138 cmd_ev, # type: cmd_eval.CommandEvaluator
139 shell_ex, # type: _Executor
140 pure_ex, # type: _Executor
141 prompt_ev, # type: prompt.Evaluator
142 global_io, # type: Obj
143 tracer, # type: dev.Tracer
144):
145 # type: (...) -> None
146 """Wire up mutually recursive evaluators and runtime objects."""
147 arith_ev.word_ev = word_ev
148 bool_ev.word_ev = word_ev
149
150 if expr_ev: # for pure OSH
151 expr_ev.shell_ex = shell_ex
152 expr_ev.cmd_ev = cmd_ev
153 expr_ev.word_ev = word_ev
154
155 word_ev.arith_ev = arith_ev
156 word_ev.expr_ev = expr_ev
157 word_ev.prompt_ev = prompt_ev
158 word_ev.shell_ex = shell_ex
159
160 cmd_ev.shell_ex = shell_ex
161 cmd_ev.arith_ev = arith_ev
162 cmd_ev.bool_ev = bool_ev
163 cmd_ev.expr_ev = expr_ev
164 cmd_ev.word_ev = word_ev
165 cmd_ev.tracer = tracer
166
167 shell_ex.cmd_ev = cmd_ev
168 pure_ex.cmd_ev = cmd_ev
169
170 prompt_ev.word_ev = word_ev
171 prompt_ev.expr_ev = expr_ev
172 prompt_ev.global_io = global_io
173
174 tracer.word_ev = word_ev
175
176 arith_ev.CheckCircularDeps()
177 bool_ev.CheckCircularDeps()
178 if expr_ev:
179 expr_ev.CheckCircularDeps()
180 word_ev.CheckCircularDeps()
181 cmd_ev.CheckCircularDeps()
182 shell_ex.CheckCircularDeps()
183 pure_ex.CheckCircularDeps()
184 prompt_ev.CheckCircularDeps()
185 tracer.CheckCircularDeps()
186
187
188class _Executor(object):
189
190 def __init__(
191 self,
192 mem, # type: state.Mem
193 exec_opts, # type: optview.Exec
194 mutable_opts, # type: state.MutableOpts
195 procs, # type: state.Procs
196 hay_state, # type: hay_ysh.HayState
197 builtins, # type: Dict[int, _Builtin]
198 tracer, # type: dev.Tracer
199 errfmt # type: ui.ErrorFormatter
200 ):
201 self.mem = mem
202 self.exec_opts = exec_opts
203 self.mutable_opts = mutable_opts # for IsDisabled(), not mutating
204 self.procs = procs
205 self.hay_state = hay_state
206 self.builtins = builtins
207 self.tracer = tracer
208 self.errfmt = errfmt
209
210 # Not a constructor argument
211 self.cmd_ev = None # type: cmd_eval.CommandEvaluator
212
213 def CheckCircularDeps(self):
214 # type: () -> None
215 assert self.cmd_ev is not None
216
217 def RunSimpleCommand(self, cmd_val, cmd_st, run_flags):
218 # type: (cmd_value.Argv, CommandStatus, int) -> int
219 """Shared between ShellExecutor and PureExecutor"""
220 if len(cmd_val.arg_locs):
221 arg0_loc = cmd_val.arg_locs[0] # type: loc_t
222 else:
223 arg0_loc = loc.Missing
224
225 argv = cmd_val.argv
226 # This happens when you write "$@" but have no arguments.
227 if len(argv) == 0:
228 if self.exec_opts.strict_argv():
229 e_die("Command evaluated to an empty argv array", arg0_loc)
230 else:
231 return 0 # do nothing
232
233 return self._RunSimpleCommand(argv[0], arg0_loc, cmd_val, cmd_st,
234 run_flags)
235
236 def _RunSimpleCommand(self, arg0, arg0_loc, cmd_val, cmd_st, run_flags):
237 # type: (str, loc_t, cmd_value.Argv, CommandStatus, int) -> int
238 raise NotImplementedError()
239
240 def RunBuiltin(self, builtin_id, cmd_val):
241 # type: (int, cmd_value.Argv) -> int
242 """Run a builtin.
243
244 Also called by the 'builtin' builtin, in builtin/meta_oils.py
245 """
246 self.tracer.OnBuiltin(builtin_id, cmd_val.argv)
247
248 builtin_proc = self.builtins[builtin_id]
249
250 return self._RunBuiltinProc(builtin_proc, cmd_val)
251
252 def _RunBuiltinProc(self, builtin_proc, cmd_val):
253 # type: (_Builtin, cmd_value.Argv) -> int
254
255 io_errors = [] # type: List[error.IOError_OSError]
256 with ctx_FlushStdout(io_errors):
257 # note: could be second word, like 'builtin read'
258 with ui.ctx_Location(self.errfmt, cmd_val.arg_locs[0]):
259 try:
260 status = builtin_proc.Run(cmd_val)
261 assert isinstance(status, int)
262 except (IOError, OSError) as e:
263 self.errfmt.PrintMessage(
264 '%s builtin I/O error: %s' %
265 (cmd_val.argv[0], pyutil.strerror(e)),
266 cmd_val.arg_locs[0])
267 return 1
268 except error.Usage as e:
269 arg0 = cmd_val.argv[0]
270 # e.g. 'type' doesn't accept flag '-x'
271 self.errfmt.PrefixPrint(e.msg, '%r ' % arg0, e.location)
272 return 2 # consistent error code for usage error
273
274 if len(io_errors): # e.g. disk full, ulimit
275 self.errfmt.PrintMessage(
276 '%s builtin I/O error: %s' %
277 (cmd_val.argv[0], pyutil.strerror(io_errors[0])),
278 cmd_val.arg_locs[0])
279 return 1
280
281 return status
282
283 def _RunInvokable(self, proc_val, self_obj, arg0_loc, cmd_val):
284 # type: (value_t, Optional[Obj], loc_t, cmd_value.Argv) -> int
285
286 cmd_val.self_obj = self_obj # MAYBE bind self
287
288 if self.exec_opts.strict_errexit():
289 disabled_tok = self.mutable_opts.ErrExitDisabledToken()
290 if disabled_tok:
291 self.errfmt.Print_('errexit was disabled for this construct',
292 disabled_tok)
293 self.errfmt.StderrLine('')
294 e_die(
295 "Can't run functions or procs while errexit is disabled (OILS-ERR-301)",
296 arg0_loc)
297
298 with tagswitch(proc_val) as case:
299 if case(value_e.BuiltinProc):
300 # Handle the special case of the BUILTIN proc
301 # module_ysh.ModuleInvoke, which is returned on the Obj
302 # created by 'use util.ysh'
303 builtin_proc = cast(value.BuiltinProc, proc_val)
304 b = cast(_Builtin, builtin_proc.builtin)
305 status = self._RunBuiltinProc(b, cmd_val)
306
307 elif case(value_e.Proc):
308 proc = cast(value.Proc, proc_val)
309 with dev.ctx_Tracer(self.tracer, 'proc', cmd_val.argv):
310 # NOTE: Functions could call 'exit 42' directly, etc.
311 status = self.cmd_ev.RunProc(proc, cmd_val)
312
313 else:
314 # GetInvokable() should only return 1 of 2 things
315 raise AssertionError()
316
317 return status
318
319 def RunBackgroundJob(self, node):
320 # type: (command_t) -> int
321 return 0
322
323 def RunPipeline(self, node, status_out):
324 # type: (command.Pipeline, CommandStatus) -> None
325 pass
326
327 def RunSubshell(self, node):
328 # type: (command_t) -> int
329 return 0
330
331 def CaptureStdout(self, node):
332 # type: (command_t) -> Tuple[int, str]
333 return 0, ''
334
335 def Capture3(self, node):
336 # type: (command_t) -> Tuple[int, str, str]
337 return 0, '', ''
338
339 def RunCommandSub(self, cs_part):
340 # type: (CommandSub) -> str
341 return ''
342
343 def RunProcessSub(self, cs_part):
344 # type: (CommandSub) -> str
345 return ''
346
347 def PushRedirects(self, redirects, err_out):
348 # type: (List[RedirValue], List[error.IOError_OSError]) -> None
349 pass
350
351 def PopRedirects(self, num_redirects, err_out):
352 # type: (int, List[error.IOError_OSError]) -> None
353 pass
354
355 def PushProcessSub(self):
356 # type: () -> None
357 pass
358
359 def PopProcessSub(self, compound_st):
360 # type: (StatusArray) -> None
361 pass
362
363
364#
365# Abstract base classes
366#
367
368
369class _AssignBuiltin(object):
370 """Interface for assignment builtins."""
371
372 def __init__(self):
373 # type: () -> None
374 """Empty constructor for mycpp."""
375 pass
376
377 def Run(self, cmd_val):
378 # type: (cmd_value.Assign) -> int
379 raise NotImplementedError()
380
381
382class _Builtin(object):
383 """All builtins except 'command' obey this interface.
384
385 Assignment builtins use cmd_value.Assign; others use cmd_value.Argv.
386 """
387
388 def __init__(self):
389 # type: () -> None
390 """Empty constructor for mycpp."""
391 pass
392
393 def Run(self, cmd_val):
394 # type: (cmd_value.Argv) -> int
395 raise NotImplementedError()
396
397
398class _Callable(object):
399 """Interface for functions in the runtime."""
400
401 def __init__(self):
402 # type: () -> None
403 """Empty constructor for mycpp."""
404 pass
405
406 def Call(self, args):
407 # type: (typed_args.Reader) -> value_t
408 raise NotImplementedError()
409
410
411class ctx_MaybePure(object):
412 """Enforce purity of the shell interpreter
413
414 Use this for:
415
416 --eval-pure
417 func - pure functions
418 eval() evalToDict() - builtin pure functions, not methods
419 """
420
421 def __init__(
422 self,
423 pure_ex, # type: Optional[_Executor]
424 cmd_ev, # type: cmd_eval.CommandEvaluator
425 ):
426 # type: (...) -> None
427 self.pure_ex = pure_ex
428 if not pure_ex:
429 return # do nothing
430
431 word_ev = cmd_ev.word_ev
432 expr_ev = cmd_ev.expr_ev
433
434 # Save the Shell Executor
435 self.saved = cmd_ev.shell_ex
436 assert self.saved is word_ev.shell_ex
437 assert self.saved is expr_ev.shell_ex
438
439 # Patch evaluators to use the Pure Executor
440 cmd_ev.shell_ex = pure_ex
441 word_ev.shell_ex = pure_ex
442 expr_ev.shell_ex = pure_ex
443
444 self.cmd_ev = cmd_ev
445
446 def __enter__(self):
447 # type: () -> None
448 pass
449
450 def __exit__(self, type, value, traceback):
451 # type: (Any, Any, Any) -> None
452 if not self.pure_ex:
453 return # do nothing
454
455 # Unpatch the evaluators
456 self.cmd_ev.shell_ex = self.saved
457 self.cmd_ev.word_ev.shell_ex = self.saved
458 self.cmd_ev.expr_ev.shell_ex = self.saved
459
460
461class ctx_Redirect(object):
462 """For closing files.
463
464 This is asymmetric because if PushRedirects fails, then we don't execute
465 the command at all.
466
467 Example:
468 { seq 3 > foo.txt; echo 4; } > bar.txt
469 """
470
471 def __init__(self, shell_ex, num_redirects, err_out):
472 # type: (_Executor, int, List[error.IOError_OSError]) -> None
473 self.shell_ex = shell_ex
474 self.num_redirects = num_redirects
475 self.err_out = err_out
476
477 def __enter__(self):
478 # type: () -> None
479 pass
480
481 def __exit__(self, type, value, traceback):
482 # type: (Any, Any, Any) -> None
483 self.shell_ex.PopRedirects(self.num_redirects, self.err_out)
484
485
486class ctx_ProcessSub(object):
487 """For waiting on processes started during word evaluation.
488
489 Example:
490 diff <(seq 3) <(seq 4) > >(tac)
491 """
492
493 def __init__(self, shell_ex, process_sub_status):
494 # type: (_Executor, StatusArray) -> None
495 shell_ex.PushProcessSub()
496 self.shell_ex = shell_ex
497 self.process_sub_status = process_sub_status
498
499 def __enter__(self):
500 # type: () -> None
501 pass
502
503 def __exit__(self, type, value, traceback):
504 # type: (Any, Any, Any) -> None
505
506 # Wait and return array to set _process_sub_status
507 self.shell_ex.PopProcessSub(self.process_sub_status)
508
509
510class ctx_FlushStdout(object):
511
512 def __init__(self, err_out):
513 # type: (List[error.IOError_OSError]) -> None
514 self.err_out = err_out
515
516 def __enter__(self):
517 # type: () -> None
518 pass
519
520 def __exit__(self, type, value, traceback):
521 # type: (Any, Any, Any) -> None
522
523 # Can't raise exception in destructor! So we append it to out param.
524 err = pyos.FlushStdout()
525 if err is not None:
526 self.err_out.append(err)