Shell – zsh: command substitution does not inherit stdin from its parent

command-substitutionpipestdinsubshellzsh

Consider the following command:

seq 5 | grep $(tail -n1) <(seq 9)

When running it in zsh1:

tail: error reading 'standard input': Input/output error

Now running the same in bash, it outputs:

5

OK. As explained in the comments, the command substitution $(tail -n1) inherits stdin from its parent. But why doesn't that happen with zsh ?
Is this a zsh-only thing or is it something that other shells do too ? Where is it documented ?


Now, if I run the same command via zsh -c:

zsh -c 'seq 5 | grep $(tail -n1) <(seq 9)'

instead of printing the same error message, it stops after tail -n1 and waits for user input so if I type

19
2
4

then hit Ctrl+D, it prints

4

What's going on here ?


1: this is with zsh 5.3.1 on archlinux, if it matters.

Best Answer

You'll notice that in bash/ksh

echo foo | echo "$(cat)"

outputs foo but

<<< foo echo "$(cat)"

Doesn't.

In the first case, the $(cat) is expanded in the child process that will eventually execute echo after its stdin has been redirected from the pipe.

In the second case, the $(cat) is expanded before the redirection.

pipes and redirections are different things. pipes involve some redirection but also starting commands in parallel. That happens early, before the redirections inside each pipe component.

In zsh

$ sleep 1 | ps -jfH $(ps -fH >&2)
UID        PID  PPID  C STIME TTY          TIME CMD
chazelas  2495  2494  0 20:59 pts/1    00:00:00 /bin/zsh
chazelas 31201  2495  0 21:20 pts/1    00:00:00   sleep 1
chazelas 31202  2495  0 21:20 pts/1    00:00:00   ps -fH
UID        PID  PPID  PGID   SID  C STIME TTY          TIME CMD
chazelas  2495  2494  2495  2495  0 20:59 pts/1    00:00:00 /bin/zsh
chazelas 31201  2495 31201  2495  0 21:20 pts/1    00:00:00   sleep 1
chazelas 31203  2495 31201  2495  0 21:20 pts/1    00:00:00   ps -jfH

You'll notice that this time the command substitution is expanded by the parent shell.

One thing to bear in mind is that in zsh, pipes are treated a bit more like redirections in particular when it comes to the mult_ios option (enabled by default).

When you do:

echo foo > file | tr o e

foo goes to both file and tr.

In:

uname | cat < /etc/issue

cat is fed both the output of uname and the content of /etc/issue. So in zsh, redirection from </> and from pipe have to happen at the same stage. Preferably after the expansions.

In any case, you can always do:

echo foo | { echo "$(cat)"; }

in both zsh and bash/ksh like you can always do:

{ echo "$(cat)"; } <<< foo

in both bash and zsh.


As to the cause of the:

tail: error reading 'standard input': Input/output error

error. In interactive shells, since the command substitution is done in the parent, it's not done in the foreground process group of the terminal.

tail will be run in the process group of the parent shell. If that shell is the session leader, it will be an orphaned process group, so when tail tries to read from the tty device, it will fail with EIO.

If zsh was not the session leader. For instance, if you started zsh from another shell, then the process group would receive a SIGTTIN. The main shell process would ignore it, but tail would end up being suspended.