The script below has no other purpose than to illustrate this question.

#!/usr/bin/env zsh

arbitrary_pipeline () {
    shuf | tr a-z A-Z

tmpdir=$( mktemp -d )

mkfifo $tmpdir/{orig,alt}

{ tee $tmpdir/orig | arbitrary_pipeline > $tmpdir/alt; } &

paste $tmpdir/orig $tmpdir/alt

rm -rf $tmpdir

wait $pid

The script uses tee and two named pipes to split some (arbitrary) standard input into two streams, redirects one of them to an arbitrary pipeline, and passes the resulting two streams the inputs to paste. Schematically:

STDIN --- > --.- arbitrary_pipeline -.
               \                      \
          paste `----<FIRST-ARGUMENT>  `- <SECOND-ARGUMENT> --> STDOUT

(In this case arbitrary_pipeline just shuffles its stdin, and converts it to uppercase, but, as the name implies, it could be anything.)

The script's stdout output looks fine, but the wait command always fails:

% grep -iP 'z.*s.*h' /usr/share/dict/words | /tmp/test.sh
Zubeneschamali  NIETZSCHE
Zubeneschamali's    ZUBENESCHAMALI
citizenship CITIZENSHIP
citizenship's   ZUBENESCHAMALI'S
/tmp/test.sh:wait:18: pid 26357 is not a child of this shell

What am I doing wrong?


/usr/bin/env zsh --version
# zsh 5.0.7 (x86_64-pc-linux-gnu)


  1. added curly braces around the tee pipeline, per jordanm's suggestion. (Results did not change, though.)
  2. replaced &! with & in response to Stéphane Chazelas' comment. (Again, results did not change.)
  • 1
    Try adding braces: { tee $tmpdir/orig | arbitrary_pipeline > $tmpdir/alt; } & – jordanm Nov 4 '16 at 20:05
  • @jordanm: I get the same results – kjo Nov 4 '16 at 20:10
  • &! is explicitely to disown the job! Of course wait will fail, that's what &! is for. Use & instead of &! if you still want the shell to consider the job as its child so you can wait for it. – Stéphane Chazelas Nov 5 '16 at 10:07
  • @StéphaneChazelas: I get exactly the same results (at least outwardly) with & alone. (I will edit my post to eliminate my stupid red herring.) – kjo Nov 5 '16 at 11:19

Before 5.0.8, zsh couldn't wait for already dead jobs. That was changed in 5.0.8 in 2014. See the change there.

Here, you can just redirect stderr to /dev/null to ignore the problem:

wait $pid 2> /dev/null

Note that in:

{ tee $tmpdir/orig | arbitrary_pipeline > $tmpdir/alt; } &

as an optimisation, zsh will not fork an extra process for arbitrary_pipeline, it will execute it in the same process as the one that runs that subshell started in background.

paste will not finish before it sees EOF on its stdin, its stdin being the pipe where $pid (and its children if any) is writing at the other end. So, it will not see eof until $pid (and children) has closed all its file descriptors (typically only stdout) to the writing end of the pipe. Unless $pid does explicitly close its stdout (which is very uncommon), that will only happen when it exits.

What that means is the paste will, in most cases, not exit before $pid, it's still a good idea to do the wait just in case.

Note that here, you could use a coproc to avoid the temporary fifos:

coproc arbitrary_pipeline
cat >&p | paste - /dev/fd/3 3<&p &
coproc : close

(note that wait also waits for the coprocs).

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.