Course Outline (Part 9)

In Part 9 of the Linux Bash Course, we cover the patterns required to build resilient, production-ready shell utilities. You will learn exit enforcement configurations, signal handling using traps, date conversions and execution profiling, and how to prevent race conditions using secure temporary workspaces and locks.

For more details on error trapping, read the GNU Bash Reference Manual on Signals.


Chapter 25: Error Handling & Debugging

25.1 Exit on Error: set -e

By default, Bash continues running a script even if a command inside it fails. To change this behavior and force the script to exit immediately if any command returns a non-zero exit status, use the set -e option (also called errexit).

  • Syntax:
    set -e
    # To disable
    set +e
  • Example Command:
    cat << 'EOF' > error_test.sh
    #!/bin/bash
    set -e
    ls /invalid_directory_path
    echo "This line will not run because of set -e."
    EOF
    bash error_test.sh
  • Expected Output:
    ls: cannot access '/invalid_directory_path': No such file or directory
  • Flag & Command Breakdown:
    • set -e: Instructs the shell to exit immediately if a pipeline, list, or compound command returns an error status.

25.2 Undefined Variables Fail: set -u

Referencing an undefined variable in Bash evaluates to an empty string by default, which can cause issues (e.g., rm -rf $UNSET_VAR/* resolving to rm -rf /*). To prevent this, use set -u (also called nounset), which causes the script to throw an error and exit immediately if it encounters an undefined variable.

  • Syntax:
    set -u
  • Example Command:
    cat << 'EOF' > unset_test.sh
    #!/bin/bash
    set -u
    echo "Database: $DB_HOST"
    EOF
    bash unset_test.sh
  • Expected Output:
    unset_test.sh: line 3: DB_HOST: unbound variable

25.3 Printing Commands Before Execution: set -v

The set -v (verbose) option instructs the shell to print each line of code to stderr before evaluating it. This is useful for tracing the control flow of large scripts.

  • Syntax:
    set -v
  • Example Command:
    cat << 'EOF' > verbose_test.sh
    #!/bin/bash
    set -v
    MSG="Initializing System"
    echo $MSG
    EOF
    bash verbose_test.sh
  • Expected Output:
    MSG="Initializing System"
    echo $MSG
    Initializing System

25.4 The trap Command (Catching Signals)

The trap command allows your script to catch OS signals (like SIGINT when pressing Ctrl+C, SIGTERM on system shutdowns, or EXIT when a script finishes) and execute a custom function or command in response.

  • Syntax:
    trap "commands_or_function" SIGNAL_NAME
  • Example Command:
    # Catch SIGINT (Ctrl+C)
    trap "echo ' Caught interrupt signal! Exiting...'; exit 1" SIGINT
    # Simulate signal after 1 second in a subshell
    (sleep 1 && kill -s SIGINT $$) &
    sleep 3
  • Expected Output:
     Caught interrupt signal! Exiting...
  • Flag & Command Breakdown:
    • kill -s SIGINT $$: Sends a SIGINT interrupt signal to the parent shell process ($$).

25.5 Cleanup on Script Exit

A common use case for the trap command is executing cleanup tasks (like deleting temporary files or stopping background processes) whenever the script exits, whether it completes successfully or exits early due to an error.

  • Syntax:
    trap "cleanup_function" EXIT
  • Example Command:
    cat << 'EOF' > exit_trap_test.sh
    #!/bin/bash
    cleanup() {
        echo "Running cleanup tasks..."
    }
    trap cleanup EXIT
    echo "Running main script operations..."
    exit 0
    EOF
    bash exit_trap_test.sh
  • Expected Output:
    Running main script operations...
    Running cleanup tasks...

25.6 Custom Error Messages

Writing helper functions to format error output makes debugging much easier. You should redirect these messages to standard error (>&2) to separate them from standard output.

  • Example Script:
    #!/bin/bash
    # Print error to stderr and exit
    log_err() {
        echo -e "[ERROR] $(date): $1" >&2
        exit 1
    }
    
    if [[ ! -f "config.json" ]]; then
        log_err "Config file config.json is missing!"
    fi

25.7 Logging Errors to a File

You can redirect errors to a central log file by appending standard error output (using 2>>) to a log file path.

  • Syntax:
    command 2>> error_log.txt
  • Example Command:
    # Force an error and save to log file
    ls /nonexistent_folder 2>> script_errors.log
    cat script_errors.log
    rm -f script_errors.log
  • Expected Output:
    ls: cannot access '/nonexistent_folder': No such file or directory

25.8 Debugging with trap DEBUG

In Bash, trapping the DEBUG signal causes the specified command to run before every command in the script. This can be used to print variables or inspect the execution state line-by-line.

  • Example Command:
    cat << 'EOF' > debug_trap.sh
    #!/bin/bash
    # Print the line number about to be executed
    trap 'echo "Executing line: $LINENO"' DEBUG
    X=10
    Y=20
    EOF
    bash debug_trap.sh
  • Expected Output:
    Executing line: 4
    Executing line: 5
  • Flag & Command Breakdown:
    • $LINENO: A built-in shell variable that returns the line number of the script currently being executed.

25.9 Using bashdb (Bash Debugger)

For complex scripts, manual debugging with echo and set -x can be slow. bashdb is a third-party source-code debugger for Bash that provides advanced debugging tools similar to gdb or pdb.

  • Key Features:
    • Set breakpoints at specific lines or conditions.
    • Step into, over, or out of functions.
    • Inspect variables and call stacks interactively during execution.
    • Run scripts with: bashdb ./your_script.sh

25.10 Assertions and Input Sanitization

Assertions validate that script inputs and variables meet expected formats and constraints before running any commands. This is critical for preventing security vulnerabilities like command injection.

  • Example Script:
    #!/bin/bash
    # Sanitize user inputs
    read -p "Enter backup name: " BACKUP_NAME
    
    # Assert input contains only alphanumeric characters
    if [[ ! "$BACKUP_NAME" =~ ^[a-zA-Z0-9_]+$ ]]; then
        echo "Error: Forbidden characters detected!" >&2
        exit 1
    fi
    echo "Creating backup: ${BACKUP_NAME}.tar.gz"

Chapter 26: Working with Dates & Times

26.1 The date Command Formatting

The date command prints or sets the system date and time. You can format the output using conversion specifiers prefixed by a + sign.

  • Syntax:
    date +%format_specifier
  • Example Command:
    date "+%Y-%m-%d %H:%M:%S"
  • Expected Output:
    2026-06-12 13:45:00
  • Common Format Specifiers:
    • %Y: 4-digit Year (e.g., 2026).
    • %m: Month (01 to 12).
    • %d: Day of the month (01 to 31).
    • %H: Hour in 24-hour format (00 to 23).
    • %M: Minute (00 to 59).
    • %S: Second (00 to 59).

26.2 Getting Unix Timestamps

A Unix timestamp (Epoch time) is the number of seconds that have elapsed since January 1, 1970 (00:00:00 UTC). To print the current Unix timestamp, use the %s format specifier.

  • Syntax:
    date +%s
  • Example Command:
    date +%s
  • Expected Output:
    1778683500

26.3 Date Arithmetic (Yesterday, Tomorrow)

On GNU systems, the date command can calculate offset times using the -d or --date flag.

  • Syntax:
    date -d "offset description"
  • Example Command:
    date -d "yesterday" +%Y-%m-%d
    date -d "2 weeks ago" +%Y-%m-%d
    date -d "tomorrow" +%Y-%m-%d
  • Expected Output:
    2026-06-11
    2026-05-29
    2026-06-13
  • Flag & Command Breakdown:
    • -d: Evaluates the user-specified date string instead of using the current system time.

26.4 Converting Between Timezones

You can display a date in a specific timezone by temporarily overriding the TZ environment variable before running the date command.

  • Syntax:
    TZ="Timezone_Region" date
  • Example Command:
    TZ="America/New_York" date
    TZ="Europe/London" date
  • Expected Output:
    Fri Jun 12 04:15:00 EDT 2026
    Fri Jun 12 09:15:00 BST 2026

26.5 Using date in Scripts for Logging

Standardizing date formats in log messages makes it much easier to parse and analyze script logs later.

  • Example Snippet:
    log_info() {
        local TIMESTAMP
        TIMESTAMP=$(date "+%Y-%m-%dT%H:%M:%S%z")
        echo "[INFO] [$TIMESTAMP]: $1"
    }
    log_info "Database connection established."

26.6 Measuring Script Execution Time

You can measure the execution time of a script or command by capturing the Unix timestamp before and after the operation and calculating the difference.

  • Example Script:
    #!/bin/bash
    START_TIME=$(date +%s)
    
    # Simulate work
    sleep 2
    
    END_TIME=$(date +%s)
    ELAPSED=$((END_TIME - START_TIME))
    echo "Task completed in: $ELAPSED seconds."

26.7 The time Command (Real, User, Sys)

The time command measures how long a utility takes to execute, outputting:

  • real: The total elapsed wall-clock time from start to finish.

  • user: The CPU time spent running the process in user-space.

  • sys: The CPU time spent running inside the kernel on behalf of the process.

  • Syntax:

    time command
  • Example Command:

    time sleep 1
  • Expected Output:

    real    0m1.002s
    user    0m0.000s
    sys     0m0.001s

26.8 Cron-like Scheduling with at

The at utility is used to schedule one-off commands to run at a specific time in the future. Unlike cron, which runs tasks on a recurring schedule, at only runs a task once.

  • Syntax:
    echo "command" | at time_specification
  • Example Command:
    echo "touch /tmp/at_ran.txt" | at now + 1 minute
  • Expected Output:
    warning: commands will be executed using /bin/sh
    job 3 at Fri Jun 12 13:46:00 2026

26.9 Sleep and Delays: sleep and usleep

  • sleep: Pauses execution for a specified number of seconds. Modern GNU sleep also supports decimal numbers (e.g. 0.5 seconds).

  • usleep (microsecond sleep): Pauses execution for a specified number of microseconds (1 million microseconds = 1 second).

  • Syntax:

    sleep [seconds]
    usleep [microseconds]
  • Example Command:

    # Pause for 1.5 seconds
    sleep 1.5
    echo "Awake!"
  • Expected Output:

    Awake!

26.10 High-Resolution Timing

For precise performance profiling, you can get nanosecond-level timestamps using the %N specifier with the date command.

  • Syntax:
    date +%s.%N
  • Example Command:
    START=$(date +%s.%N)
    sleep 0.1
    END=$(date +%s.%N)
    # Calculate duration using bc
    DURATION=$(echo "$END - $START" | bc)
    echo "Duration: $DURATION seconds"
  • Expected Output:
    Duration: .102482391 seconds

Chapter 27: Working with Temporary Files

27.1 Creating Temp Files: mktemp

Hardcoding temporary filenames (like /tmp/temp.txt) is unsafe because multiple instances of the script running concurrently can overwrite each other’s data, and it opens the door to symlink attacks. Use the mktemp utility to create files with randomized, unique names.

  • Syntax:
    mktemp
  • Example Command:
    TEMP_FILE=$(mktemp)
    echo "Generated: $TEMP_FILE"
    echo "Confidential data" > "$TEMP_FILE"
    cat "$TEMP_FILE"
    rm -f "$TEMP_FILE"
  • Expected Output:
    Generated: /tmp/tmp.aBC12D34e5
    Confidential data

27.2 Creating Temp Directories: mktemp -d

To isolate an entire temporary workspace, use the -d flag with the mktemp command to create a randomized temporary directory.

  • Syntax:
    mktemp -d
  • Example Command:
    TEMP_DIR=$(mktemp -d)
    echo "Created directory: $TEMP_DIR"
    rm -rf "$TEMP_DIR"
  • Expected Output:
    Created directory: /tmp/tmp.dirXyz123
  • Flag & Command Breakdown:
    • -d: Instructs mktemp to create a directory instead of a regular file.

27.3 Using trap to Delete Temp Files

Combining mktemp with trap ensures your script automatically cleans up its temporary files on exit, even if the script crashes or is terminated early.

  • Example Script:
    #!/bin/bash
    # Create temp file securely
    TMP_WORKSPACE=$(mktemp)
    
    # Register automatic cleanup trap
    trap 'rm -f "$TMP_WORKSPACE"; echo "Cleaned up temp files."' EXIT
    
    echo "Processing data in: $TMP_WORKSPACE"
    # Even if an error happens here, the trap runs
    ls /invalid_folder

27.4 Named Pipes (FIFOs)

A Named Pipe (FIFO - First In, First Out) is a special file type that allows different processes to communicate with each other. It behaves like a standard command pipe, but lives on the filesystem.

  • Syntax:
    mkfifo pipe_name
  • Example Command:
    mkfifo /tmp/my_ipc_pipe
    # Start reader in background
    cat /tmp/my_ipc_pipe &
    # Write to pipe
    echo "Hello over pipe" > /tmp/my_ipc_pipe
    # Clean up
    rm -f /tmp/my_ipc_pipe
  • Expected Output:
    Hello over pipe
  • Flag & Command Breakdown:
    • mkfifo: Creates a FIFO special file at the specified path.

27.5 The /dev/shm (RAM Disk)

On Linux systems, the /dev/shm directory maps directly to system memory (RAM). Writing files to /dev/shm is much faster than writing to /tmp because the data is stored in memory, avoiding slow disk writes. This is ideal for high-throughput temp files.

  • Example Command:
    # Create a temp file on the RAM disk
    SHM_FILE=$(mktemp -p /dev/shm)
    echo "RAM storage path: $SHM_FILE"
    rm -f "$SHM_FILE"
  • Expected Output:
    RAM storage path: /dev/shm/tmp.ab12CD34

27.6 Creating Lock Files for Mutual Exclusion

To prevent data corruption, you can use a lock file to ensure only one instance of a script can execute at a time. The script creates a lock file when it starts and deletes it when it exits; if the file already exists, the script exits immediately.

  • Example Script:
    #!/bin/bash
    LOCK_FILE="/tmp/sync_script.lock"
    
    # Try to create the lock file. mkdir is an atomic operation.
    if ! mkdir "$LOCK_FILE" 2>/dev/null; then
        echo "Error: Another instance of this script is already running." >&2
        exit 1
    fi
    
    # Register cleanup trap
    trap 'rmdir "$LOCK_FILE"' EXIT
    
    echo "Running exclusive task..."
    sleep 3

27.7 Using flock for File Locking

The flock utility manages file locks from within shell scripts. Unlike manual lock files, flock is managed by the Linux kernel, which ensures locks are automatically released if the script crashes or is terminated.

  • Syntax:
    flock [flags] lock_file_path command
  • Example Command:
    # Run a command under an exclusive lock file descriptor (descriptor 200)
    (
      flock -x 200
      echo "Start exclusive block"
      sleep 1
      echo "End exclusive block"
    ) 200>/tmp/lock.file
  • Expected Output:
    Start exclusive block
    End exclusive block
  • Flag & Command Breakdown:
    • -x: Acquires an exclusive lock.
    • 200>/tmp/lock.file: Opens a file descriptor (200) pointing to the lock file.

27.8 Race Conditions and Prevention

A race condition occurs when a script’s behavior depends on the order or timing of external events. For example, checking if a file exists before writing to it ([[ ! -f file ]] && touch file) can fail if another process creates the file in the split second between the check and the write.

  • Prevention: Use atomic operations that perform the check and creation at the same time (e.g. mkdir to create directories, or set -o noclobber in Bash to prevent overwriting existing files).

27.9 Cleaning Up Orphaned Temp Files

If a system crashes or loses power, traps won’t run, leaving orphaned files in the temp directory. You should schedule a cron job or startup script to clean up temp files that haven’t been accessed for a certain number of days.

  • Example Command:
    # Find and delete temp files that haven't been accessed in 7 days
    find /tmp/tmp.* -atime +7 -exec rm -f {} \;

27.10 Portable Temp File Patterns

mktemp behaves differently on macOS (BSD-based) than on Linux (GNU-based). To ensure your scripts run portably across platforms, use the following template block:

  • Portable Snippet:
    # Creates a temporary file in a cross-platform compatible way
    TMP_FILE=$(mktemp 2>/dev/null || mktemp -t 'tmp')