Enhancing Bash Script Reliability with set -xeuo pipefail

April 7, 2024 by Sharjeel Aziz5 minutes

Bash scripts are an essential tool for automating tasks and streamlining workflows and there are some key commands that can enhance the reliability, security, and debuggability of your scripts.

In this post, we’ll explore four such commands: set -e, set -u, set -o pipefail, and set -x, and see how they can help prevent real-world script failures. These commands really enhance scripts that are part of container images, most commonly, entry point scripts where increased control over how the script behaves is required.

1. Exit on Error: set -e

The set -e command is a game-changer when it comes to error handling in Bash scripts. By including this command at the beginning of your script, you instruct Bash to immediately exit if any command returns a non-zero exit status–an indicator of an error. This early exit strategy helps in addressing and identying errors early, preventing them from escalating.

Without set -e example:

#!/bin/bash

# script without set -e
rm important_file
cp backup_file important_file

If the rm command fails (e.g., if important_file doesn’t exist), the script will continue executing, potentially leading to unexpected behavior or data loss. With set -e, the script would exit immediately upon the rm command failing, preventing further issues. There are exceptions to set -e option though, for instance, commands following if statements or in a list following && or ||, do not cause the script to exit on failure.

terminal showing bash scripts results

Bash Manual: “Exit immediately if a pipeline, which may consist of a single simple command, a list, or a compound command returns a non-zero status” GNU Bash Manual, 4.3.1 The Set-Builtin

2. Treat Unset Variables as Errors: set -u

Another powerful command is set -u, which tells Bash to treat any reference to an unset variable as an error. This feature is invaluable for catching typos, forgotten variable initialization, and other subtle bugs that can be difficult to diagnose.

Without `set -u’ example:

#!/bin/bash

# script without set -u
config_file="path/to/config"
rm "$config_filee"

Here, $config_filee is a typo and refers to an unset variable. Without set -u, the script would silently continue, potentially deleting the wrong file or causing other unintended consequences. With set -u, the script would halt immediately, alerting you to the typo.

Bash Manual: “Treat unset variables and parameters other than the special parameters ‘@’ or ‘*’ as an error when performing parameter expansion” GNU Bash Manual, 4.3.1 The Set-Builtin

3. Handle Pipeline Failures: set -o pipefail

Consider a script that processes a file, extracts specific lines, and then counts the number of occurrences of a particular pattern. The script is working with an existing file and a file that does not exist:

#!/bin/bash

# script without set -o pipefail
cat doesnotexist.txt | grep "error" | wc -l
echo "Processing complete for file that does not exist. Exit status: $?"

cat file.txt | grep "error" | wc -l
echo "Processing complete. Exit status: $?"

When we run the script, we get the following output:

scripts/set/logs on main [✘!?] 
➜ ./process.sh 
cat: doesnotexist.txt: No such file or directory
0
Processing complete for file does not exist. Exit status: 0
2
Processing complete. Exit status: 0

Even though cat failed because the file doesn’t exist, the pipeline’s exit status is 0 because the last command (wc -l) succeeded.

Now, let’s modify the script to use set -o pipefail and demonstrate how it affects the return status:

#!/bin/bash

# script with set -o pipefail
set -o pipefail

cat doesnotexist.txt | grep "error" | wc -l
echo "Processing complete for file does not exist. Exit status: $?"

# file exists
cat file.txt | grep "error" | wc -l
echo "Processing complete. Exit status: $?"

When we run this script, we get the following output:

scripts/set/logs on main [✘!?] 
➜ ./process_pipefail.sh 
cat: doesnotexist.txt: No such file or directory
0
Processing complete for file does not exist. Exit status: 1
2
Processing complete. Exit status: 0

With set -o pipefail, the pipeline’s exit status is now 1, reflecting the failure of the cat command. This behavior allows us to detect failures in any command within the pipeline.

This example demonstrates how set -o pipefail can help you catch errors in your pipelines and prevent your scripts from producing misleading results. Here is an interesting article article: PIPEFAIL: How a missing shell option slowed Cloudflare down

Bash Manual: “If set, the return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status, or zero if all commands in the pipeline exit successfully” GNU Bash Manual, 4.3.1 The Set-Builtin.

4. Enable Debugging Mode: set -x

Debugging Bash scripts can be a challenging task, but set -x makes it much more manageable. This command enables a debugging mode that prints each command in your script to the console as it’s executed, along with its arguments. This feature provides valuable insights into the flow and state of your script, making it easier to identify and troubleshoot issues.

Suppose you have a complex script with multiple conditionals and loops:

#!/bin/bash

# Script without set -x
for file in "$@"; do
    if [ -f "$file" ]; then
        process_file "$file"
    fi
done
blog/scripts/set on main [✘!?] 
➜ ./script_without_x.sh logs

If the script isn’t behaving as expected, adding set -x at the beginning will print out each command as it’s executed, helping you pinpoint where the issue lies.

blog/scripts/set on main [✘!?] 
➜ ./script_with_x.sh logs
+ for file in "$@"
+ '[' -f logs ']'

Bash Manual: “Print a trace of simple commands, and their arguments after they are expanded and before they are executed” GNU Bash Manual, 4.3.1 The Set-Builtin.

To get the most out of your Bash scripts, consider using these commands in combination. They are not mutually exclusive and can work together to create robust, secure, and easily debuggable scripts. By incorporating set -e, set -u, set -o pipefail, and set -x into your scripting practices, you’ll be equipped with a powerful toolset for writing high-quality Bash scripts that can handle real-world challenges.

Further Reading: