Stackvora.tech

Bash Script Boilerplate

Robust Bash scripts are foundational for automation, system administration, and CI/CD pipelines. While quick one-liners suffice for simple tasks,…

Robust Bash scripts are foundational for automation, system administration, and CI/CD pipelines. While quick one-liners suffice for simple tasks, production-grade scripts require careful handling of errors, input validation, and clear execution flow. This article details a comprehensive Bash script boilerplate designed to enhance reliability, maintainability, and debuggability, suitable for use in professional environments.

The provided boilerplate incorporates best practices for error handling, variable safety, function structure, and argument parsing, forming a solid base for complex scripting projects. It aims to reduce common pitfalls and standardize script behavior across different systems.

Core Boilerplate Structure and Shebang

Every Bash script begins with a shebang line, specifying the interpreter. #!/usr/bin/env bash is preferred over #!/bin/bash because env locates bash in the user's PATH, making the script more portable across environments where bash might not be in a fixed location like /bin.

#!/usr/bin/env bash

# Global constants (read-only)
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
readonly S SCRIPT_VERSION="1.0.0"

# --- Configuration & State Variables ---
# Default values for command-line options
VERBOSE=0
DRY_RUN=0
CONFIG_FILE=""

# --- Error Handling & Debugging ---
# Enable strict error checking and useful debugging options
set -Eeuo pipefail
IFS=
  

\n\t' # Internal Field Separator set to newline and tab only

# Trap errors, exit, and signals for cleanup
trap cleanup EXIT
trap 'handle_error $LINENO "$BASH_COMMAND"' ERR
trap 'handle_signal SIGINT' SIGINT
trap 'handle_signal SIGTERM' SIGTERM

# --- Functions ---

# Helper function to print messages to stderr
log() {
    local level="$1"
    shift
    local message="$@"
    case "$level" in
        "INFO")    echo "[INFO] $SCRIPT_NAME: $message" >&2 ;;
        "WARN")    echo "[WARN] $SCRIPT_NAME: $message" >&2 ;;
        "ERROR")   echo "[ERROR] $SCRIPT_NAME: $message" >> "$SCRIPT_DIR/error.log" >&2 ;;
        "DEBUG")   if [[ "$VERBOSE" -ge 1 ]]; then echo "[DEBUG] $SCRIPT_NAME: $message" >&2; fi ;;
        *)         echo "[UNKNOWN] $SCRIPT_NAME: $message" >&2 ;;
    esac
}

handle_error() {
    local lineno="$1"
    local command="$2"
    log "ERROR" "Fatal error on line $lineno while executing '$command'."
    # Optionally, dump state or gather more info here
    exit 1
}

handle_signal() {
    local signal="$1"
    log "WARN" "Received signal $signal. Initiating graceful shutdown."
    # Perform quick cleanup for signals, don't re-trap EXIT here to avoid infinite loops
    exit 128 # Standard exit code for signals
}

cleanup() {
    local exit_code="$?"
    if [[ "$exit_code" -ne 0 ]]; then
        log "ERROR" "$SCRIPT_NAME exited with non-zero status: $exit_code."
    else
        log "INFO" "$SCRIPT_NAME completed successfully."
    fi
    # Any other cleanup operations (e.g., removing temporary files)
    # rm -f /tmp/myscript_temp_file_$
}

show_help() {
    echo "Usage: $SCRIPT_NAME [-h|--help] [-v|--verbose] [-d|--dry-run] [-c|--config FILE] <argument>..."
    echo "This is a robust Bash script boilerplate."
    echo ""
    echo "Options:"
    echo "  -h, --help        Show this help message and exit."
    echo "  -v, --verbose     Enable verbose output (can be used multiple times for more verbosity)."
    echo "  -d, --dry-run     Simulate execution without making changes."
    echo "  -c, --config FILE Specify a configuration file."
    echo ""
    echo "Arguments:"
    echo "  <argument>...     One or more required arguments."
    exit 0
}

parse_args() {
    while (( "$#" )); do
        case "$1" in
            -h|--help)
                show_help
                ;;
            -v|--verbose)
                ((VERBOSE++))
                shift
                ;;
            -d|--dry-run)
                DRY_RUN=1
                shift
                ;;
            -c|--config)
                if [[ "$2" ]]; then
                    CONFIG_FILE="$2"
                    shift 2
                else
                    log "ERROR" "Argument for $1 is missing."
                    show_help
                fi
                ;;
            --) # End of all options
                shift
                break
                ;;
            -?*)
                log "ERROR" "Unknown option: $1"
                show_help
                ;;
            *) # Positional arguments
                POSITIONAL_ARGS+=("$1")
                shift
                ;;
        esac
    done

    # Restore positional arguments if needed
    set -- "${POSITIONAL_ARGS[@]}"

    if [[ "${#POSITIONAL_ARGS[@]}" -lt 1 ]]; then
        log "ERROR" "Missing required positional arguments."
        show_help
    fi
}

load_config() {
    if [[ -n "$CONFIG_FILE" ]]; then
        if [[ -f "$CONFIG_FILE" ]]; then
            log "INFO" "Loading configuration from $CONFIG_FILE"
            # Source the config file. Ensure it's trusted and well-formed.
            # Example: source "$CONFIG_FILE"
            # Or parse specific key-value pairs if more robust parsing is needed.
            # For simpler scenarios, sourcing is fine, but be aware of its security implications.
        else
            log "ERROR" "Configuration file not found: $CONFIG_FILE"
            exit 1
        fi
    fi
}

main() {
    # Parse command-line arguments
    declare -a POSITIONAL_ARGS=()
    parse_args "$@"

    # Load configuration
    load_config

    log "INFO" "Script $SCRIPT_NAME (v$SCRIPT_VERSION) started."
    log "DEBUG" "Verbose level: $VERBOSE"
    log "DEBUG" "Dry Run: $DRY_RUN"
    log "DEBUG" "Config File: ${CONFIG_FILE:-"None"}"
    log "DEBUG" "Positional Arguments: ${POSITIONAL_ARGS[*]}"

    # --- Main logic starts here ---
    local first_arg="${POSITIONAL_ARGS[0]}"
    log "INFO" "Processing first argument: '$first_arg'"

    if [[ "$DRY_RUN" -eq 1 ]]; then
        log "INFO" "Dry run mode: No changes will be made."
        echo "Would normally execute: some_command --option '$first_arg'"
    else
        # Example of a command
        # some_command --option "$first_arg" || handle_error $LINENO "some_command"
        echo "Hello, ${first_arg} from $SCRIPT_NAME!"
    fi
    # --- Main logic ends here ---

    log "INFO" "Script finished."
}

# --- Script Entry Point ---
main "$@"

Strict Mode Options: set -Eeuo pipefail

This critical line at the top of the script enforces strict error handling, making scripts more predictable and preventing silent failures:

  • set -E or set -o errtrace: Ensures that the ERR trap is inherited by shell functions. Without this, an error in a function might not trigger the trap. (Bash 4.4+ users might prefer set -o functrace instead, as -E can have subtle interactions with subshells).
  • set -e or set -o errexit: Exits immediately if a command exits with a non-zero status. This is crucial for preventing subsequent commands from executing based on a failed prerequisite. There are exceptions: commands in if, while, until, or the left operand of && or || do not trigger errexit.
  • set -u or set -o nounset: Treats unset variables as an error and exits. This helps catch typos or uninitialized variables, preventing unexpected behavior.
  • set -o pipefail: Returns the exit status of the last command in the pipeline that exited with a non-zero status, or zero if all commands in the pipeline exit successfully. Without pipefail, a pipeline like command1 | command2 would only report the exit status of command2, potentially masking failures in command1.

IFS= \n\t': Setting the Internal Field Separator to only newline and tab (default includes space) helps prevent word splitting issues, especially when iterating over file names that might contain spaces. This improves the reliability of operations involving lists or arrays.

Robust Error and Signal Trapping

Traps allow specific commands or functions to be executed upon certain events. This boilerplate includes traps for:

  • trap cleanup EXIT: The cleanup function runs regardless of how the script exits (successful completion, exit command, or an error that triggers errexit). It's ideal for final logging, releasing locks, or removing temporary files.
  • trap 'handle_error $LINENO "$BASH_COMMAND"' ERR: The handle_error function executes whenever a command exits with a non-zero status. $LINENO provides the line number where the error occurred, and "$BASH_COMMAND" provides the command itself, which are invaluable for debugging.
  • trap 'handle_signal SIGINT' SIGINT: Catches Ctrl+C (interrupt signal).
  • trap 'handle_signal SIGTERM' SIGTERM: Catches termination signals, often sent by process managers like systemd or orchestration systems.

The log function centralizes message output, directing errors to stderr and optionally to a log file, and allowing for different verbosity levels.

Argument Parsing with getopts vs. Manual Loop

For simple scripts with single-character options, getopts is efficient. For more complex scripts requiring long options (e.g., --verbose), getopt (with a 's' at the end) or a manual while loop with case statements are necessary. The boilerplate uses a manual loop for flexibility and long option support.

Example: Using the parse_args function

The parse_args function demonstrates how to handle short and long options, options with arguments, and positional arguments. It populates script-global variables (e.g., VERBOSE, DRY_RUN, CONFIG_FILE) and stores remaining positional arguments in the POSITIONAL_ARGS array.

./my_script.sh -v --config my_settings.conf --dry-run file1.txt file2.csv

In this example, VERBOSE would be 1, CONFIG_FILE would be "my_settings.conf", DRY_RUN would be 1, and POSITIONAL_ARGS would contain "file1.txt" and "file2.csv".

Configuration Loading

The load_config function provides a designated place to load configuration settings from an external file. Sourcing a file (source "$CONFIG_FILE") is the simplest method if the configuration file itself is a Bash script containing variable assignments. This approach, however, should be used with caution as sourcing executes the file's content, posing a security risk if the file is untrusted. For more secure or complex configuration needs (e.g., JSON, YAML), dedicated parsers (like jq for JSON or Python scripts) would be integrated here.

Main Logic and Execution Flow

The main function encapsulates the primary logic of the script. It first calls parse_args and load_config to prepare the environment. The actual task-specific code then resides within main. By structuring the script this way, global scope pollution is minimized, and the flow is clear: setup, configuration, execution, cleanup.

Common Pitfalls

  • Unquoted Variables: Failing to quote variables (e.g., echo $VAR instead of echo "$VAR") can lead to unexpected word splitting and globbing, especially with filenames containing spaces or special characters. Always quote variables unless specific word splitting/globbing behavior is desired.
  • Forgetting set -o pipefail: Silently ignoring errors in pipelines is a frequent source of bugs. Ensure this option is always enabled for critical pipelines.
  • Incorrect Trap Usage: Be careful with what commands or functions are called within traps, especially EXIT and ERR. Recursive traps or commands that themselves might fail can lead to infinite loops or unexpected behavior. Keep trap functions simple and robust.
  • Assuming Fixed Paths: Hardcoding paths like /bin/bash or /usr/bin/python reduces portability. Use #!/usr/bin/env for interpreters and leverage PATH for common binaries.
  • Input Validation: Always validate user input or external data. For example, check if files exist, directories are writable, or arguments are in the expected format before operating on them.
  • Over-reliance on exit 0: While exit 0 is for success, ensure that intermediate steps correctly report their status so set -e or the ERR trap can catch failures. Avoid exiting with non-zero status manually unless absolutely necessary; let set -e handle most failures.
  • Sourcing Untrusted Configuration: Be extremely cautious when using source with external configuration files that could be modified by unauthorized users. The sourced file executes as part of your script with its permissions.

Back to the knowledge base · Ask the AI assistant