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

858 lines, 466 significant
1"""executor.py."""
2from __future__ import print_function
3
4from errno import EINTR
5
6from _devbuild.gen.id_kind_asdl import Id
7from _devbuild.gen.option_asdl import builtin_i
8from _devbuild.gen.runtime_asdl import RedirValue, trace
9from _devbuild.gen.syntax_asdl import (
10 command,
11 command_e,
12 CommandSub,
13 CompoundWord,
14 loc,
15 loc_t,
16)
17from _devbuild.gen.value_asdl import value, value_e
18from builtin import hay_ysh
19from core import dev
20from core import error
21from core import process
22from core.error import e_die, e_die_status
23from core import pyos
24from core import pyutil
25from core import state
26from core import vm
27from display import ui
28from frontend import consts
29from frontend import lexer
30from mycpp import mylib
31from mycpp.mylib import log, print_stderr, tagswitch
32from pylib import os_path
33from pylib import path_stat
34
35import posix_ as posix
36from posix_ import X_OK # translated directly to C macro
37
38from typing import cast, Dict, List, Tuple, Optional, TYPE_CHECKING
39if TYPE_CHECKING:
40 from _devbuild.gen.runtime_asdl import (cmd_value, CommandStatus,
41 StatusArray)
42 from _devbuild.gen.syntax_asdl import command_t
43 from builtin import trap_osh
44 from core import optview
45
46_ = log
47
48
49def LookupExecutable(name, path_dirs, exec_required=True):
50 # type: (str, List[str], bool) -> Optional[str]
51 """
52 Returns either
53 - the name if it's a relative path that exists
54 - the executable name resolved against path_dirs
55 - None if not found
56 """
57 if len(name) == 0: # special case for "$(true)"
58 return None
59
60 if '/' in name:
61 return name if path_stat.exists(name) else None
62
63 for path_dir in path_dirs:
64 full_path = os_path.join(path_dir, name)
65 if exec_required:
66 found = posix.access(full_path, X_OK)
67 else:
68 found = path_stat.exists(full_path)
69
70 if found:
71 return full_path
72
73 return None
74
75
76class SearchPath(object):
77 """For looking up files in $PATH or ENV.PATH"""
78
79 def __init__(self, mem, exec_opts):
80 # type: (state.Mem, optview.Exec) -> None
81 self.mem = mem
82 # TODO: remove exec_opts
83 self.cache = {} # type: Dict[str, str]
84
85 def _GetPath(self):
86 # type: () -> List[str]
87
88 # In YSH, we read from ENV.PATH
89 s = self.mem.env_config.Get('PATH')
90 if s is None:
91 return [] # treat as empty path
92
93 # TODO: Could cache this to avoid split() allocating all the time.
94 return s.split(':')
95
96 def LookupOne(self, name, exec_required=True):
97 # type: (str, bool) -> Optional[str]
98 """
99 Returns the path itself (if relative path), the resolved path, or None.
100 """
101 return LookupExecutable(name,
102 self._GetPath(),
103 exec_required=exec_required)
104
105 def LookupReflect(self, name, do_all):
106 # type: (str, bool) -> List[str]
107 """
108 Like LookupOne(), with an option for 'type -a' to return all paths.
109 """
110 if len(name) == 0: # special case for "$(true)"
111 return []
112
113 if '/' in name:
114 if path_stat.exists(name):
115 return [name]
116 else:
117 return []
118
119 results = [] # type: List[str]
120 for path_dir in self._GetPath():
121 full_path = os_path.join(path_dir, name)
122 if path_stat.exists(full_path):
123 results.append(full_path)
124 if not do_all:
125 return results
126
127 return results
128
129 def CachedLookup(self, name):
130 # type: (str) -> Optional[str]
131 #log('name %r', name)
132 if name in self.cache:
133 return self.cache[name]
134
135 full_path = self.LookupOne(name)
136 if full_path is not None:
137 self.cache[name] = full_path
138 return full_path
139
140 def MaybeRemoveEntry(self, name):
141 # type: (str) -> None
142 """When the file system changes."""
143 mylib.dict_erase(self.cache, name)
144
145 def ClearCache(self):
146 # type: () -> None
147 """For hash -r."""
148 self.cache.clear()
149
150 def CachedCommands(self):
151 # type: () -> List[str]
152 return self.cache.values()
153
154
155class _ProcessSubFrame(object):
156 """To keep track of diff <(cat 1) <(cat 2) > >(tac)"""
157
158 def __init__(self):
159 # type: () -> None
160
161 # These objects appear unconditionally in the main loop, and aren't
162 # commonly used, so we manually optimize [] into None.
163
164 self._to_wait = [] # type: List[process.Process]
165 self._to_close = [] # type: List[int] # file descriptors
166 self._locs = [] # type: List[loc_t]
167 self._modified = False
168
169 def WasModified(self):
170 # type: () -> bool
171 return self._modified
172
173 def Append(self, p, fd, status_loc):
174 # type: (process.Process, int, loc_t) -> None
175 self._modified = True
176
177 self._to_wait.append(p)
178 self._to_close.append(fd)
179 self._locs.append(status_loc)
180
181 def MaybeWaitOnProcessSubs(self, waiter, status_array):
182 # type: (process.Waiter, StatusArray) -> None
183
184 # Wait in the same order that they were evaluated. That seems fine.
185 for fd in self._to_close:
186 posix.close(fd)
187
188 codes = [] # type: List[int]
189 locs = [] # type: List[loc_t]
190 for i, p in enumerate(self._to_wait):
191 #log('waiting for %s', p)
192 st = p.Wait(waiter)
193 codes.append(st)
194 locs.append(self._locs[i])
195
196 status_array.codes = codes
197 status_array.locs = locs
198
199
200# Big flags for RunSimpleCommand
201IS_LAST_CMD = 1 << 1
202NO_CALL_PROCS = 1 << 2 # command ls suppresses function lookup
203USE_DEFAULT_PATH = 1 << 3 # for command -p ls changes the path
204
205# Copied from var.c in dash
206DEFAULT_PATH = [
207 '/usr/local/sbin', '/usr/local/bin', '/usr/sbin', '/usr/bin', '/sbin',
208 '/bin'
209]
210
211
212class ShellExecutor(vm._Executor):
213 """An executor combined with the OSH language evaluators in osh/ to create
214 a shell interpreter."""
215
216 def __init__(
217 self,
218 mem, # type: state.Mem
219 exec_opts, # type: optview.Exec
220 mutable_opts, # type: state.MutableOpts
221 procs, # type: state.Procs
222 hay_state, # type: hay_ysh.HayState
223 builtins, # type: Dict[int, vm._Builtin]
224 search_path, # type: SearchPath
225 ext_prog, # type: process.ExternalProgram
226 waiter, # type: process.Waiter
227 tracer, # type: dev.Tracer
228 job_control, # type: process.JobControl
229 job_list, # type: process.JobList
230 fd_state, # type: process.FdState
231 trap_state, # type: trap_osh.TrapState
232 errfmt # type: ui.ErrorFormatter
233 ):
234 # type: (...) -> None
235 vm._Executor.__init__(self)
236 self.mem = mem
237 self.exec_opts = exec_opts
238 self.mutable_opts = mutable_opts # for IsDisabled(), not mutating
239 self.procs = procs
240 self.hay_state = hay_state
241 self.builtins = builtins
242 self.search_path = search_path
243 self.ext_prog = ext_prog
244 self.waiter = waiter
245 self.tracer = tracer
246 self.multi_trace = tracer.multi_trace
247 self.job_control = job_control
248 # sleep 5 & puts a (PID, job#) entry here. And then "jobs" displays it.
249 self.job_list = job_list
250 self.fd_state = fd_state
251 self.trap_state = trap_state
252 self.errfmt = errfmt
253 self.process_sub_stack = [] # type: List[_ProcessSubFrame]
254 self.clean_frame_pool = [] # type: List[_ProcessSubFrame]
255
256 # When starting a pipeline in the foreground, we need to pass a handle to it
257 # through the evaluation of the last node back to ourselves for execution.
258 # We use this handle to make sure any processes forked for the last part of
259 # the pipeline are placed into the same process group as the rest of the
260 # pipeline. Since there is, by design, only ever one foreground pipeline and
261 # any pipelines started within subshells run in their parent's process
262 # group, we only need one pointer here, not some collection.
263 self.fg_pipeline = None # type: Optional[process.Pipeline]
264
265 def CheckCircularDeps(self):
266 # type: () -> None
267 assert self.cmd_ev is not None
268
269 def _MakeProcess(self, node, inherit_errexit, inherit_errtrace):
270 # type: (command_t, bool, bool) -> process.Process
271 """Assume we will run the node in another process.
272
273 Return a process.
274 """
275 UP_node = node
276 if node.tag() == command_e.ControlFlow:
277 node = cast(command.ControlFlow, UP_node)
278 # Pipeline or subshells with control flow are invalid, e.g.:
279 # - break | less
280 # - continue | less
281 # - ( return )
282 # NOTE: This could be done at parse time too.
283 if node.keyword.id != Id.ControlFlow_Exit:
284 e_die(
285 'Invalid control flow %r in pipeline / subshell / background'
286 % lexer.TokenVal(node.keyword), node.keyword)
287
288 # NOTE: If ErrExit(), we could be verbose about subprogram errors? This
289 # only really matters when executing 'exit 42', because the child shell
290 # inherits errexit and will be verbose. Other notes:
291 #
292 # - We might want errors to fit on a single line so they don't get #
293 # interleaved.
294 # - We could turn the `exit` builtin into a error.FatalRuntime exception
295 # and get this check for "free".
296 thunk = process.SubProgramThunk(self.cmd_ev, node, self.trap_state,
297 self.multi_trace, inherit_errexit,
298 inherit_errtrace)
299 p = process.Process(thunk, self.job_control, self.job_list,
300 self.tracer)
301 return p
302
303 def RunBuiltin(self, builtin_id, cmd_val):
304 # type: (int, cmd_value.Argv) -> int
305 """Run a builtin.
306
307 Also called by the 'builtin' builtin.
308 """
309 self.tracer.OnBuiltin(builtin_id, cmd_val.argv)
310
311 builtin_proc = self.builtins[builtin_id]
312
313 return self.RunBuiltinProc(builtin_proc, cmd_val)
314
315 def RunBuiltinProc(self, builtin_proc, cmd_val):
316 # type: (vm._Builtin, cmd_value.Argv) -> int
317
318 io_errors = [] # type: List[error.IOError_OSError]
319 with vm.ctx_FlushStdout(io_errors):
320 # note: could be second word, like 'builtin read'
321 with ui.ctx_Location(self.errfmt, cmd_val.arg_locs[0]):
322 try:
323 status = builtin_proc.Run(cmd_val)
324 assert isinstance(status, int)
325 except (IOError, OSError) as e:
326 self.errfmt.PrintMessage(
327 '%s builtin I/O error: %s' %
328 (cmd_val.argv[0], pyutil.strerror(e)),
329 cmd_val.arg_locs[0])
330 return 1
331 except error.Usage as e:
332 arg0 = cmd_val.argv[0]
333 # e.g. 'type' doesn't accept flag '-x'
334 self.errfmt.PrefixPrint(e.msg, '%r ' % arg0, e.location)
335 return 2 # consistent error code for usage error
336
337 if len(io_errors): # e.g. disk full, ulimit
338 self.errfmt.PrintMessage(
339 '%s builtin I/O error: %s' %
340 (cmd_val.argv[0], pyutil.strerror(io_errors[0])),
341 cmd_val.arg_locs[0])
342 return 1
343
344 return status
345
346 def RunSimpleCommand(self, cmd_val, cmd_st, run_flags):
347 # type: (cmd_value.Argv, CommandStatus, int) -> int
348 """Run builtins, functions, external commands.
349
350 Possible variations:
351 - YSH might have different, simpler rules. No special builtins, etc.
352 - YSH might have OILS_PATH = :| /bin /usr/bin | or something.
353 - Interpreters might want to define all their own builtins.
354 """
355 argv = cmd_val.argv
356 if len(cmd_val.arg_locs):
357 arg0_loc = cmd_val.arg_locs[0] # type: loc_t
358 else:
359 arg0_loc = loc.Missing
360
361 # This happens when you write "$@" but have no arguments.
362 if len(argv) == 0:
363 if self.exec_opts.strict_argv():
364 e_die("Command evaluated to an empty argv array", arg0_loc)
365 else:
366 return 0 # status 0, or skip it?
367
368 arg0 = argv[0]
369
370 builtin_id = consts.LookupAssignBuiltin(arg0)
371 if builtin_id != consts.NO_INDEX:
372 # command readonly is disallowed, for technical reasons. Could relax it
373 # later.
374 self.errfmt.Print_("Can't run assignment builtin recursively",
375 arg0_loc)
376 return 1
377
378 builtin_id = consts.LookupSpecialBuiltin(arg0)
379 if builtin_id != consts.NO_INDEX:
380 cmd_st.show_code = True # this is a "leaf" for errors
381 status = self.RunBuiltin(builtin_id, cmd_val)
382 # TODO: Enable this and fix spec test failures.
383 # Also update _SPECIAL_BUILTINS in osh/builtin.py.
384 #if status != 0:
385 # e_die_status(status, 'special builtin failed')
386 return status
387
388 # Builtins like 'true' can be redefined as functions.
389 call_procs = not (run_flags & NO_CALL_PROCS)
390 if call_procs:
391 proc_val, self_obj = self.procs.GetInvokable(arg0)
392 cmd_val.self_obj = self_obj # MAYBE bind self
393
394 if proc_val is not None:
395 if self.exec_opts.strict_errexit():
396 disabled_tok = self.mutable_opts.ErrExitDisabledToken()
397 if disabled_tok:
398 self.errfmt.Print_(
399 'errexit was disabled for this construct',
400 disabled_tok)
401 self.errfmt.StderrLine('')
402 e_die(
403 "Can't run a proc while errexit is disabled. "
404 "Use 'try' or wrap it in a process with $0 myproc",
405 arg0_loc)
406
407 with tagswitch(proc_val) as case:
408 if case(value_e.BuiltinProc):
409 # Handle the special case of the BUILTIN proc
410 # module_ysh.ModuleInvoke, which is returned on the Obj
411 # created by 'use util.ysh'
412 builtin_proc = cast(value.BuiltinProc, proc_val)
413 b = cast(vm._Builtin, builtin_proc.builtin)
414 status = self.RunBuiltinProc(b, cmd_val)
415
416 elif case(value_e.Proc):
417 proc = cast(value.Proc, proc_val)
418 with dev.ctx_Tracer(self.tracer, 'proc', argv):
419 # NOTE: Functions could call 'exit 42' directly, etc.
420 status = self.cmd_ev.RunProc(proc, cmd_val)
421
422 else:
423 # GetInvokable() should only return 1 of 2 things
424 raise AssertionError()
425
426 return status
427
428 # Notes:
429 # - procs shadow hay names
430 # - hay names shadow normal builtins? Should we limit to CAPS or no?
431 if self.hay_state.Resolve(arg0):
432 return self.RunBuiltin(builtin_i.haynode, cmd_val)
433
434 builtin_id = consts.LookupNormalBuiltin(arg0)
435
436 if self.exec_opts._running_hay():
437 # Hay: limit the builtins that can be run
438 # - declare 'use dialect'
439 # - echo and write for debugging
440 # - no JSON?
441 if builtin_id in (builtin_i.haynode, builtin_i.use, builtin_i.echo,
442 builtin_i.write):
443 cmd_st.show_code = True # this is a "leaf" for errors
444 return self.RunBuiltin(builtin_id, cmd_val)
445
446 self.errfmt.Print_('Unknown command %r while running hay' % arg0,
447 arg0_loc)
448 return 127
449
450 if builtin_id != consts.NO_INDEX:
451 cmd_st.show_code = True # this is a "leaf" for errors
452 return self.RunBuiltin(builtin_id, cmd_val)
453
454 environ = self.mem.GetEnv() # Include temporary variables
455
456 if cmd_val.proc_args:
457 e_die(
458 '%r appears to be external. External commands don\'t accept typed args (OILS-ERR-200)'
459 % arg0, cmd_val.proc_args.typed_args.left)
460
461 # Resolve argv[0] BEFORE forking.
462 if run_flags & USE_DEFAULT_PATH:
463 argv0_path = LookupExecutable(arg0, DEFAULT_PATH)
464 else:
465 argv0_path = self.search_path.CachedLookup(arg0)
466 if argv0_path is None:
467 self.errfmt.Print_('%r not found (OILS-ERR-100)' % arg0, arg0_loc)
468 return 127
469
470 if self.trap_state.ThisProcessHasTraps():
471 do_fork = True
472 else:
473 do_fork = not cmd_val.is_last_cmd
474
475 # Normal case: ls /
476 if do_fork:
477 thunk = process.ExternalThunk(self.ext_prog, argv0_path, cmd_val,
478 environ)
479 p = process.Process(thunk, self.job_control, self.job_list,
480 self.tracer)
481
482 if self.job_control.Enabled():
483 if self.fg_pipeline is not None:
484 pgid = self.fg_pipeline.ProcessGroupId()
485 # If job control is enabled, this should be true
486 assert pgid != process.INVALID_PGID
487
488 change = process.SetPgid(pgid, self.tracer)
489 self.fg_pipeline = None # clear to avoid confusion in subshells
490 else:
491 change = process.SetPgid(process.OWN_LEADER, self.tracer)
492 p.AddStateChange(change)
493
494 status = p.RunProcess(self.waiter, trace.External(cmd_val.argv))
495
496 # this is close to a "leaf" for errors
497 # problem: permission denied EACCESS prints duplicate messages
498 # TODO: add message command 'ls' failed
499 cmd_st.show_code = True
500
501 return status
502
503 self.tracer.OnExec(cmd_val.argv)
504
505 # Already forked for pipeline: ls / | wc -l
506 self.ext_prog.Exec(argv0_path, cmd_val, environ) # NEVER RETURNS
507
508 raise AssertionError('for -Wreturn-type in C++')
509
510 def RunBackgroundJob(self, node):
511 # type: (command_t) -> int
512 """For & etc."""
513 # Special case for pipeline. There is some evidence here:
514 # https://www.gnu.org/software/libc/manual/html_node/Launching-Jobs.html#Launching-Jobs
515 #
516 # "You can either make all the processes in the process group be children
517 # of the shell process, or you can make one process in group be the
518 # ancestor of all the other processes in that group. The sample shell
519 # program presented in this chapter uses the first approach because it
520 # makes bookkeeping somewhat simpler."
521 UP_node = node
522
523 if UP_node.tag() == command_e.Pipeline:
524 node = cast(command.Pipeline, UP_node)
525 pi = process.Pipeline(self.exec_opts.sigpipe_status_ok(),
526 self.job_control, self.job_list, self.tracer)
527 for child in node.children:
528 p = self._MakeProcess(child, True, self.exec_opts.errtrace())
529 p.Init_ParentPipeline(pi)
530 pi.Add(p)
531
532 pi.StartPipeline(self.waiter)
533 pi.SetBackground()
534 last_pid = pi.LastPid()
535 self.mem.last_bg_pid = last_pid # for $!
536
537 job_id = self.job_list.AddJob(pi) # show in 'jobs' list
538
539 else:
540 # Problem: to get the 'set -b' behavior of immediate notifications, we
541 # have to register SIGCHLD. But then that introduces race conditions.
542 # If we haven't called Register yet, then we won't know who to notify.
543
544 p = self._MakeProcess(node, True, self.exec_opts.errtrace())
545 if self.job_control.Enabled():
546 p.AddStateChange(
547 process.SetPgid(process.OWN_LEADER, self.tracer))
548
549 p.SetBackground()
550 pid = p.StartProcess(trace.Fork)
551 self.mem.last_bg_pid = pid # for $!
552 job_id = self.job_list.AddJob(p) # show in 'jobs' list
553
554 if self.exec_opts.interactive():
555 # Print it like %1 to show it's a job
556 print_stderr('[%%%d] PID %d Started' %
557 (job_id, self.mem.last_bg_pid))
558
559 return 0
560
561 def RunPipeline(self, node, status_out):
562 # type: (command.Pipeline, CommandStatus) -> None
563
564 pi = process.Pipeline(self.exec_opts.sigpipe_status_ok(),
565 self.job_control, self.job_list, self.tracer)
566
567 # initialized with CommandStatus.CreateNull()
568 pipe_locs = [] # type: List[loc_t]
569
570 # First n-1 processes (which is empty when n == 1)
571 n = len(node.children)
572 for i in xrange(n - 1):
573 child = node.children[i]
574
575 # TODO: determine these locations at parse time?
576 pipe_locs.append(loc.Command(child))
577
578 p = self._MakeProcess(child, True, self.exec_opts.errtrace())
579 p.Init_ParentPipeline(pi)
580 pi.Add(p)
581
582 last_child = node.children[n - 1]
583 # Last piece of code is in THIS PROCESS. 'echo foo | read line; echo $line'
584 pi.AddLast((self.cmd_ev, last_child))
585 pipe_locs.append(loc.Command(last_child))
586
587 with dev.ctx_Tracer(self.tracer, 'pipeline', None):
588 pi.StartPipeline(self.waiter)
589 self.fg_pipeline = pi
590 status_out.pipe_status = pi.RunLastPart(self.waiter, self.fd_state)
591 self.fg_pipeline = None # clear in case we didn't end up forking
592
593 status_out.pipe_locs = pipe_locs
594
595 def RunSubshell(self, node):
596 # type: (command_t) -> int
597 p = self._MakeProcess(node, True, self.exec_opts.errtrace())
598 if self.job_control.Enabled():
599 p.AddStateChange(process.SetPgid(process.OWN_LEADER, self.tracer))
600
601 return p.RunProcess(self.waiter, trace.ForkWait)
602
603 def CaptureStdout(self, node):
604 # type: (command_t) -> Tuple[int, str]
605
606 p = self._MakeProcess(node, self.exec_opts.inherit_errexit(),
607 self.exec_opts.errtrace())
608 # Shell quirk: Command subs remain part of the shell's process group, so we
609 # don't use p.AddStateChange(process.SetPgid(...))
610
611 r, w = posix.pipe()
612 p.AddStateChange(process.StdoutToPipe(r, w))
613
614 p.StartProcess(trace.CommandSub)
615 #log('Command sub started %d', pid)
616
617 chunks = [] # type: List[str]
618 posix.close(w) # not going to write
619 while True:
620 n, err_num = pyos.Read(r, 4096, chunks)
621
622 if n < 0:
623 if err_num == EINTR:
624 pass # retry
625 else:
626 # Like the top level IOError handler
627 e_die_status(
628 2,
629 'Oils I/O error (read): %s' % posix.strerror(err_num))
630
631 elif n == 0: # EOF
632 break
633 posix.close(r)
634
635 status = p.Wait(self.waiter)
636 stdout_str = ''.join(chunks).rstrip('\n')
637
638 return status, stdout_str
639
640 def RunCommandSub(self, cs_part):
641 # type: (CommandSub) -> str
642
643 if not self.exec_opts._allow_command_sub():
644 # _allow_command_sub is used in two places. Only one of them turns
645 # off _allow_process_sub
646 if not self.exec_opts._allow_process_sub():
647 why = "status wouldn't be checked (strict_errexit)"
648 else:
649 why = 'eval_unsafe_arith is off'
650
651 e_die("Command subs not allowed here because %s" % why,
652 loc.WordPart(cs_part))
653
654 node = cs_part.child
655
656 # Hack for weird $(<file) construct
657 if node.tag() == command_e.Redirect:
658 redir_node = cast(command.Redirect, node)
659 # Detect '< file'
660 if (len(redir_node.redirects) == 1 and
661 redir_node.redirects[0].op.id == Id.Redir_Less and
662 redir_node.child.tag() == command_e.NoOp):
663
664 # Change it to __cat < file.
665 # TODO: could be 'internal cat' (issue #1013)
666 tok = lexer.DummyToken(Id.Lit_Chars, '__cat')
667 cat_word = CompoundWord([tok])
668
669 # Blame < because __cat has no location
670 blame_tok = redir_node.redirects[0].op
671 simple = command.Simple(blame_tok, [], [cat_word], None, None,
672 False)
673
674 # MUTATE redir node so it's like $(<file _cat)
675 redir_node.child = simple
676
677 status, stdout_str = self.CaptureStdout(node)
678
679 # OSH has the concept of aborting in the middle of a WORD. We're not
680 # waiting until the command is over!
681 if self.exec_opts.command_sub_errexit():
682 if status != 0:
683 msg = 'Command Sub exited with status %d' % status
684 raise error.ErrExit(status, msg, loc.WordPart(cs_part))
685
686 else:
687 # Set a flag so we check errexit at the same time as bash. Example:
688 #
689 # a=$(false)
690 # echo foo # no matter what comes here, the flag is reset
691 #
692 # Set ONLY until this command node has finished executing.
693
694 # HACK: move this
695 self.cmd_ev.check_command_sub_status = True
696 self.mem.SetLastStatus(status)
697
698 # Runtime errors test case: # $("echo foo > $@")
699 # Why rstrip()?
700 # https://unix.stackexchange.com/questions/17747/why-does-shell-command-substitution-gobble-up-a-trailing-newline-char
701 return stdout_str
702
703 def RunProcessSub(self, cs_part):
704 # type: (CommandSub) -> str
705 """Process sub creates a forks a process connected to a pipe.
706
707 The pipe is typically passed to another process via a /dev/fd/$FD path.
708
709 Life cycle of a process substitution:
710
711 1. Start with this code
712
713 diff <(seq 3) <(seq 4)
714
715 2. To evaluate the command line, we evaluate every word. The
716 NormalWordEvaluator this method, RunProcessSub(), which does 3 things:
717
718 a. Create a pipe(), getting r and w
719 b. Starts the seq process, which inherits r and w
720 It has a StdoutToPipe() redirect, which means that it dup2(w, 1)
721 and close(r)
722 c. Close the w FD, because neither the shell or 'diff' will write to it.
723 However we must retain 'r', because 'diff' hasn't opened /dev/fd yet!
724 d. We evaluate <(seq 3) to /dev/fd/$r, so "diff" can read from it
725
726 3. Now we're done evaluating every word, so we know the command line of
727 diff, which looks like
728
729 diff /dev/fd/64 /dev/fd/65
730
731 Those are the FDs for the read ends of the pipes we created.
732
733 4. diff inherits a copy of the read end of bot pipes. But it actually
734 calls open() both files passed as argv. (I think this is fine.)
735
736 5. wait() for the diff process.
737
738 6. The shell closes both the read ends of both pipes. Neither us or
739 'diffd' will read again.
740
741 7. The shell waits for both 'seq' processes.
742
743 Related:
744 shopt -s process_sub_fail
745 _process_sub_status
746 """
747 cs_loc = loc.WordPart(cs_part)
748
749 if not self.exec_opts._allow_process_sub():
750 e_die(
751 "Process subs not allowed here because status wouldn't be checked (strict_errexit)",
752 cs_loc)
753
754 p = self._MakeProcess(cs_part.child, True, self.exec_opts.errtrace())
755
756 r, w = posix.pipe()
757 #log('pipe = %d, %d', r, w)
758
759 op_id = cs_part.left_token.id
760 if op_id == Id.Left_ProcSubIn:
761 # Example: cat < <(head foo.txt)
762 #
763 # The head process should write its stdout to a pipe.
764 redir = process.StdoutToPipe(r,
765 w) # type: process.ChildStateChange
766
767 elif op_id == Id.Left_ProcSubOut:
768 # Example: head foo.txt > >(tac)
769 #
770 # The tac process should read its stdin from a pipe.
771
772 # Note: this example sometimes requires you to hit "enter" in bash and
773 # zsh. WHy?
774 redir = process.StdinFromPipe(r, w)
775
776 else:
777 raise AssertionError()
778
779 p.AddStateChange(redir)
780
781 if self.job_control.Enabled():
782 p.AddStateChange(process.SetPgid(process.OWN_LEADER, self.tracer))
783
784 # Fork, letting the child inherit the pipe file descriptors.
785 p.StartProcess(trace.ProcessSub)
786
787 ps_frame = self.process_sub_stack[-1]
788
789 # Note: bash never waits() on the process, but zsh does. The calling
790 # program needs to read() before we can wait, e.g.
791 # diff <(sort left.txt) <(sort right.txt)
792
793 # After forking, close the end of the pipe we're not using.
794 if op_id == Id.Left_ProcSubIn:
795 posix.close(w) # cat < <(head foo.txt)
796 ps_frame.Append(p, r, cs_loc) # close later
797 elif op_id == Id.Left_ProcSubOut:
798 posix.close(r)
799 #log('Left_ProcSubOut closed %d', r)
800 ps_frame.Append(p, w, cs_loc) # close later
801 else:
802 raise AssertionError()
803
804 # Is /dev Linux-specific?
805 if op_id == Id.Left_ProcSubIn:
806 return '/dev/fd/%d' % r
807
808 elif op_id == Id.Left_ProcSubOut:
809 return '/dev/fd/%d' % w
810
811 else:
812 raise AssertionError()
813
814 def PushRedirects(self, redirects, err_out):
815 # type: (List[RedirValue], List[error.IOError_OSError]) -> None
816 if len(redirects) == 0: # Optimized to avoid allocs
817 return
818 self.fd_state.Push(redirects, err_out)
819
820 def PopRedirects(self, num_redirects, err_out):
821 # type: (int, List[error.IOError_OSError]) -> None
822 if num_redirects == 0: # Optimized to avoid allocs
823 return
824 self.fd_state.Pop(err_out)
825
826 def PushProcessSub(self):
827 # type: () -> None
828 if len(self.clean_frame_pool):
829 # Optimized to avoid allocs
830 new_frame = self.clean_frame_pool.pop()
831 else:
832 new_frame = _ProcessSubFrame()
833 self.process_sub_stack.append(new_frame)
834
835 def PopProcessSub(self, compound_st):
836 # type: (StatusArray) -> None
837 """This method is called by a context manager, which means we always
838 wait() on the way out, which I think is the right thing.
839
840 We don't always set _process_sub_status, e.g. if some fatal
841 error occurs first, but we always wait.
842 """
843 frame = self.process_sub_stack.pop()
844 if frame.WasModified():
845 frame.MaybeWaitOnProcessSubs(self.waiter, compound_st)
846 else:
847 # Optimized to avoid allocs
848 self.clean_frame_pool.append(frame)
849
850 # Note: the 3 lists in _ProcessSubFrame are hot in our profiles. It would
851 # be nice to somehow "destroy" them here, rather than letting them become
852 # garbage that needs to be traced.
853
854 # The CommandEvaluator could have a ProcessSubStack, which supports Push(),
855 # Pop(), and Top() of VALUES rather than GC objects?
856
857
858# vim: sw=4