TL;DR: When executing part of a pipeline, the shell performs pipe-redirection of stdin/stdout first and >/<
redirection last. Command substitution happens in between those two, so pipeline-redirection of stdin/stdout is inherited, whilst >/<
redirection is not. It's a design decision.
To be fair, I accepted chepner's answer because he was first and he was correct. However, I decided to add my own answer to document my process of understanding this issue by reading bash's sources, as chepner's answer doesn't explain why the >/<
redirection isn't inherited.
It is helpful to understand the steps involved (grossly simplified), when a complex pipeline is encountered by the shell. I have simplified my original problem to this example:
$ echo x >(echo y) >file
y
$ cat file
x /dev/fd/63
$ echo x >(echo y) | cat >file
$ cat file
x /dev/fd/63
y
Redirection-only
When the shell encounters echo x >(echo y) >file
, it first forks once to execute the complex command (this can be avoided for some cases, like builtins), and then the forked shell:
- creates a
pipe
(for process substitution)
- forks for second echo
- fork: connects its
stdin
to pipe[1]
- fork:
exec
's echo y
; the exec
'ed echo
inherits:
- stdin connected to pipe[1]
- unchanged stdout
- opens
file
- connects its
stdout
to file
exec
's echo x /proc/<pid>/fd/<pipe id>
; the exec
'ed echo
inherits:
- stdin unchanged
- stdout connected to
file
Here, the second echo inherits the stdout
of the forked shell, before that forked shell redirects its stdout
to file
. I see no absolute necessity for this order of actions in this context, but I assume it makes more sense this way.
Pipe-Redirect
When the shell encounters echo x >(echo y) | cat >file
, it detects a pipeline and starts processing it (without forking):
- parent: creates a
pipe
(corresponding to the only actual |
in the full command)
- parent: forks for left side of
pipe
- fork1: connects its
stdout
to pipe[0]
- fork1: creates a
pipe_subst
(for process substitution)
- fork1: forks for second echo
- nested-fork: connects its
stdin
to pipe_subst[1]
- nested-fork:
exec
's echo y
; the exec
'ed echo
inherits:
- stdin connected to
pipe_subst[1]
from the inner fork
- stdout connected to
pipe[0]
from the outer fork
- fork1:
exec
's echo x /proc/<pid>/fd/<pipe_subst id>
; the exec
'ed echo
inherits:
- stdin unchanged
- stdout connected to
pipe[0]
- parent: forks for right side of
pipe
(this fork, again, can sometimes be avoided)
- fork2: connects its
stdin
to pipe[1]
- fork2: opens
file
- fork2: connects its
stdout
to file
- fork2:
exec
's cat
; the exec
'ed cat
inherits:
- stdin connected to
pipe[1]
- stdout connected to
file
Here, the pipe takes precedence, i.e. redirection of stdin/stdout due to the pipe is performed before any other actions take place in executing the pipeline elements. Thus both echo
's inherit the stdout
redirected to cat
.
All of this is really a design-consequence of >file
redirection being handled after process substitution. If >file
redirection were handled before that (like pipe redirection is), then >file
would also have been inherited by the substituted processes.