Mathijs Kwik
Mathijs Kwik

Reputation: 1237

bash: wait for single process from a pipeline

I'm starting 2 processes, where one pipes into the other and then background them both. I want to wait until the second process finishes to then decide the fate of the first one.

The following snippet gives the output I'm expecting ("exit 1"). However, wait seems to wait for both processes to be finished (so 5 seconds), which is not what I want.

What have I misunderstood about wait? How can I make it wait for just the second process in the pipeline and decide to wait for the first one (or kill it) later?

#!/bin/sh
{
  sleep 5
  exit 5
} | {
  sleep 1
  exit 1
} &
wait $!
echo "exit $?"

Upvotes: 1

Views: 4042

Answers (3)

chepner
chepner

Reputation: 531165

When you write {...} | {...} &, there are at least three processes involved:

  1. The one that executes the commands in the LHS of the |
  2. The one that executes the commands in the RHS of the |
  3. The one that executes the entire pipeline

& is a list terminator. It does not apply to the previous simple or compound command, or even the previous pipeline. This entire command is executed in a single background process:

a | b || c | d && e | f &

& does not apply only to f, or e | f, or even c | d && e | f. It applies to the entire sequence of pipelines.

@KamilCuk mentions named pipes. I would prefer them over process substitution because it allows you to treat both processes uniformly, at least as far as syntax is concerned. (Also, named pipes work in any POSIX-compliant shell, not just bash.)

mk_fifo p1

{ sleep 5; exit 5; } > p1 & p1_pid=$!
{ sleep 1; exit 1; } < p1 & p2_pid=$!

wait $p2_pid

# Now, kill or wait for the first process as desired

Upvotes: 2

Zilog80
Zilog80

Reputation: 2562

You don't have misunderstood the wait builtin behavior. You have misunderstood the shell pipe processing instead.

The wait command behaves as expected, it waits for the end of the subshell from the right part of the pipe (the sleep 1) and it waits for five seconds because this right part doesn't effectively terminate until its pipe input is closed. Its pipe input is closed only when the left part of the pipe closes it on its termination (the sleep 5), thus after five seconds.

To wait for the right part of the pipe, you can use signal to know when the right part is about to finish like that :

#!/bin/dash
{
  sleep 5
  exit 5
} | {
  sleep 1
  kill -s USR1 $$
  exit 1
} &
trap "echo \"Second shell finished\";" USR1
wait $!
echo "exit $?"

This way, you'll get a signal from the right part and the wait command will then only wait until it get these signal (the wait command wait for any signal state change, not only termination). The kill -s USR1 $$ send the USR1 signal to the parent shell. Beware that it can imply termination for the process of the current subshell, you have to know how the process will handle USR1 (i guess sleep ignores it).

However, you can't then get the right exit code from the shell of the right part of the pipe, as it is not effectively terminated until its pipe input is closed. To be more precise, you'll get here the exit status 138 which is the the signal status USR1 (value 10) plus the POSIX signal base (128), not the subshell exit status. You can use different signals (USR1, USR2 (POSIX), non POSIX: .. USR64) to signify an exit status or use the subshell STDOUT or an environment variable to transmit an exit status instead if required.

Upvotes: 3

KamilCuk
KamilCuk

Reputation: 141000

Instead of using a pipe, you can create fifos and control each process yourself.

The easiest would be just to use process substitution.

{
   cmd2
} < <(cmd1) &

Or in the same fashion, using coprocess for the first command if explicit lifetime control isneeded.

Another solution would be to make interprocess communication between the shell in the pipeline and parent shell, be it a signal or just a file, or pid could be sent to parent process so that parent can while kill -0 on just the pid of last pipeline.

: > file
... | {
  sleep 1
  echo end > file
} &
while [[ $(cat file) != "end" ]]; do
   sleep 0.1
done

Upvotes: 1

Related Questions