Bash scripts aren’t just for gluing commands together; they’re full-fledged programs that can be surprisingly brittle if you don’t treat them with respect.

Let’s see a script in action that handles some common pitfalls. Imagine we have a script that needs to process a file, but it also needs to create a temporary directory and clean it up afterward.

#!/bin/bash

# Exit immediately if a command exits with a non-zero status.
set -e
# Treat unset variables as an error and exit immediately.
set -u
# Pipe commands should propagate exit statuses.
set -o pipefail

# Check if input file is provided
if [ "$#" -ne 1 ]; then
    echo "Usage: $0 <input_file>"
    exit 1
fi

INPUT_FILE="$1"
TEMP_DIR=$(mktemp -d)

# Ensure cleanup even if script fails
trap "rm -rf $TEMP_DIR" EXIT

# Check if input file exists and is readable
if [ ! -f "$INPUT_FILE" ] || [ ! -r "$INPUT_FILE" ]; then
    echo "Error: Input file '$INPUT_FILE' not found or not readable."
    exit 1
fi

echo "Processing file: $INPUT_FILE"
echo "Temporary directory created: $TEMP_DIR"

# Simulate some processing
echo "Hello" > "$TEMP_DIR/temp_output.txt"
cat "$INPUT_FILE" >> "$TEMP_DIR/temp_output.txt"

# Example of a command that might fail (uncomment to test)
# grep "nonexistent_pattern" "$TEMP_DIR/temp_output.txt"

echo "Processing complete. Output in $TEMP_DIR/temp_output.txt"

# The trap will automatically clean up TEMP_DIR on exit
exit 0

This script is designed to be robust. It checks for necessary arguments, ensures files are accessible, creates a secure temporary directory, and crucially, guarantees that directory is removed no matter how the script exits.

The core problem this solves is preventing unexpected behavior and resource leaks. A script that crashes midway through might leave temporary files lying around, or worse, might have started a process that never finishes.

Here’s how it works internally:

  • set -e: This is your first line of defense. If any command returns a non-zero exit code (indicating an error), the script immediately terminates. This prevents the script from continuing with corrupted data or in an unexpected state.
  • set -u: This catches typos in variable names or attempts to use variables that haven’t been assigned a value. Without it, you might be operating on an empty string or a completely wrong variable, leading to silent, hard-to-debug errors.
  • set -o pipefail: This is vital for pipelines. Normally, if any command in a pipeline fails, the exit status of the entire pipeline is the exit status of the last command, even if it succeeded. pipefail makes the pipeline’s exit status the exit status of the first command that failed.
  • mktemp -d: This creates a temporary directory with a unique name. This is safer than just mkdir /tmp/myscript_temp because it avoids race conditions and ensures you aren’t accidentally overwriting someone else’s directory or having your temporary files accessed by other users.
  • trap "rm -rf $TEMP_DIR" EXIT: This is the cleanup guarantee. The trap command registers a command to be executed when a specific signal is received. EXIT is a special signal that is sent just before the script exits, regardless of whether it exits successfully, due to an error (set -e), or an uncaught signal. The rm -rf command then removes the temporary directory and its contents.

The most surprising thing to many is that trap with EXIT is often the only reliable way to guarantee cleanup of resources like temporary files or network connections, especially when dealing with signals like INT (Ctrl+C) or TERM which might also be trapped.

The next concept you’ll likely encounter is more sophisticated error handling and logging, moving beyond simple echo statements to structured output or integration with centralized logging systems.

Want structured learning?

Take the full Linux & Systems Programming course →