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

919 lines, 501 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 tracer, # type: dev.Tracer
224 errfmt # type: ui.ErrorFormatter
225 ):
226 vm._Executor.__init__(self, mem, exec_opts, mutable_opts, procs,
227 hay_state, builtins, tracer, errfmt)
228
229 def _RunSimpleCommand(self, arg0, arg0_loc, cmd_val, cmd_st, run_flags):
230 # type: (str, loc_t, cmd_value.Argv, CommandStatus, int) -> int
231
232 call_procs = not (run_flags & NO_CALL_PROCS)
233 if call_procs:
234 proc_val, self_obj = self.procs.GetInvokable(arg0)
235 if proc_val is not None:
236 return self._RunInvokable(proc_val, self_obj, arg0_loc,
237 cmd_val)
238
239 if self.hay_state.Resolve(arg0):
240 return self.RunBuiltin(builtin_i.haynode, cmd_val)
241
242 self.errfmt.Print_(
243 'Command %r not found in pure mode (OILS-ERR-102)' % arg0,
244 arg0_loc)
245 return 127
246
247 def RunBackgroundJob(self, node):
248 # type: (command_t) -> int
249 raise error.Structured(
250 _PURITY_STATUS,
251 "Background jobs aren't allowed in pure mode (OILS-ERR-204)",
252 loc.Command(node))
253
254 def RunPipeline(self, node, status_out):
255 # type: (command.Pipeline, CommandStatus) -> None
256 raise error.Structured(
257 _PURITY_STATUS,
258 "Pipelines aren't allowed in pure mode (OILS-ERR-204)",
259 loc.Command(node))
260
261 def RunSubshell(self, node):
262 # type: (command_t) -> int
263 raise error.Structured(
264 _PURITY_STATUS,
265 "Subshells aren't allowed in pure mode (OILS-ERR-204)",
266 loc.Command(node))
267
268 def CaptureStdout(self, node):
269 # type: (command_t) -> Tuple[int, str]
270 """
271 Used by io->captureStdout() method, and called by command sub
272 """
273 return 0, ''
274
275 def Capture3(self, node):
276 # type: (command_t) -> Tuple[int, str, str]
277 """
278 Used by io->captureAll() method, and called by command sub
279 """
280 return 0, '', ''
281
282 def RunCommandSub(self, cs_part):
283 # type: (CommandSub) -> str
284 raise error.Structured(
285 _PURITY_STATUS,
286 "Command subs aren't allowed in pure mode (OILS-ERR-204)",
287 loc.WordPart(cs_part))
288
289 def RunProcessSub(self, cs_part):
290 # type: (CommandSub) -> str
291 raise error.Structured(
292 _PURITY_STATUS,
293 "Process subs aren't allowed in pure mode (OILS-ERR-204)",
294 loc.WordPart(cs_part))
295
296 def PushRedirects(self, redirects, err_out):
297 # type: (List[RedirValue], List[error.IOError_OSError]) -> None
298 pass
299
300 def PopRedirects(self, num_redirects, err_out):
301 # type: (int, List[error.IOError_OSError]) -> None
302 pass
303
304 def PushProcessSub(self):
305 # type: () -> None
306 pass
307
308 def PopProcessSub(self, compound_st):
309 # type: (StatusArray) -> None
310 pass
311
312
313class ShellExecutor(vm._Executor):
314 """An executor combined with the OSH language evaluators in osh/ to create
315 a shell interpreter."""
316
317 def __init__(
318 self,
319 mem, # type: state.Mem
320 exec_opts, # type: optview.Exec
321 mutable_opts, # type: state.MutableOpts
322 procs, # type: state.Procs
323 hay_state, # type: hay_ysh.HayState
324 builtins, # type: Dict[int, vm._Builtin]
325 tracer, # type: dev.Tracer
326 errfmt, # type: ui.ErrorFormatter
327 search_path, # type: SearchPath
328 ext_prog, # type: process.ExternalProgram
329 waiter, # type: process.Waiter
330 job_control, # type: process.JobControl
331 job_list, # type: process.JobList
332 fd_state, # type: process.FdState
333 trap_state, # type: trap_osh.TrapState
334 ):
335 # type: (...) -> None
336 vm._Executor.__init__(self, mem, exec_opts, mutable_opts, procs,
337 hay_state, builtins, tracer, errfmt)
338 self.search_path = search_path
339 self.ext_prog = ext_prog
340 self.waiter = waiter
341 self.multi_trace = tracer.multi_trace
342 self.job_control = job_control
343 # sleep 5 & puts a (PID, job#) entry here. And then "jobs" displays it.
344 self.job_list = job_list
345 self.fd_state = fd_state
346 self.trap_state = trap_state
347 self.process_sub_stack = [] # type: List[_ProcessSubFrame]
348 self.clean_frame_pool = [] # type: List[_ProcessSubFrame]
349
350 # When starting a pipeline in the foreground, we need to pass a handle to it
351 # through the evaluation of the last node back to ourselves for execution.
352 # We use this handle to make sure any processes forked for the last part of
353 # the pipeline are placed into the same process group as the rest of the
354 # pipeline. Since there is, by design, only ever one foreground pipeline and
355 # any pipelines started within subshells run in their parent's process
356 # group, we only need one pointer here, not some collection.
357 self.fg_pipeline = None # type: Optional[process.Pipeline]
358
359 def _MakeProcess(self, node, inherit_errexit, inherit_errtrace):
360 # type: (command_t, bool, bool) -> process.Process
361 """Assume we will run the node in another process.
362
363 Return a process.
364 """
365 UP_node = node
366 if node.tag() == command_e.ControlFlow:
367 node = cast(command.ControlFlow, UP_node)
368 # Pipeline or subshells with control flow are invalid, e.g.:
369 # - break | less
370 # - continue | less
371 # - ( return )
372 # NOTE: This could be done at parse time too.
373 if node.keyword.id != Id.ControlFlow_Exit:
374 e_die(
375 'Invalid control flow %r in pipeline / subshell / background'
376 % lexer.TokenVal(node.keyword), node.keyword)
377
378 # NOTE: If ErrExit(), we could be verbose about subprogram errors? This
379 # only really matters when executing 'exit 42', because the child shell
380 # inherits errexit and will be verbose. Other notes:
381 #
382 # - We might want errors to fit on a single line so they don't get
383 # interleaved.
384 # - We could turn the `exit` builtin into a error.FatalRuntime exception
385 # and get this check for "free".
386 thunk = process.SubProgramThunk(self.cmd_ev, node, self.trap_state,
387 self.multi_trace, inherit_errexit,
388 inherit_errtrace)
389 p = process.Process(thunk, self.job_control, self.job_list,
390 self.tracer)
391 return p
392
393 def _RunSimpleCommand(self, arg0, arg0_loc, cmd_val, cmd_st, run_flags):
394 # type: (str, loc_t, cmd_value.Argv, CommandStatus, int) -> int
395 """Run builtins, functions, external commands.
396
397 Possible variations:
398 - YSH might have different, simpler rules. No special builtins, etc.
399 - YSH might have OILS_PATH = :| /bin /usr/bin | or something.
400 - Interpreters might want to define all their own builtins.
401 """
402
403 builtin_id = consts.LookupAssignBuiltin(arg0)
404 if builtin_id != consts.NO_INDEX:
405 # command readonly is disallowed, for technical reasons. Could relax it
406 # later.
407 self.errfmt.Print_("Simple command can't run assignment builtin",
408 arg0_loc)
409 return 1
410
411 builtin_id = consts.LookupSpecialBuiltin(arg0)
412 if builtin_id != consts.NO_INDEX:
413 cmd_st.show_code = True # this is a "leaf" for errors
414 status = self.RunBuiltin(builtin_id, cmd_val)
415 # TODO: Enable this and fix spec test failures.
416 # Also update _SPECIAL_BUILTINS in osh/builtin.py.
417 #if status != 0:
418 # e_die_status(status, 'special builtin failed')
419 return status
420
421 # Call procs first. Builtins like 'true' can be redefined.
422 call_procs = not (run_flags & NO_CALL_PROCS)
423 if call_procs:
424 proc_val, self_obj = self.procs.GetInvokable(arg0)
425 if proc_val is not None:
426 return self._RunInvokable(proc_val, self_obj, arg0_loc,
427 cmd_val)
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_('Command %r not found (OILS-ERR-100)' % arg0,
469 arg0_loc)
470 return 127
471
472 if self.trap_state.ThisProcessHasTraps():
473 do_fork = True
474 else:
475 do_fork = not cmd_val.is_last_cmd
476
477 # Normal case: ls /
478 if do_fork:
479 thunk = process.ExternalThunk(self.ext_prog, argv0_path, cmd_val,
480 environ)
481 p = process.Process(thunk, self.job_control, self.job_list,
482 self.tracer)
483
484 if self.job_control.Enabled():
485 if self.fg_pipeline is not None:
486 pgid = self.fg_pipeline.ProcessGroupId()
487 # If job control is enabled, this should be true
488 assert pgid != process.INVALID_PGID
489
490 change = process.SetPgid(pgid, self.tracer)
491 self.fg_pipeline = None # clear to avoid confusion in subshells
492 else:
493 change = process.SetPgid(process.OWN_LEADER, self.tracer)
494 p.AddStateChange(change)
495
496 status = p.RunProcess(self.waiter, trace.External(cmd_val.argv))
497
498 # this is close to a "leaf" for errors
499 # problem: permission denied EACCESS prints duplicate messages
500 # TODO: add message command 'ls' failed
501 cmd_st.show_code = True
502
503 return status
504
505 self.tracer.OnExec(cmd_val.argv)
506
507 # Already forked for pipeline: ls / | wc -l
508 self.ext_prog.Exec(argv0_path, cmd_val, environ) # NEVER RETURNS
509
510 raise AssertionError('for -Wreturn-type in C++')
511
512 def RunBackgroundJob(self, node):
513 # type: (command_t) -> int
514 """For & etc."""
515 # Special case for pipeline. There is some evidence here:
516 # https://www.gnu.org/software/libc/manual/html_node/Launching-Jobs.html#Launching-Jobs
517 #
518 # "You can either make all the processes in the process group be children
519 # of the shell process, or you can make one process in group be the
520 # ancestor of all the other processes in that group. The sample shell
521 # program presented in this chapter uses the first approach because it
522 # makes bookkeeping somewhat simpler."
523 UP_node = node
524
525 if UP_node.tag() == command_e.Pipeline:
526 node = cast(command.Pipeline, UP_node)
527 pi = process.Pipeline(self.exec_opts.sigpipe_status_ok(),
528 self.job_control, self.job_list, self.tracer)
529 for child in node.children:
530 p = self._MakeProcess(child, True, self.exec_opts.errtrace())
531 p.Init_ParentPipeline(pi)
532 pi.Add(p)
533
534 pi.StartPipeline(self.waiter)
535 pi.SetBackground()
536 self.mem.last_bg_pid = pi.PidForWait() # for $!
537 job_id = self.job_list.RegisterJob(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 = p.PidForWait() # for $!
552 job_id = self.job_list.RegisterJob(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: # EOF
623 break
624
625 elif n > 0:
626 # common shell behavior: remove NUL from stdout
627 chunks[-1] = chunks[-1].replace('\0', '')
628
629 else: # n < 0
630 if err_num == EINTR:
631 pass # retry
632 else:
633 # Like the top level IOError handler
634 e_die_status(
635 2,
636 'Oils I/O error (read): %s' % posix.strerror(err_num))
637
638 posix.close(r)
639
640 status = p.Wait(self.waiter)
641 stdout_str = ''.join(chunks).rstrip('\n')
642
643 return status, stdout_str
644
645 def Capture3(self, node):
646 # type: (command_t) -> Tuple[int, str, str]
647
648 p = self._MakeProcess(node, self.exec_opts.inherit_errexit(),
649 self.exec_opts.errtrace())
650 # Shell quirk: Command subs remain part of the shell's process group, so we
651 # don't use p.AddStateChange(process.SetPgid(...))
652
653 stdout_fd, w = posix.pipe()
654 stderr_fd, w2 = posix.pipe()
655 p.AddStateChange(process.StdoutToPipe(stdout_fd, w))
656 p.AddStateChange(process.StderrToPipe(stderr_fd, w2))
657
658 p.StartProcess(trace.CommandSub)
659 #log('Command sub started %d', pid)
660
661 stdout_chunks = [] # type: List[str]
662 stderr_chunks = [] # type: List[str]
663 posix.close(w) # not going to write
664 posix.close(w2) # not going to write
665 open_fds = [stdout_fd, stderr_fd]
666 while True:
667 fds = pyos.WaitForReading(open_fds)
668
669 # zero outputs mean something went wrong
670 if len(fds) == 0:
671 break
672
673 for fd in fds:
674 if fd == stdout_fd:
675 n, err_num = pyos.Read(fd, 4096, stdout_chunks)
676 else:
677 n, err_num = pyos.Read(fd, 4096, stderr_chunks)
678 if n < 0:
679 if err_num == EINTR:
680 pass # retry
681 else:
682 # Like the top level IOError handler
683 e_die_status(
684 2, 'Oils I/O error (read): %s' %
685 posix.strerror(err_num))
686 elif n == 0: # EOF
687 open_fds.remove(fd)
688
689 if len(open_fds) == 0:
690 break
691
692 posix.close(stdout_fd)
693 posix.close(stderr_fd)
694
695 status = p.Wait(self.waiter)
696 stdout_str = ''.join(stdout_chunks)
697 stderr_str = ''.join(stderr_chunks)
698
699 return status, stdout_str, stderr_str
700
701 def RunCommandSub(self, cs_part):
702 # type: (CommandSub) -> str
703
704 if not self.exec_opts._allow_command_sub():
705 # _allow_command_sub is used in two places. Only one of them turns
706 # off _allow_process_sub
707 if not self.exec_opts._allow_process_sub():
708 why = "status wouldn't be checked (strict_errexit)"
709 else:
710 why = 'eval_unsafe_arith is off'
711
712 e_die("Command subs not allowed here because %s" % why,
713 loc.WordPart(cs_part))
714
715 node = cs_part.child
716
717 # Hack for weird $(<file) construct
718 if node.tag() == command_e.Redirect:
719 redir_node = cast(command.Redirect, node)
720 # Detect '< file'
721 if (len(redir_node.redirects) == 1 and
722 redir_node.redirects[0].op.id == Id.Redir_Less and
723 redir_node.child.tag() == command_e.NoOp):
724
725 # Change it to __cat < file.
726 # TODO: could be 'internal cat' (issue #1013)
727 tok = lexer.DummyToken(Id.Lit_Chars, '__cat')
728 cat_word = CompoundWord([tok])
729
730 # Blame < because __cat has no location
731 blame_tok = redir_node.redirects[0].op
732 simple = command.Simple(blame_tok, [], [cat_word], None, None,
733 False)
734
735 # MUTATE redir node so it's like $(<file _cat)
736 redir_node.child = simple
737
738 status, stdout_str = self.CaptureStdout(node)
739
740 # OSH has the concept of aborting in the middle of a WORD. We're not
741 # waiting until the command is over!
742 if self.exec_opts.command_sub_errexit():
743 if status != 0:
744 msg = 'Command Sub exited with status %d' % status
745 raise error.ErrExit(status, msg, loc.WordPart(cs_part))
746
747 else:
748 # Set a flag so we check errexit at the same time as bash. Example:
749 #
750 # a=$(false)
751 # echo foo # no matter what comes here, the flag is reset
752 #
753 # Set ONLY until this command node has finished executing.
754
755 # HACK: move this
756 self.cmd_ev.check_command_sub_status = True
757 self.mem.SetLastStatus(status)
758
759 # Runtime errors test case: # $("echo foo > $@")
760 # Why rstrip()?
761 # https://unix.stackexchange.com/questions/17747/why-does-shell-command-substitution-gobble-up-a-trailing-newline-char
762 return stdout_str
763
764 def RunProcessSub(self, cs_part):
765 # type: (CommandSub) -> str
766 """Process sub creates a forks a process connected to a pipe.
767
768 The pipe is typically passed to another process via a /dev/fd/$FD path.
769
770 Life cycle of a process substitution:
771
772 1. Start with this code
773
774 diff <(seq 3) <(seq 4)
775
776 2. To evaluate the command line, we evaluate every word. The
777 NormalWordEvaluator this method, RunProcessSub(), which does 3 things:
778
779 a. Create a pipe(), getting r and w
780 b. Starts the seq process, which inherits r and w
781 It has a StdoutToPipe() redirect, which means that it dup2(w, 1)
782 and close(r)
783 c. Close the w FD, because neither the shell or 'diff' will write to it.
784 However we must retain 'r', because 'diff' hasn't opened /dev/fd yet!
785 d. We evaluate <(seq 3) to /dev/fd/$r, so "diff" can read from it
786
787 3. Now we're done evaluating every word, so we know the command line of
788 diff, which looks like
789
790 diff /dev/fd/64 /dev/fd/65
791
792 Those are the FDs for the read ends of the pipes we created.
793
794 4. diff inherits a copy of the read end of bot pipes. But it actually
795 calls open() both files passed as argv. (I think this is fine.)
796
797 5. wait() for the diff process.
798
799 6. The shell closes both the read ends of both pipes. Neither us or
800 'diffd' will read again.
801
802 7. The shell waits for both 'seq' processes.
803
804 Related:
805 shopt -s process_sub_fail
806 _process_sub_status
807 """
808 cs_loc = loc.WordPart(cs_part)
809
810 if not self.exec_opts._allow_process_sub():
811 e_die(
812 "Process subs not allowed here because status wouldn't be checked (strict_errexit)",
813 cs_loc)
814
815 p = self._MakeProcess(cs_part.child, True, self.exec_opts.errtrace())
816
817 r, w = posix.pipe()
818 #log('pipe = %d, %d', r, w)
819
820 op_id = cs_part.left_token.id
821 if op_id == Id.Left_ProcSubIn:
822 # Example: cat < <(head foo.txt)
823 #
824 # The head process should write its stdout to a pipe.
825 redir = process.StdoutToPipe(r,
826 w) # type: process.ChildStateChange
827
828 elif op_id == Id.Left_ProcSubOut:
829 # Example: head foo.txt > >(tac)
830 #
831 # The tac process should read its stdin from a pipe.
832
833 # Note: this example sometimes requires you to hit "enter" in bash and
834 # zsh. WHy?
835 redir = process.StdinFromPipe(r, w)
836
837 else:
838 raise AssertionError()
839
840 p.AddStateChange(redir)
841
842 if self.job_control.Enabled():
843 p.AddStateChange(process.SetPgid(process.OWN_LEADER, self.tracer))
844
845 # Fork, letting the child inherit the pipe file descriptors.
846 p.StartProcess(trace.ProcessSub)
847
848 ps_frame = self.process_sub_stack[-1]
849
850 # Note: bash never waits() on the process, but zsh does. The calling
851 # program needs to read() before we can wait, e.g.
852 # diff <(sort left.txt) <(sort right.txt)
853
854 # After forking, close the end of the pipe we're not using.
855 if op_id == Id.Left_ProcSubIn:
856 posix.close(w) # cat < <(head foo.txt)
857 ps_frame.Append(p, r, cs_loc) # close later
858 elif op_id == Id.Left_ProcSubOut:
859 posix.close(r)
860 #log('Left_ProcSubOut closed %d', r)
861 ps_frame.Append(p, w, cs_loc) # close later
862 else:
863 raise AssertionError()
864
865 # Is /dev Linux-specific?
866 if op_id == Id.Left_ProcSubIn:
867 return '/dev/fd/%d' % r
868
869 elif op_id == Id.Left_ProcSubOut:
870 return '/dev/fd/%d' % w
871
872 else:
873 raise AssertionError()
874
875 def PushRedirects(self, redirects, err_out):
876 # type: (List[RedirValue], List[error.IOError_OSError]) -> None
877 if len(redirects) == 0: # Optimized to avoid allocs
878 return
879 self.fd_state.Push(redirects, err_out)
880
881 def PopRedirects(self, num_redirects, err_out):
882 # type: (int, List[error.IOError_OSError]) -> None
883 if num_redirects == 0: # Optimized to avoid allocs
884 return
885 self.fd_state.Pop(err_out)
886
887 def PushProcessSub(self):
888 # type: () -> None
889 if len(self.clean_frame_pool):
890 # Optimized to avoid allocs
891 new_frame = self.clean_frame_pool.pop()
892 else:
893 new_frame = _ProcessSubFrame()
894 self.process_sub_stack.append(new_frame)
895
896 def PopProcessSub(self, compound_st):
897 # type: (StatusArray) -> None
898 """This method is called by a context manager, which means we always
899 wait() on the way out, which I think is the right thing.
900
901 We don't always set _process_sub_status, e.g. if some fatal
902 error occurs first, but we always wait.
903 """
904 frame = self.process_sub_stack.pop()
905 if frame.WasModified():
906 frame.MaybeWaitOnProcessSubs(self.waiter, compound_st)
907 else:
908 # Optimized to avoid allocs
909 self.clean_frame_pool.append(frame)
910
911 # Note: the 3 lists in _ProcessSubFrame are hot in our profiles. It would
912 # be nice to somehow "destroy" them here, rather than letting them become
913 # garbage that needs to be traced.
914
915 # The CommandEvaluator could have a ProcessSubStack, which supports Push(),
916 # Pop(), and Top() of VALUES rather than GC objects?
917
918
919# vim: sw=4