5

This is a followup question to: Why does `... | sed 's/^/stdout: /'` print on empty stdin when `... > >(sed 's/^/stdout: /')` doesn't?

To be more concrete, why is this a pipe:

$ tee 2> >(readlink /proc/self/fd/1) < /dev/null | cat        
pipe:[17955449]

when this is a terminal device?:

$ tee >(readlink /proc/self/fd/1) < /dev/null | cat   
/dev/pts/31

I would have thought that the former's output would too have naturally been a terminal as inherited from the shell where I'm typing the commands, but it seems both bash and zsh must take an explicit step to redirect it to the output of the command they're redirecting. Why are they doing that? or is something else happening? Is the former's subshell spawned from tee's process before exec'ing tee? Does one subshell inherit from the child process while the other from the parent process? How?

Hmm... While doing the tags and checking bash, I see that it's a pipe in bash for both cases... So this is a zsh peculiarity.

1

zsh supports multiple redirections of the same command. For example, abc > def > ghi puts the full output of abc into both def and 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
/dev/tty1
pipe:[2975]

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 |) and what their behaviour is.


Case analysis

Overall, there are four distinct cases for zsh's behaviour:

  1. |, no >

    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.

  2. No |, no >

    abc >( def )
    

    This sends everything to the TTY and passes a path to abc.

  3. No |, but >

    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.

  4. | and >

    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.

  • "It also allows both > and a single | redirection at once, which is what you're using in your second example." -- I'm not. The second example is not a redirection, it just leaves the pipe path as an argument to tee. The first example does redirect, but on file descriptor 2, while the | works on file descriptor 1. – JoL Jan 18 at 22:53
  • Sorry, of course that meant in your first example. – Michael Homer Jan 18 at 22:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.