16

Let's assume the following piece of bash code:

foo > logfile 2>&1 &
foo_pid=$!

while ps -p$foo_pid
do
    ping -c 1 localhost
done

wait $foo_pid

if [[ $? == 0 ]]
then
    echo "foo success"
fi

Is it safe to assume that $? indeed contains the return code of foo and not the return code of ping? If the answer to that question is: "You cannot assume that." then how can I modify this piece of code to be sure that $? always contains the return code of foo?

2 Answers2

16

With bash, you'll have that guarantee unless you've started another background job (and beware that background jobs can be started with & but also with coproc and with process substitution) between the foo & and the wait.

POSIX requires that a shell remembers the exit status of at least 25 jobs after they're gone, but bash remembers a lot more than that.

Now, if you do:

foo & pid=$!
...
bar &
wait "$pid"

You've got no guarantee that bar will not be given the same pid as foo (if foo has terminated by the time bar starts), so even though it's unlikely, wait "$pid" may give you the exit status of bar.

You can reproduce it with:

bash -c '(exit 12; foo) & pid=$!
         while : bar & [ "$pid" != "$!" ]; do :;done
         wait "$pid"; echo "$?"'

which will (eventually) give you 0 instead of 12.

To avoid the problem, one way would be to write it as:

{
  foo_pid=$!

  while ps -p "$foo_pid"
  do
      ping -c 1 localhost
  done

  bar &
  ...

  read <&3 ret
  if [ "$ret" = 0 ]; then
    echo foo was sucessful.
  fi
} 3< <(foo > logfile 2>&1; echo "$?")
0

I believe your assumption is correct. Here's an extract from man bash regarding waiting for background processes.

If n specifies a non-existent process or job, the return status is 127. Otherwise, the return status is the exit status of the last process or job waited for.

So maybe you should check for 127

There's a similar question with a completely different answer than might help.

Bash script wait for processes and get return code

edit 1

Inspired by @Stephane's comments and answers I have expanded his script. I can start about 34 background processes before it starts to loose track.

tback

$ cat tback 
plist=()
elist=()
slist=([1]=12 [2]=15 [3]=17 [4]=19 [5]=21 [6]=23)
count=30

#start background tasksto monitor
for i in 1 2 3 4
do
  #echo pid $i ${plist[$i]} ${slist[$i]}
  (echo $BASHPID-${slist[$i]} running; exit ${slist[$i]}) & 
  plist[$i]=$!
done

echo starting $count background echos to test history
for i in `eval echo {1..$count}`
do
  echo -n "." &
  elist[$i]=$! 
done
# wait for each background echo to complete
for i in `eval echo {1..$count}`
do
  wait ${elist[$i]}
  echo -n $? 
done
echo ""
# Now wait for each monitored process and check return status with expected
failed=0
for i in 1 2 3 4
do
  wait ${plist[$i]}
  rv=$?
  echo " pid ${plist[$i]} returns $rv should be ${slist[$i]}"
  if [[ $rv != ${slist[$i]} ]] 
  then
    failed=1
  fi
done

wait
echo "Complete $failed"
if [[ $failed = "1" ]]
then
  echo Failed
else
  echo Success
fi
exit $failed
$ 

on my system produces

$ bash tback
14553-12 running
14554-15 running
14555-17 running
starting 30 background echos to test history
14556-19 running
..............................000000000000000000000000000000
 pid 14553 returns 12 should be 12
 pid 14554 returns 15 should be 15
 pid 14555 returns 17 should be 17
 pid 14556 returns 19 should be 19
Complete 0
Success
X Tian
  • 10,463