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

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