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

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