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

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