1 | """vm.py: Library for executing shell."""
|
2 | from __future__ import print_function
|
3 |
|
4 | from _devbuild.gen.id_kind_asdl import Id, Id_t, Id_str
|
5 | from _devbuild.gen.runtime_asdl import (CommandStatus, StatusArray, flow_e,
|
6 | flow_t)
|
7 | from _devbuild.gen.syntax_asdl import Token, loc, loc_t
|
8 | from _devbuild.gen.value_asdl import value, value_e, value_t, Obj
|
9 | from core import dev
|
10 | from core import error
|
11 | from core.error import e_die
|
12 | from core import pyos
|
13 | from core import pyutil
|
14 | from display import ui
|
15 | from mycpp.mylib import log, tagswitch
|
16 |
|
17 | from typing import List, Dict, Tuple, Optional, Any, cast, TYPE_CHECKING
|
18 | if 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 |
|
36 | class 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 |
|
46 | class 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 |
|
109 | class 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 |
|
126 | def 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 |
|
133 | def 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 |
|
188 | class _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 |
|
371 | class _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 |
|
384 | class _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 |
|
400 | class _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 |
|
413 | class 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 |
|
463 | class 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 |
|
488 | class 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 |
|
512 | class 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)
|