Error catching in scripts and pipes
Intro
Error catching in Bash is really useful if you need to run things unattended, on a server or on the cloud. This guide runs through what exit codes are, how they can be used to detect how the commands in your script are doing, and some more complictated examples.
The magic of exit codes
Every time you run a command in Bash, whether in Linux, on a Mac or any other Unix-based system, it exits with an exit code.
What this code is depends on the command that was run, and whether there were any errors.
Exit codes can be accessed by the special variable: $?.
By convention, commands exit with a zero if they completed successfully.
$ echo "This command will run successfully."
This command will run successfully.
$ echo $?
0
If there is an error, you will get a code that isn’t zero, and there are conventions to do with the kind of error that occurred. More info on exit codes are available here: https://www.cyberciti.biz/faq/linux-bash-exit-status-set-exit-statusin-bash/
Note: In bioinformatics, there are lots of people who don’t know what exit codes are, and don’t follow these conventions, so whenever you want to implement any kind of error catching, do try some dummy commands to check that you get the expected exit codes.
$ echo "The following should be error code 2, by convention."
The following should be error code 2, by convention.
$ samtools view ANonExistentFile.bam
[E::hts_open_format] Failed to open file "ANonExistentFile.bam" : No such file or directory
samtools view: failed to open "ANonExistentFile.bam" for reading: No such file or directory
$ echo $?
1
Error catching with exit codes
You can catch an error in a command, and shut your script down using it.
By convention, spitting an error message to STDERR (/dev/stderr) keeps the STDOUT free for normal use (more info here).
Alternatively, you can direct the error logging to a file.
echo "This command succeeds"
if [[ $? != '0' ]]
then
echo "This command failed" > /dev/stderr
exit
fi
cd "This command fails" "because of this bit!"
if [[ $? != '0' ]]
then
echo "This command failed" > /dev/stderr
sleep 10
exit
fi
Sticking the above in a function:
logAndExitIfFail() {
local exitCode=$1
local noKill=$2
# Stick an error in the STDERR if the command succeeded or failed.
# If there is a second argument given to the function, don't exit.
# Otherwise, exit
if [[ $exitCode != '0' ]]
then
echo "This command failed" > /dev/stderr
else
echo "This command succeeded" > /dev/stderr
fi
if [[ -z $noKill ]]
then
exit
fi
}
# Try with a couple of commands
echo "This command succeeds"
logAndExitIfFail $? nokill
cd "This command fails" "because of this bit!"
logAndExitIfFail $? nokill
Exit codes and pipes
If you try and pipe some things together, the $? variable will only store a single exit code, which is not that useful.
If a process fails in the middle of your pipeline, you want to know which one, and what the exit code it gave.
bwa mem FileThatDoesNotExist.fastq \
| samtools view \
-F 4 \
-o alignedFile.bam
echo $?
That’s what pipestatus is for.
After you have run a set of piped commands, the exit codes are stored in the PIPESTATUS array.
These can be grabbed like so:
bwa mem FileThatDoesNotExist.fastq \
| samtools view \
-F 4 \
-o alignedFile.bam \
/dev/stdin
echo "${PIPESTATUS[*]}"
Slapping that into the previous function, you can log the error codes!
logAndExitIfFail() {
local exitCodes=$1
local noKill=$2
# Stick an error in the STDERR if the command succeeded or failed.
# If there is a second argument given to the function, don't exit.
# Otherwise, exit
if [[ $exitCodes != '0' ]]
then
echo "This pipe failed with the following exit codes: $exitCodes" > /dev/stderr
else
echo "This pipe succeeded" > /dev/stderr
fi
# Add all of the exit codes together, to check if any of them were not zero
# Stop now if any exit code isn't zero and the $noKill flag is unset
eCode=$(echo "$exitCodes" | tr -s ' ' + | bc)
if [ $eCode != 0 ] && [ -z "$noKill" ]
then
echo "Exiting" > /dev/stderr
exit
fi
}
# Try with a command
bwa mem FileThatDoesNotExist.fastq \
| samtools view \
-F 4 \
-o alignedFile.bam \
/dev/stdin
logAndExitIfFail "${PIPESTATUS[*]}" nokill