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

534 lines, 300 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 internals, # type: Dict[int, _Builtin]
199 tracer, # type: dev.Tracer
200 errfmt # type: ui.ErrorFormatter
201 ):
202 self.mem = mem
203 self.exec_opts = exec_opts
204 self.mutable_opts = mutable_opts # for IsDisabled(), not mutating
205 self.procs = procs
206 self.hay_state = hay_state
207 self.builtins = builtins
208 self.internals = internals
209 self.tracer = tracer
210 self.errfmt = errfmt
211
212 # Not a constructor argument
213 self.cmd_ev = None # type: cmd_eval.CommandEvaluator
214
215 def CheckCircularDeps(self):
216 # type: () -> None
217 assert self.cmd_ev is not None
218
219 def RunSimpleCommand(self, cmd_val, cmd_st, run_flags):
220 # type: (cmd_value.Argv, CommandStatus, int) -> int
221 """Shared between ShellExecutor and PureExecutor"""
222 if len(cmd_val.arg_locs):
223 arg0_loc = cmd_val.arg_locs[0] # type: loc_t
224 else:
225 arg0_loc = loc.Missing
226
227 argv = cmd_val.argv
228 # This happens when you write "$@" but have no arguments.
229 if len(argv) == 0:
230 if self.exec_opts.strict_argv():
231 e_die("Command evaluated to an empty argv array", arg0_loc)
232 else:
233 return 0 # do nothing
234
235 return self._RunSimpleCommand(argv[0], arg0_loc, cmd_val, cmd_st,
236 run_flags)
237
238 def _RunSimpleCommand(self, arg0, arg0_loc, cmd_val, cmd_st, run_flags):
239 # type: (str, loc_t, cmd_value.Argv, CommandStatus, int) -> int
240 raise NotImplementedError()
241
242 def RunBuiltin(self, builtin_id, cmd_val):
243 # type: (int, cmd_value.Argv) -> int
244 """Run a builtin.
245
246 Also called by the 'builtin' builtin, in builtin/meta_oils.py
247 """
248 self.tracer.OnBuiltin(builtin_id, cmd_val.argv)
249 builtin_proc = self.builtins[builtin_id]
250 return self._RunBuiltinProc(builtin_proc, cmd_val)
251
252 def RunInternal(self, builtin_id, cmd_val):
253 # type: (int, cmd_value.Argv) -> int
254 """Run an internal command like 'sleep'
255 """
256 self.tracer.OnBuiltin(builtin_id, cmd_val.argv)
257 builtin_proc = self.internals[builtin_id]
258 return self._RunBuiltinProc(builtin_proc, cmd_val)
259
260 def _RunBuiltinProc(self, builtin_proc, cmd_val):
261 # type: (_Builtin, cmd_value.Argv) -> int
262
263 io_errors = [] # type: List[error.IOError_OSError]
264 with ctx_FlushStdout(io_errors):
265 # note: could be second word, like 'builtin read'
266 with ui.ctx_Location(self.errfmt, cmd_val.arg_locs[0]):
267 try:
268 status = builtin_proc.Run(cmd_val)
269 assert isinstance(status, int)
270 except (IOError, OSError) as e:
271 self.errfmt.PrintMessage(
272 '%s builtin I/O error: %s' %
273 (cmd_val.argv[0], pyutil.strerror(e)),
274 cmd_val.arg_locs[0])
275 return 1
276 except error.Usage as e:
277 arg0 = cmd_val.argv[0]
278 # e.g. 'type' doesn't accept flag '-x'
279 self.errfmt.PrefixPrint(e.msg, '%r ' % arg0, e.location)
280 return 2 # consistent error code for usage error
281
282 if len(io_errors): # e.g. disk full, ulimit
283 self.errfmt.PrintMessage(
284 '%s builtin I/O error: %s' %
285 (cmd_val.argv[0], pyutil.strerror(io_errors[0])),
286 cmd_val.arg_locs[0])
287 return 1
288
289 return status
290
291 def _RunInvokable(self, proc_val, self_obj, arg0_loc, cmd_val):
292 # type: (value_t, Optional[Obj], loc_t, cmd_value.Argv) -> int
293
294 cmd_val.self_obj = self_obj # MAYBE bind self
295
296 if self.exec_opts.strict_errexit():
297 disabled_tok = self.mutable_opts.ErrExitDisabledToken()
298 if disabled_tok:
299 self.errfmt.Print_('errexit was disabled for this construct',
300 disabled_tok)
301 self.errfmt.StderrLine('')
302 e_die(
303 "Can't run functions or procs while errexit is disabled (OILS-ERR-301)",
304 arg0_loc)
305
306 with tagswitch(proc_val) as case:
307 if case(value_e.BuiltinProc):
308 # Handle the special case of the BUILTIN proc
309 # module_ysh.ModuleInvoke, which is returned on the Obj
310 # created by 'use util.ysh'
311 builtin_proc = cast(value.BuiltinProc, proc_val)
312 b = cast(_Builtin, builtin_proc.builtin)
313 status = self._RunBuiltinProc(b, cmd_val)
314
315 elif case(value_e.Proc):
316 proc = cast(value.Proc, proc_val)
317 with dev.ctx_Tracer(self.tracer, 'proc', cmd_val.argv):
318 # NOTE: Functions could call 'exit 42' directly, etc.
319 status = self.cmd_ev.RunProc(proc, cmd_val)
320
321 else:
322 # GetInvokable() should only return 1 of 2 things
323 raise AssertionError()
324
325 return status
326
327 def RunBackgroundJob(self, node):
328 # type: (command_t) -> int
329 return 0
330
331 def RunPipeline(self, node, status_out):
332 # type: (command.Pipeline, CommandStatus) -> None
333 pass
334
335 def RunSubshell(self, node):
336 # type: (command_t) -> int
337 return 0
338
339 def CaptureStdout(self, node):
340 # type: (command_t) -> Tuple[int, str]
341 return 0, ''
342
343 def Capture3(self, node):
344 # type: (command_t) -> Tuple[int, str, str]
345 return 0, '', ''
346
347 def RunCommandSub(self, cs_part):
348 # type: (CommandSub) -> str
349 return ''
350
351 def RunProcessSub(self, cs_part):
352 # type: (CommandSub) -> str
353 return ''
354
355 def PushRedirects(self, redirects, err_out):
356 # type: (List[RedirValue], List[error.IOError_OSError]) -> None
357 pass
358
359 def PopRedirects(self, num_redirects, err_out):
360 # type: (int, List[error.IOError_OSError]) -> None
361 pass
362
363 def PushProcessSub(self):
364 # type: () -> None
365 pass
366
367 def PopProcessSub(self, compound_st):
368 # type: (StatusArray) -> None
369 pass
370
371
372#
373# Abstract base classes
374#
375
376
377class _AssignBuiltin(object):
378 """Interface for assignment builtins."""
379
380 def __init__(self):
381 # type: () -> None
382 """Empty constructor for mycpp."""
383 pass
384
385 def Run(self, cmd_val):
386 # type: (cmd_value.Assign) -> int
387 raise NotImplementedError()
388
389
390class _Builtin(object):
391 """All builtins except 'command' obey this interface.
392
393 Assignment builtins use cmd_value.Assign; others use cmd_value.Argv.
394 """
395
396 def __init__(self):
397 # type: () -> None
398 """Empty constructor for mycpp."""
399 pass
400
401 def Run(self, cmd_val):
402 # type: (cmd_value.Argv) -> int
403 raise NotImplementedError()
404
405
406class _Callable(object):
407 """Interface for functions in the runtime."""
408
409 def __init__(self):
410 # type: () -> None
411 """Empty constructor for mycpp."""
412 pass
413
414 def Call(self, args):
415 # type: (typed_args.Reader) -> value_t
416 raise NotImplementedError()
417
418
419class ctx_MaybePure(object):
420 """Enforce purity of the shell interpreter
421
422 Use this for:
423
424 --eval-pure
425 func - pure functions
426 eval() evalToDict() - builtin pure functions, not methods
427 """
428
429 def __init__(
430 self,
431 pure_ex, # type: Optional[_Executor]
432 cmd_ev, # type: cmd_eval.CommandEvaluator
433 ):
434 # type: (...) -> None
435 self.pure_ex = pure_ex
436 if not pure_ex:
437 return # do nothing
438
439 word_ev = cmd_ev.word_ev
440 expr_ev = cmd_ev.expr_ev
441
442 # Save the Shell Executor
443 self.saved = cmd_ev.shell_ex
444 assert self.saved is word_ev.shell_ex
445 assert self.saved is expr_ev.shell_ex
446
447 # Patch evaluators to use the Pure Executor
448 cmd_ev.shell_ex = pure_ex
449 word_ev.shell_ex = pure_ex
450 expr_ev.shell_ex = pure_ex
451
452 self.cmd_ev = cmd_ev
453
454 def __enter__(self):
455 # type: () -> None
456 pass
457
458 def __exit__(self, type, value, traceback):
459 # type: (Any, Any, Any) -> None
460 if not self.pure_ex:
461 return
462
463 # Unpatch the evaluators
464 self.cmd_ev.shell_ex = self.saved
465 self.cmd_ev.word_ev.shell_ex = self.saved
466 self.cmd_ev.expr_ev.shell_ex = self.saved
467
468
469class ctx_Redirect(object):
470 """For closing files.
471
472 This is asymmetric because if PushRedirects fails, then we don't execute
473 the command at all.
474
475 Example:
476 { seq 3 > foo.txt; echo 4; } > bar.txt
477 """
478
479 def __init__(self, shell_ex, num_redirects, err_out):
480 # type: (_Executor, int, List[error.IOError_OSError]) -> None
481 self.shell_ex = shell_ex
482 self.num_redirects = num_redirects
483 self.err_out = err_out
484
485 def __enter__(self):
486 # type: () -> None
487 pass
488
489 def __exit__(self, type, value, traceback):
490 # type: (Any, Any, Any) -> None
491 self.shell_ex.PopRedirects(self.num_redirects, self.err_out)
492
493
494class ctx_ProcessSub(object):
495 """For waiting on processes started during word evaluation.
496
497 Example:
498 diff <(seq 3) <(seq 4) > >(tac)
499 """
500
501 def __init__(self, shell_ex, process_sub_status):
502 # type: (_Executor, StatusArray) -> None
503 shell_ex.PushProcessSub()
504 self.shell_ex = shell_ex
505 self.process_sub_status = process_sub_status
506
507 def __enter__(self):
508 # type: () -> None
509 pass
510
511 def __exit__(self, type, value, traceback):
512 # type: (Any, Any, Any) -> None
513
514 # Wait and return array to set _process_sub_status
515 self.shell_ex.PopProcessSub(self.process_sub_status)
516
517
518class ctx_FlushStdout(object):
519
520 def __init__(self, err_out):
521 # type: (List[error.IOError_OSError]) -> None
522 self.err_out = err_out
523
524 def __enter__(self):
525 # type: () -> None
526 pass
527
528 def __exit__(self, type, value, traceback):
529 # type: (Any, Any, Any) -> None
530
531 # Can't raise exception in destructor! So we append it to out param.
532 err = pyos.FlushStdout()
533 if err is not None:
534 self.err_out.append(err)