Linux Bash Scripting & Systems Automation

Shell scripting is the primary automation mechanism in Linux environments, enabling administrators and software engineers to orchestrate deployments, backup workflows, batch processing, and system maintenance. This guide covers production-grade shell scripting standards, control structures, functions, argument parsing, error trapping, and automation patterns.


⚡ Quick Dive

Bash Scripting Core Syntax Cheat Sheet

Feature Syntax / Command Example
Shebang & Strict Mode #!/usr/bin/env bash
set -euo pipefail
Fail immediately on error, unset variable, or pipe failure
Variable Declaration VAR="value" (no spaces around =) TARGET_DIR="/var/backups"
Command Substitution $(command) CURRENT_DATE=$(date +%F)
Arithmetic Evaluation $(( expression )) NEXT_COUNT=$(( COUNT + 1 ))
Conditional String Test [[ "$STR1" == "$STR2" ]] [[ "$ENV" == "production" ]]
Conditional Number Test [[ "$NUM" -gt 10 ]] [[ "$EXIT_CODE" -eq 0 ]]
File Condition Test [[ -f "$FILE" ]] / [[ -d "$DIR" ]] [[ -s "/etc/hosts" ]] (exists & non-empty)
Function Definition function_name() { ... } log_info() { echo "[INFO] $*"; }
Trap Signal Handler trap cleanup_handler EXIT ERR trap 'rm -rf "$TMP_DIR"' EXIT
Script Argument List $# (count), $@ (all), $1..$9 echo "Running on target: $1"

Production Script Skeleton

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

# Global constants & variables
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LOG_FILE="/tmp/automation.log"

# Cleanup handler executed on exit (success or failure)
cleanup() {
    local exit_code=$?
    echo "[INFO] Cleaning temporary files..."
    # rm -f /tmp/lockfile
    exit "$exit_code"
}
trap cleanup EXIT INT TERM

log() {
    echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] $*" | tee -a "$LOG_FILE"
}

main() {
    log "Starting automated workflow from ${SCRIPT_DIR}..."
    # Core logic here
    log "Task completed successfully."
}

main "$@"

📖 Extended Guide

1. Bash Strict Mode & Safety Directives

Writing production-grade Bash requires preventing silent failures:

set -euo pipefail
  • -e (errexit): Exit immediately if any command returns a non-zero exit code (unless wrapped in an if, while, or until condition).
  • -u (nounset): Treat unset or uninitialized variables as an error and exit immediately. Prevents disastrous typos like rm -rf "$UNSET_VAR/*".
  • -o pipefail: Returns the exit code of the last command in a pipeline that failed (failed command1 | command2 won't be masked by a successful command2).
  • IFS=$'\n\t': Internal Field Separator set to newline and tab (prevents accidental word splitting on spaces in file names).

2. Variables, Parameter Expansion, and Types

Default & Fallback Expansions

# Use default if unset or empty
PORT="${APP_PORT:-8080}"

# Assign default if unset
: "${BACKUP_DIR:=/tmp/backup}"

# Error and exit if variable is unset
DB_PASS="${DATABASE_PASSWORD:?Database password must be provided}"

String Manipulation in Native Bash

Avoid calling external binaries (cut, sed, awk) for simple string transformations:

FILENAME="app_v2.1.0_linux_amd64.tar.gz"

# Substring Removal from Front
echo "${FILENAME#*_}"    # Non-greedy: v2.1.0_linux_amd64.tar.gz
echo "${FILENAME##*_}"   # Greedy:     amd64.tar.gz

# Substring Removal from End (Extract extension or basename)
echo "${FILENAME%.*}"    # Non-greedy: app_v2.1.0_linux_amd64.tar
echo "${FILENAME%%.*}"   # Greedy:     app_v2

# Search and Replace
echo "${FILENAME//_/-}"  # Replace all underscores with dashes: app-v2.1.0-linux-amd64.tar.gz

# String Length & Slicing
echo "${#FILENAME}"      # Character count
echo "${FILENAME:0:3}"   # First 3 characters: app

3. Conditional Structures & Comparisons

Always use modern double brackets [[ ... ]] instead of legacy single brackets [ ... ] or test:

Comparison Operators Matrix

Comparison Type Operator Meaning / Example
Integer -eq, -ne Equal / Not equal: [[ $count -eq 0 ]]
Integer -lt, -le, -gt, -ge Numeric comparisons: [[ $age -ge 18 ]]
String ==, != Equality: [[ "$role" == "admin" ]]
String =~ Regex matching: [[ "$email" =~ ^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$ ]]
String -z, -n Empty check (-z) / Non-empty check (-n): [[ -z "$TOKEN" ]]
Filesystem -f, -d, -e Is regular file / Directory / Path exists
Filesystem -s Exists and size > 0 (non-empty file)
Filesystem -r, -w, -x Readable / Writable / Executable by current user
# Branching Example
if [[ -f "/etc/nginx/nginx.conf" && -x "$(command -v nginx)" ]]; then
    nginx -t
elif [[ -d "/etc/caddy" ]]; then
    echo "Using Caddy Web Server"
else
    echo "No supported web server found" >&2
    exit 1
fi

4. Loops & Iteration Patterns

Iterating Files Safely with Globs

# Always quote variables to handle spaces safely
for file in /var/log/nginx/*.log; do
    [[ -f "$file" ]] || continue   # Handle empty directory edge case
    echo "Processing $file (Size: $(stat -c%s "$file") bytes)"
done

Reading Files Line-by-Line (The Standard Idiom)

# Safe against backslashes and trailing newlines
while IFS= read -r line || [[ -n "$line" ]]; do
    # Skip comments and blank lines
    [[ "$line" =~ ^[[:space:]]*# ]] && continue
    [[ -z "$line" ]] && continue
    
    echo "Entry: $line"
done < "/etc/hosts"

C-Style Loops & Ranges

# Numeric brace expansion
for i in {1..5}; do
    echo "Retry attempt: $i"
done

# C-style syntax
for ((i=0; i<10; i+=2)); do
    echo "Index: $i"
done

5. Robust Function Design

# Functions should use local variables and explicit return codes
deploy_service() {
    local service_name="$1"
    local target_env="${2:-staging}"
    
    echo "[INFO] Deploying ${service_name} to ${target_env}..."
    
    if ! systemctl is-active --quiet "$service_name"; then
        echo "[ERROR] Service ${service_name} is not running!" >&2
        return 1
    fi
    
    return 0
}

# Invocation and error check
if ! deploy_service "nginx" "production"; then
    echo "Deployment failed!"
fi

6. Command-Line Argument Parsing with getopts

For production CLI scripts, parse flags and parameters cleanly:

#!/usr/bin/env bash

usage() {
    echo "Usage: $0 [-h] -e <env> -p <port> [target]"
    echo "  -h         Display help"
    echo "  -e <env>   Target environment (dev|prod)"
    echo "  -p <port>  Port number"
    exit 1
}

ENVIRONMENT=""
PORT="8080"

while getopts ":he:p:" opt; do
    case "$opt" in
        h) usage ;;
        e) ENVIRONMENT="$OPTARG" ;;
        p) PORT="$OPTARG" ;;
        :) echo "Error: Option -$OPTARG requires an argument." >&2; usage ;;
        \?) echo "Error: Invalid option -$OPTARG" >&2; usage ;;
    esac
done
shift $((OPTIND - 1))

# Remaining positional arguments
TARGET="${1:-localhost}"

if [[ -z "$ENVIRONMENT" ]]; then
    echo "Error: Environment (-e) is required." >&2
    usage
fi

echo "Deploying to $TARGET on port $PORT in $ENVIRONMENT mode."

7. Signal Trapping & Temporary Directories

Use trap to guarantee resource cleanup and prevent orphaned processes or temporary files:

# Create a secure temporary directory that is guaranteed to be deleted on script exit
TMP_DIR=$(mktemp -d /tmp/backup_job.XXXXXX)
trap 'rm -rf "$TMP_DIR"' EXIT

# Perform work inside isolated temporary directory
tar -czf "${TMP_DIR}/archive.tar.gz" /data/
cp "${TMP_DIR}/archive.tar.gz" /mnt/backups/
# When script exits (normal, Ctrl+C, or error), TMP_DIR is deleted automatically