41

Say we have a bash script like so:

echo "x" &
echo "y" &
echo "z" &
.....
echo "Z" &
wait

is there a way to collect the exit codes of the sub shells / sub processes? Looking for way to do this and can't find anything. I need to run these subshells in parallel, otherwise yes this would be easier.

I am looking for a generic solution (I have an unknown/dynamic number of sub processes to run in parallel).

2
  • 1
    I'm going to suggest you figure out what it is that you want and then ask a new question, trying to be clear on exactly the behaviour you're looking for (perhaps with pseudocode or a larger example). Commented Feb 12, 2017 at 9:43
  • 4
    I actually think the question is good now - I have a dynamic number of sub processes. I need to collect all the exit codes. That's all. Commented Feb 12, 2017 at 9:49

10 Answers 10

31

Use wait with a PID, which will:

Wait until the child process specified by each process ID pid or job specification jobspec exits and return the exit status of the last command waited for.

You'll need to save the PID of each process as you go:

echo "x" & X=$!
echo "y" & Y=$!
echo "z" & Z=$!

You can also enable job control in the script with set -m and use a %n jobspec, but you almost certainly don't want to - job control has a lot of other side effects.

wait will return the same code as the process finished with. You can use wait $X at any (reasonable) later point to access the final code as $? or simply use it as true/false:

echo "x" & X=$!
echo "y" & Y=$!
...
wait $X
echo "job X returned $?"

wait will pause until the command completes if it hasn't already.

If you want to avoid stalling like that, you can set a trap on SIGCHLD, count the number of terminations, and handle all the waits at once when they've all finished. You can probably get away with using wait alone almost all the time.

16
  • 1
    ughh, sorry, I need to run these subshells in parallel, I will specify that in the question... Commented Feb 12, 2017 at 9:28
  • nevermind, maybe this works with my setup...where does the wait command come into play in your code? I don't follow Commented Feb 12, 2017 at 9:30
  • 2
    @AlexanderMills They are running in parallel. If you have a variable number of them, use an array. (as e.g. here which may thus be a duplicate). Commented Feb 12, 2017 at 9:30
  • 1
    You run wait $X at any (reasonable) later point. Commented Feb 12, 2017 at 9:33
  • 1
    @pmor wait on the PID of Y in the same way. Commented Nov 5, 2022 at 2:28
11

The answer by Alexander Mills which uses handleJobs gave me a great starting point, but also gave me this error

warning: run_pending_traps: bad value in trap_list[17]: 0x461010

Which may be a bash race-condition problem

Instead I did just store pid of each child and wait and gets exit code for each child specifically. I find this cleaner in terms of subprocesses spawning subprocesses in functions and avoiding the risk of waiting for a parent process where I meant to wait for child. Its clearer what happens because its not using the trap.

#!/usr/bin/env bash

# it seems it does not work well if using echo for function return value, and calling inside $() (is a subprocess spawned?) 
function wait_and_get_exit_codes() {
    children=("$@")
    EXIT_CODE=0
    for job in "${children[@]}"; do
       echo "PID => ${job}"
       CODE=0;
       wait ${job} || CODE=$?
       if [[ "${CODE}" != "0" ]]; then
           echo "At least one test failed with exit code => ${CODE}" ;
           EXIT_CODE=1;
       fi
   done
}

DIRN=$(dirname "$0");

commands=(
    "{ echo 'a'; exit 1; }"
    "{ echo 'b'; exit 0; }"
    "{ echo 'c'; exit 2; }"
    )

clen=`expr "${#commands[@]}" - 1` # get length of commands - 1

children_pids=()
for i in `seq 0 "$clen"`; do
    (echo "${commands[$i]}" | bash) &   # run the command via bash in subshell
    children_pids+=("$!")
    echo "$i ith command has been issued as a background job"
done
# wait; # wait for all subshells to finish - its still valid to wait for all jobs to finish, before processing any exit-codes if we wanted to
#EXIT_CODE=0;  # exit code of overall script
wait_and_get_exit_codes "${children_pids[@]}"

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end
4
  • cool, I think for job in "${childen[@]}"; do should be for job in "${1}"; do though, for clarity Commented Apr 11, 2018 at 12:31
  • the only concern I have with this script, is if children_pids+=("$!") is actually capturing the desired pid for the sub shell. Commented Apr 11, 2018 at 12:40
  • 1
    I tested with "${1}" and it doesn't work. I'm passing an array to the function, and apparently that needs special attention in bash. $! is the pid of the last spawned job, see tldp.org/LDP/abs/html/internalvariables.html It seems to work correctly in my tests, and I'm now using in the in unRAID cache_dirs script, and it seems to do its job. I'm using bash 4.4.12.
    – arberg
    Commented Apr 12, 2018 at 15:51
  • nice yep seems like you are correct Commented Apr 12, 2018 at 16:15
10

I think the other answers are needlessly convoluted.

If you save your background pids in an array as you launch them, you can wait for them explicitly and collect the return codes in a second array:

pids=()
echo "x" & pids+=($!)
echo "y" & pids+=($!)
echo "z" & pids+=($!)
....
echo "Z" & pids+=($!)

# wait and collect return codes
rets=()
for pid in ${pids[*]}; do
    wait $pid
    rets+=($?)
done
echo "Return codes: ${rets[*]}"

The return codes are collected in the same order as the jobs were launched.

If you only need to know if one or more jobs failed, you don't need to collect all return codes:

error=false
for pid in ${pids[*]}; do
    if ! wait $pid; then
        error=true
    fi
done
1
  • 1
    Thank you for also showing what to do when you only need to know if any jobs failed. Commented Feb 10, 2022 at 16:46
6

If you had a good way to identify the commands, you could print their exit code to a tmp file and then access the specific file you're interested in:

#!/bin/bash

for i in `seq 1 5`; do
    ( sleep $i ; echo $? > /tmp/cmd__${i} ) &
done

wait

for i in `seq 1 5`; do # or even /tmp/cmd__*
    echo "process $i:"
    cat /tmp/cmd__${i}
done

Don't forget to remove the tmp files.

5

Use a compound command - put the statement in parentheses:

( echo "x" ; echo X: $? ) &
( true ; echo TRUE: $? ) &
( false ; echo FALSE: $? ) &

will give the output

x
X: 0
TRUE: 0
FALSE: 1

A really different way to run several commands in parallel is by using GNU Parallel. Make a list of commands to run and put them in the file list:

cat > list
sleep 2 ; exit 7
sleep 3 ; exit 55
^D

Run all the commands in parallel and collect the exit codes in the file job.log:

cat list | parallel -j0 --joblog job.log
cat job.log

and the output is:

Seq     Host    Starttime       JobRuntime      Send    Receive Exitval Signal  Command
1       :       1486892487.325       1.976      0       0       7       0       sleep 2 ; exit 7
2       :       1486892487.326       3.003      0       0       55      0       sleep 3 ; exit 55
7
  • ok thanks, is there a way to generify this? I don't just have 3 sub processes, I have Z sub processes. Commented Feb 12, 2017 at 9:22
  • I updated the original question to reflect that I am looking for a generic solution, thanks Commented Feb 12, 2017 at 9:23
  • One way to generify it might be to use a looping construct? Commented Feb 12, 2017 at 9:25
  • Looping? Do you have a fixed list of commands or is that controlled by the user? I am not sure I understand what you are trying to do but maybe PIPESTATUS is something you should check out. This seq 10 | gzip -c > seq.gz ; echo ${PIPESTATUS[@]} returns 0 0 (exit code from first and last command).
    – hschou
    Commented Feb 12, 2017 at 9:39
  • Yeah essentially controlled by the user Commented Feb 12, 2017 at 9:40
3

script3 will be executed only if script1 and script2 are successful and script1 and script2 will be executed in parallel:

./script1 &
process1=$!

./script2 &
process2=$!

wait $process1
rc1=$?

wait $process2
rc2=$?

if [[ $rc1 -eq 0 ]] && [[ $rc2 -eq 0  ]];then
./script3
fi
1
2

this is the generic script you're looking for. The only downside is your commands are in quotes which means syntax highlighting via your IDE will not really work. Otherwise, I have tried a couple of the other answers and this is the best one. This answer incorporates the idea of using wait <pid> given by @Michael but goes a step further by using the trap command which seems to work best.

#!/usr/bin/env bash

set -m # allow for job control
EXIT_CODE=0;  # exit code of overall script

function handleJobs() {
     for job in `jobs -p`; do
         echo "PID => ${job}"
         CODE=0;
         wait ${job} || CODE=$?
         if [[ "${CODE}" != "0" ]]; then
         echo "At least one test failed with exit code => ${CODE}" ;
         EXIT_CODE=1;
         fi
     done
}

trap 'handleJobs' CHLD  # trap command is the key part
DIRN=$(dirname "$0");

commands=(
    "{ echo 'a'; exit 1; }"
    "{ echo 'b'; exit 0; }"
    "{ echo 'c'; exit 2; }"
)

clen=`expr "${#commands[@]}" - 1` # get length of commands - 1

for i in `seq 0 "$clen"`; do
    (echo "${commands[$i]}" | bash) &   # run the command via bash in subshell
    echo "$i ith command has been issued as a background job"
done

wait; # wait for all subshells to finish

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end

thanks to @michael homer for getting me on the right track, but using trap command is the best approach AFAICT.

6
  • 1
    You can also use SIGCHLD trap to process the children when they exit, such as printing out the status at that time. Or updating a progress counter: declare a function then use "trap function_name CHLD" though that may also require an option to be turned on in a non-interactive shell, such as possibly "set -m"
    – Chunko
    Commented Feb 12, 2017 at 13:42
  • 1
    Also "wait -n" will wait for any child and then return the exit status of that child in the $? variable. So you can print progress as each one exits. However note that unless you use the CHLD trap, you may miss some child exits that way.
    – Chunko
    Commented Feb 12, 2017 at 13:52
  • @Chunko thanks! that is good info, could you maybe update the answer with something you think is best? Commented Feb 12, 2017 at 20:04
  • thanks @Chunko, trap works better, you're right. With wait <pid>, I got fallthrough. Commented Feb 13, 2017 at 9:37
  • Can you explain how and why you believe the version with the trap is better than the one without it?  (I believe that it’s no better, and therefore that it is worse, because it is more complex with no benefit.) Commented Mar 29, 2018 at 6:54
2

Quick and dirty solution:

for job in `jobs -p`; do wait $job || exit; done

Exits the script if any asynchronous job fails


Disclaimer: This will not check the status codes of jobs that completed very quickly (i.e. before the line is called)

1

Another variation of @rolf 's answer:

Another way to save the exit status would be something like

mkdir /tmp/status_dir

and then have each script

script_name="${0##*/}"  ## strip path from script name
tmpfile="/tmp/status_dir/${script_name}.$$"
do something
rc=$?
echo "$rc" > "$tmpfile"

This gives you a unique name for each status file including the name of the script which created it and its process id (in case more than one instance of the same script is running) which you can save for reference later and puts them all in the same place so you can just delete the whole subdirectory when you're done.

You can even save more than one status from each script by doing something like

tmpfile="$(/bin/mktemp -q "/tmp/status_dir/${script_name}.$$.XXXXXX")"

which creates the file as before, but adds a unique random string to it.

Or, you can just append more status information to the same file.

1

I think this might be the simplest one:

cmd1 & pids+="$! "
cmd2 & pids+="$! "
cmd3 & pids+="$! "

for p in $pids; do wait $p || err=$?; done

[[ $err ]] && echo "ERROR: $err"

It checks if there was any error in the background process. One can easily improve it if errors need to be collected.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .