OILS / core / executor.py View on Github | oilshell.org

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