Shell script patterns for bash
I write a lot of shell scripts using bash to glue together existing programs and their data. I have noticed that some patterns have emerged that I very often use when writing these scripts nowadays. Let the following script to demonstrate:
Fail fast in case of errors
Lines 3
–5
are related to handling erroneous situations. If any of the situations happen that these settings have effect on, they will exit the shell script and prevent it running further. They can be combined to set -ueo pipefail
as a shorthand notation.
A very common mistake is to have an undefined variable, by just forgetting to set the value, or mistyping a variable name. set -u
at line 3
prevents these kind of issues from happening. Other very common situation in shell scripts is that some program is used incorrectly, it fails, and then rest of the shell script just continues execution. This then causes hard to notice failures later on. set -e
at line 4
prevents these issues from happening. And because life is not easy in bash, we need to ensure that such program execution failures that are part of pipelines are also caught. This is done by set -o pipefail
on line 5
.
Clean up after yourself even in unexpected situations
It’s always a good idea to use unique temporary files or directories for anything that can be created during runtime that is not a specific target of the script. This makes sure that even if many instances of the script are executed in parallel, there won’t be any race condition issues. mktemp
command on line 14
shows how to create such temporary files.
Creating temporary files and directories poses a problem where they may not be deleted if the script is exited too early or if you forget to do a manual cleanup. cleanup()
function shown at lines 15
–18
includes code that makes sure that the script cleans up after itself and trap cleanup EXIT
makes sure that cleanup()
is implicitly called in any situation where this script may exit.
Executing commands
Something that is often seen in shell scripts that when you need to create a variable that has a command with arguments, you then execute the variable as it is ($COMMAND
) without using any safety mechanisms against input that can lead into unexpected results in the shell script. Lines 9
COMMAND=("$@")
and 24
"${COMMAND[@]}"
show a way to assign a command into an array and run such array as command and its parameters. Line 25
then includes a pipe just to demonstrate that set -o pipefail
works if you pass a command that fails.
Script directory reference
Sometimes it’s very useful to access other files in the same directory where the executed shell script resides in. Line 11
DIR=$(dirname "$(readlink -f "$0")")
basically gives the absolute path to the current shell script directory. The order of readlink
and dirname
commands can be changed depending if you want to access the directory where the command to execute this script points to or where the script actually resides in. These can be different locations if there is a symbolic link to the script. The order shown above points to the directory where the script is physically located at.
Quotes are not always needed
Normally in bash if you reference variables without enclosing them into quotes, you risk of shell injection attacks. But when assigning variables in bash, you don’t need to have quotes in the assignment as long as you don’t use spaces. The same applies to command substitution (some caveats with variable declarations apply). This style is demonstrated on lines 7
ITERATIONS=$1
and 11
DIR=$(...)
.
Shellcheck your scripts
One cool program that will make you a better shell script writer is ShellCheck. It basically has a list of issues in shell scripts that it detects and can notify you whenever it notices issues that should be fixed. If you are writing shell scripts as part of some version controlled project, you should add an automatic ShellCheck verification step for all shell scripts that you put into your repository.