Bash Strict Mode: set -euo pipefail Explained

Bash Strict Mode: set -euo pipefail Explained

What Is Bash Strict Mode?

Conclusion: Put set -euo pipefail at the top of a script to catch errors, unset variables, and pipe failures immediately. The goal is to surface hidden bugs early.

By default, bash is far too forgiving. A command can fail, a variable can be undefined, or a pipe can break midway, and the script keeps running. The result is the classic accident: "it failed partway through but ran to the end and left broken data behind."

Bash strict mode is the well-known idiom of enabling three options at the top of a script to switch bash into "stop on failure" behavior.

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

The four pieces covered here

  • set -e (errexit): exit immediately on command failure
  • set -u (nounset): treat unset variable references as errors
  • set -o pipefail: catch failures inside a pipeline
  • IFS=$'\n\t': prevent word-splitting accidents (optional but recommended)

Assumptions (target environment)

  • Shell: bash (#!/usr/bin/env bash)
  • pipefail is a bash extension. It does not work in POSIX sh (dash, etc.)
  • Intended for scripts, not interactive shells

Why Is Default Bash Dangerous?

Conclusion: Default bash continues after a command fails and treats mistyped variables as empty strings. Failures get ignored while processing marches on, which is where accidents happen.

Look at the script below. It moves into a backup directory and deletes old files, a very common pattern.

#!/usr/bin/env bash
cd "$BACKUP_DIR"
rm -rf ./*

If you forget to set BACKUP_DIR, default bash runs cd "" (which does nothing and stays in the current directory), and rm -rf ./* executes in the directory you are already in. Because the cd failure is ignored, you wipe a place you never intended to.

With strict mode, this accident is blocked twice over.

  • set -u stops the script on the unset $BACKUP_DIR reference
  • Even if it were an empty string, set -e exits immediately when cd fails

What Does set -e (errexit) Do, and Where Does It Fail?

Conclusion: set -e exits the script the moment a command returns non-zero. But there are many contexts where it does not trigger (if conditions, &&, and more), so do not over-rely on it.

set -e terminates the script as soon as a command returns a non-zero exit status.

set -e
cp important.conf /etc/myapp/   # stops here if it fails
systemctl restart myapp          # runs only if the above succeeded

Where errexit does NOT apply

set -e has documented contexts where it has no effect. Not knowing them leads to the "it should have stopped but didn't" accident.

  • The condition of if cmd; then ... (the failure is used for the test)
  • The left side of cmd && ... / cmd || ... (anything but the last command)
  • A command negated with !
  • Every command in a pipeline except the last (covered by pipefail)
set -e
# Does NOT stop even if grep fails (it is a condition)
if grep -q pattern file.txt; then
    echo "found"
fi

# To intentionally tolerate a failure, make it explicit with || true
risky_command || true

Do not rely on set -e alone. Explicitly check the exit status of critical steps. Treat set -e as a safety net for failures you accidentally overlooked.

The Roles of set -u and pipefail

Conclusion: set -u turns unset variable references into errors to catch typos. set -o pipefail catches mid-pipe failures so combinations like grep | sort report success or failure correctly.

set -u (nounset)

Referencing an undefined variable becomes an error and stops the script, letting you spot variable-name typos instantly.

set -u
name="penguin"
echo "$nmae"   # typo -> exits with "unbound variable"

When you intentionally want "use a default if unset," make it explicit with parameter expansion.

# Use a default without erroring when unset
echo "${OPTIONAL_VAR:-default}"

# Same for positional parameters (a guard when $1 is missing)
target="${1:-/tmp}"

set -o pipefail

By default, a pipeline's exit status is that of the last command. Even if an earlier command fails, the whole pipe is reported as success when the last one succeeds.

# Without pipefail: $? is 0 if grep succeeds, even when curl fails
curl -s https://example.com/data | grep "key"

set -o pipefail
# With pipefail: the curl failure propagates as a pipe failure
curl -s https://example.com/data | grep "key"

pipefail returns the exit status of the rightmost command that failed in the pipeline, or 0 if they all succeed. It is especially valuable when chaining data fetching and filtering.

Why Is Setting IFS Recommended?

Conclusion: IFS=$'\n\t' limits the word-splitting separators to newline and tab only, preventing unintended splits on spaces in filenames and paths.

IFS (Internal Field Separator) is the set of characters bash uses to split strings into words. The default is space, tab, and newline. The space is what causes accidents with filenames that contain spaces.

# Default IFS: "my file.txt" splits into two words on the space
for f in $(ls); do
    echo "$f"
done

# IFS=$'\n\t': split only on newline and tab
IFS=$'\n\t'

Aaron Maxwell's "unofficial bash strict mode" recommends this IFS setting alongside set -euo pipefail. Dropping the space from the separators makes splitting in for-loops and expansions behave more intuitively.

Changing IFS has side effects. If some code depends on space-based word splitting (such as reading arrays with read -a), restore IFS locally for that section or skip the setting there.

A Practical Strict Mode Template

Conclusion: Keep a boilerplate that combines the shebang, strict mode, and an error trap so you can always start a safe script with minimal effort.

A template you can use as-is. Showing the failing line with trap makes debugging dramatically easier.

#!/usr/bin/env bash
#
# A safe script skeleton
#
set -euo pipefail
IFS=$'\n\t'

# Print the line number on error
trap 'echo "Error on line $LINENO" >&2' ERR

main() {
    local target="${1:-/tmp}"
    echo "Target: $target"
    # main logic goes here
}

main "$@"

Copy-paste: minimal form

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
Why use `#!/usr/bin/env bash`?

Writing #!/bin/bash directly breaks on systems where bash is installed outside /bin (for example /usr/local/bin). Going through env searches for bash on PATH, improving portability. Because pipefail is bash-specific, it is also important to name bash explicitly rather than #!/bin/sh.

Strict Mode Caveats at a Glance

Conclusion: Strict mode is not a silver bullet. Use it knowing the contexts where set -e does not apply, the side effects of IFS, and the risk of retrofitting it onto existing scripts.

Item Caveat
set -e Has no effect in if conditions, && left sides, and more
set -u Make defaults explicit with ${VAR:-default}
pipefail Bash only. Not available in POSIX sh
IFS=$'\n\t' Watch for side effects if code depends on space splitting
Retrofitting Adding it to an old script surfaces all previously hidden bugs

What not to do

  • Use pipefail in a #!/bin/sh script
  • Reference $1 unchecked under set -u
  • Bolt strict mode onto a large existing script without testing

Next Reading