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.