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

921 lines, 502 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 last_pid = pi.LastPid()
537 self.mem.last_bg_pid = last_pid # for $!
538
539 job_id = self.job_list.AddJob(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 = pid # for $!
554 job_id = self.job_list.AddJob(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