Skip to content
Go back

Techniques for Writing Robust, Reliable Bash Scripts

· Updated:
By SumGuy 5 min read
Techniques for Writing Robust, Reliable Bash Scripts

Safer Scripting: Because Who Needs Errors, Anyway?

When it comes to bash scripting, safety should always be your top priority. After all, you don’t want your script to go rogue and wreak havoc on your system, do you? To avoid common errors and ensure your script behaves as expected, start with a solid foundation. Here’s a prolog that’ll take care of two very common mistakes:

#!/bin/bash
set -o nounset
set -o errexit

The first line, #!/bin/bash, specifies the interpreter that should be used to run your script. The second line, set -o nounset, prevents your script from referencing undefined variables, which can lead to unexpected behavior. The third line, set -o errexit, ensures that your script exits immediately if any command fails, preventing further errors from propagating.

But what if you need to tolerate a failing command? No worries! You can use the following idiom to ignore errors:

if ! <possible failing command> ; then
echo "failure ignored"
fi

Some Linux commands also have options that can suppress failures. For example, mkdir -p will create a directory and its parents if they don’t exist, without complaining if the directory already exists. Similarly, rm -f will remove a file without prompting for confirmation, even if the file doesn’t exist.

Additional Tips and Tricks:

Next, I’ll move on to the section on functions in bash. Here’s the rewritten section:

Functions in Bash: Because Code Reuse is a Good Thing

Bash functions are a great way to organize your code and make it more reusable. They’re essentially blocks of code that can be called multiple times from different parts of your script. Here’s an example of a simple function that extracts comments from a file:

ExtractBashComments() {
egrep "^#"
}

You can call this function like any other command, passing a file as an argument:

cat myscript.sh | ExtractBashComments

Or, you can use it in a pipeline:

comments=$(ExtractBashComments < myscript.sh)

Additional Tips and Tricks:

The Gotchas That’ll Ruin Your Tuesday

Functions look clean on paper. Then you run your script in CI at 2 AM and discover bash was waiting to humble you. Here are the ones that get people the most.

local doesn’t inherit errexit

This one is subtle and genuinely evil. When you use local to declare and assign a variable in one line, the assignment’s exit code is always 0 — even if the right-hand side fails. Your set -o errexit won’t save you here.

Terminal window
#!/bin/bash
set -euo pipefail
get_hostname() {
local hostname=$(some_command_that_fails) # this does NOT abort the script
echo "$hostname"
}

The fix is to split the declaration and assignment:

Terminal window
get_hostname() {
local hostname
hostname=$(some_command_that_fails) # NOW errexit fires on failure
echo "$hostname"
}

One extra line. Saves you an hour of “but why didn’t it exit??”

Subshell variable scope will gaslight you

Functions that modify variables only affect the current shell — but pipelines spawn subshells, and that’s where things get weird. If you pipe into a while read loop, any variables you set inside it vanish the moment the loop exits.

Terminal window
count=0
cat some_file.txt | while read -r line; do
count=$((count + 1)) # increments inside the subshell, not in your script
done
echo "Lines: $count" # prints 0. every. time.

Use process substitution instead:

Terminal window
count=0
while read -r line; do
count=$((count + 1))
done < <(cat some_file.txt)
echo "Lines: $count" # actually works

Trap your exits like you mean it

If your script creates temp files, locks, or modifies shared state, you need a cleanup trap. Without one, a Ctrl+C or an uncaught error leaves debris behind.

Terminal window
TMPFILE=$(mktemp)
cleanup() {
rm -f "$TMPFILE"
}
trap cleanup EXIT # fires on exit, error, AND Ctrl+C
# rest of your script
echo "doing work with $TMPFILE"

trap ... EXIT is the one you want. It covers normal exits, errexit kills, and signals. You can stack multiple cleanup actions by defining them all in the cleanup function.

Quote everything. No, actually everything.

"$variable" vs $variable seems like nitpicking until a filename with a space in it blows up your deploy script. The rule is simple: if it’s a variable, quote it. If it’s a glob, know what you’re doing. Everything else — quote it anyway.

Terminal window
# Wrong — breaks on filenames with spaces
for f in $files; do rm $f; done
# Right
for f in "${files[@]}"; do rm "$f"; done

Arrays with [@] and proper quoting are a bit more to type. Your 3 AM self will still thank you.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Switch Ubuntu to Hardware Enablement (HWE)
Next Post
Text Generation Web UI vs KoboldCpp: Power User LLM Interfaces

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts