Putorius
Bash Scripting, Featured

Using Trap to Exit Bash Scripts Cleanly

We have all been there, you write a bash script that creates some temporary files and/or directories for processing some information. You complete your script with some basic testing and all is well. You set your cron job to run the script every day at noon. Six days later you realize that the script has been exiting prematurely and leaving a bunch of trash files in the file system. Or worse, potentially sensitive data is left unprotected. Enter the trap command.

The above scenario is a bit dramatic, but not completely ridiculous. If you are automating tasks with bash scripts you will eventually run into a premature exit. Either as the result of an error, a change in the environment, or an unanticipated user action. Creating scripts that deal with this scenario is imperative to keeping a clean and secure system.

What is Trap?

Trap is a bash builtin command that is used to respond to process signals. A signal is a notification sent to a process to notify it of an event.

The most common signal people are familiar with is SIGINT (Signal Interrupt) which is sent when you press CTRL+C to interrupt a process on the command line.

There are a lot of signals available to the operating system. For this tutorial we will focus on EXIT. The exit signal is a bash builtin and can be used to catch any signal. For all intents and purposes, if you trap exit, it will be executed when the shell process terminates.

Exit the shell, returning a status of n to the shell’s parent. If n is omitted, the exit status is that of the last command executed. Any trap on EXIT is executed before the shell terminates.

-Bash Manual

Display a List of Signal Names and Their Numbers

You can display a list of signals, and their associated numbers, by using the trap command list option ( -l ).

The Wrong Way to Clean Up on Exit

If you have been around long enough you have seen scripts that use the rm command at the end to clean up temporary files.

#!/bin/bash
yum -y update > /tmp/output.txt
if grep -qi "kernel" /tmp/output.txt; then
mail -s "KERNEL UPDATED" user@example.com < /tmp/output.txt
fi
rm -f /tmp/output.txt

This is usually a sign of someone trying to clean up after themselves. Cleaning up is a good thing, just not executed correctly in this case. If anything before the rm command exits for any reason the output.txt file will not be deleted. This could get out of hand if the script uses mktemp or a dated filename. It may also be a security concern is sensitive data is in the temporary file. Additionally, if you write a long script that has several exit points, you will have to keep track of each exit and ensure you cleanup.

Using Trap on EXIT

The basic syntax for trap is:

trap [action] [signal]

or written as the man page calls it:

trap [-lp] [[arg] sigspec ...]

Basic Trap for File Cleanup

Using an trap to cleanup is simple enough. Here is an example of using trap to clean up a temporary file on exit of the script.

#!/bin/bash
trap "rm -f /tmp/output.txt" EXIT
yum -y update > /tmp/output.txt
if grep -qi "kernel" /tmp/output.txt; then
mail -s "KERNEL UPDATED" user@example.com < /tmp/output.txt
fi

NOTE: It is important that the trap statement be placed at the beginning of the script to function properly. Any commands above the trap can exit and not be caught in the trap.

Now if the script exits for any reason, it will still run the rm command to delete the file. Here is an example of me sending SIGINT (CTRL+C) to interrupt the script while it is running.

# ./test.sh
^Cremoved ‘/tmp/output.txt’

If we did not use trap here, the output.txt file would be left on the system.

NOTE: I added verbose ( -v ) output to the rm command so it prints "removed". The ^C signifies where I hit CTRL+C to send SIGINT.

This is a much cleaner and safer way to ensure the cleanup occurs when the script exists. Using EXIT ( 0 ) instead of a single defined signal (i.e. SIGINT - 2) ensures the cleanup happens on any exit, even successful completion of the script.

Using Traps to Ensure Service Availability

You can use traps for more than file cleanup. Imagine a scenario where you created a script to stop a service to do some automated task. If one of the commands exits during the script, then the service would remained stopped. You can use the trap on EXIT to ensure the service comes back up and is available.

Here we will stop the smb (SAMBA) service to create a tar archive of our share. We will use trap to ensure the service starts, even if the archive procedure exits or the script is otherwise interrupted.

#!/bin/bash
# Set trap on EXIT for service
trap "systemctl start smb.service" EXIT

# Stop service
systemctl stop smb.service

# Create the tarball
tar czf windows_share.tar.gz /srv/WindowsTeam/

# There is no need to restart the service in the script,
# the trap will catch any exit, even successful

This may not be the most glamorous example. But I think it delivers the point.

Ensure a Port is Closed After Script Completion

If you run a script that needs a port opened on the firewall, you don't want that script to exit and leave the port open. Use the trap command.

Here is an example of me opening a port to let my kids watch movies on my plex server for 2 hours. After two hours the ports close up for their roku device and it's time to read (I am a cord cutter - cable TV rots the brain).

#!/bin/bash
trap "iptables -D INPUT -p tcp -s 10.0.0.222 --dport 32400 -j ACCEPT" EXIT
echo "Opening ports for kiddos 2 hours of TV time"
iptables -I INPUT -p tcp -s 10.0.0.222 --dport 32400 -j ACCEPT
sleep 7200

This is just a small example not my actual script, the real on is more involved than this with a countdown timer so I know when to enforce the reading rule. Leave a comment below if the real script is of interest to you.

Using a Function in a Trap Call

If you want to run multiple commands in a trap it may be cleaner to put those commands into a function, then simply call the function in the trap statement. We will use our previous examples for illustration.

function egress {
rm -f /tmp/output.txt
systemctl start smb.service
iptables -D INPUT -p tcp -s 10.0.0.222 --dport 32400 -j ACCEPT
}
# Call the egress function
trap egress EXIT

# Script contents
# Do some stuff
# Then some more stuff
# Do stuff to the stuff already done
# etc...

# When the script is completed or exits for any reason, the
# commands in the egress function will be executed.

Here we have multiple actions in the egress function. We then call that function in the trap statement. When the script exits, the egress function will run.

Conclusion

At the beginning of my Linux experience I spent a lot of time dealing with the fallout of premature script exits. The trap command is a simple and effective way to ensure your bash scripts exit cleanly.

In this tutorial we listed all the signals you can trap and there are more bash builtins that we did not even mention. I will save those for a future tutorial (DEBUG anybody?).

We would love to hear the ways you use traps, please comment below if you have some to share!

Resources

Exit mobile version