zsh supports multiple redirections of the same command. For example,
abc > def > ghi puts the full output of
abc into both
ghi. It also allows both
> and a single unadorned
| redirection at once, which is what you're using in your first example.
In that multiple-redirection situation, the process substitution with a redirection into it
tee 2> >(readlink /proc/self/fd/1) < /dev/null | cat
has its output piped as well as the main command, while a process substitution without redirection into it
tee >(readlink /proc/self/fd/1) < /dev/null | cat
doesn't. The pipe takes priority in a sense: every piece of output from any branch of redirection goes through it. In this case, there are two branches - the main command itself, and the output from the process substitution - and they both go through
cat and so both see their standard output as a pipe.
Bash always pipes the substitution into your
cat regardless, and doesn't support multiple redirections like that in the first place.
In essence, with multiple redirections in zsh, there can still only be one
| and the pipe distributes across all branches of redirection, but process substitution itself is not part of that - only actual output redirections with
This is a property of redirection, made visible through process substitution. We can use both a redirected and unredirected process substitution together and see that:
$ true >( readlink /proc/self/fd/1 ) > >( readlink /proc/self/fd/1 ) | cat
The first one (just a substitution) has the TTY as stdout, and the second (redirected into) has the pipe into
cat. This is the only straightforward way of constructing that setup: a single instance of the piped-to command and a mixture of piped and unpiped commands before it. If you end up down the wrong track redirecting output when you didn't want to, you can recover with
> /dev/pts/..., but if you really did want the bare substitution to redirect as in Bash you're still out of luck.
Process substitutions on their own inherit their environment from the shell, while the redirection modifies both input and output and factors in the pipe. I don't think it's necessary that it work this way, but there is a consistent rule:
| always distributes over
>, but ignores arguments.
My experiments illustrating what the actual behaviour is are too long and unwieldy to include inline here, but are in the revision history of this answer. Below I summarise the four different cases (yes/no each of
|) and what their behaviour is.
Overall, there are four distinct cases for zsh's behaviour:
abc >( def ) | ghi
This sends the base command's output to the pipe, and the subshell's to the TTY, passing a path to
abc >( def )
This sends everything to the TTY and passes a path to
abc > >( def )
This sends the base command's output into the subshell only, and the subshell's to the TTY, giving no arguments to
abc > >( def ) | ghi
This sends the base command's output to both the process substitution and the pipe, and the subshell's output to the pipe, giving no arguments to
abc. It's acting as
abc | tee >( def ) | ghi.
I don't really like that case 4 is so different to case 1 when it feels like the change is so far away from the pipe, but that's how it is.