Master bash scripting to automate repetitive DevOps tasks, from system maintenance to deployment workflows. Learn to write robust, maintainable scripts that save time and reduce errors.
Shell scripting is one of the most powerful tools in a DevOps engineer's arsenal. While individual Linux commands are useful, combining them into scripts unlocks automation capabilities that can save hours of manual work and eliminate human error.
Why Shell Scripting Matters:
In DevOps, repetition is the enemy of productivity. Tasks like deploying applications, backing up databases, monitoring systems, and managing infrastructure often follow predictable patterns. Shell scripts automate these patterns, ensuring consistency and freeing you to focus on higher-value work.
Real-World Impact:
A well-written deployment script can reduce deployment time from 30 minutes of manual steps to 30 seconds of automated execution. A backup script running via cron ensures your data is protected without requiring daily manual intervention. Monitoring scripts can detect and alert on issues before they impact users.
What You'll Learn:
This lesson teaches you to write production-grade shell scripts. You'll learn proper script structure, error handling, logging, and best practices. By the end, you'll be able to automate complex workflows and contribute to your team's automation infrastructure.
A shell script is a text file containing a series of commands that the shell executes. Understanding the fundamentals is crucial for writing reliable scripts.
Script Structure:
Every shell script should start with a shebang (#!) that tells the system which interpreter to use:
1#!/bin/bash
2# This is a comment
3# The shebang MUST be the first line
4
5echo "Hello, DevOps!"The #!/bin/bash line specifies bash as the interpreter. For maximum portability, you can use #!/usr/bin/env bash which finds bash in the PATH.
Making Scripts Executable:
1# Make script executable
2chmod +x script.sh
3
4# Run the script
5./script.sh
6
7# Or explicitly call bash
8bash script.shVariables:
Variables store data that your script can use and manipulate:
1# Variable assignment (no spaces around =)
2NAME="DevOps Engineer"
3PORT=8080
4TODAY=$(date +%Y-%m-%d)
5
6# Using variables ($ prefix)
7echo "Hello, $NAME"
8echo "Server running on port $PORT"
9echo "Today is $TODAY"
10
11# Curly braces for clarity
12echo "Backup file: backup_${TODAY}.tar.gz"
13
14# Read-only variables (constants)
15readonly API_URL="https://api.example.com"
16declare -r MAX_RETRIES=3Command Substitution:
Capture command output in variables:
1# Modern syntax (preferred)
2CURRENT_USER=$(whoami)
3FILE_COUNT=$(ls -1 | wc -l)
4HOSTNAME=$(hostname)
5
6# Legacy syntax (backticks)
7KERNEL_VERSION=\`uname -r\`
8
9# Use in commands
10echo "Logged in as: $CURRENT_USER"
11echo "Found $FILE_COUNT files"Arithmetic Operations:
1# Using $(( ))
2COUNT=5
3TOTAL=$((COUNT + 10))
4MULTIPLIED=$((COUNT * 2))
5echo "Total: $TOTAL"
6
7# Using let
8let RESULT=5+3
9let COUNT++
10let COUNT+=10
11
12# Using expr (older method)
13RESULT=$(expr 5 + 3)String Operations:
1# String length
2TEXT="Hello World"
3echo "${#TEXT}" # Output: 11
4
5# Substring
6echo "${TEXT:0:5}" # Output: Hello
7echo "${TEXT:6}" # Output: World
8
9# Replace
10echo "${TEXT/World/DevOps}" # Output: Hello DevOps
11echo "${TEXT//l/L}" # Output: HeLLo WorLd (all occurrences)
12
13# Upper/lowercase
14echo "${TEXT^^}" # HELLO WORLD
15echo "${TEXT,,}" # hello worldSpecial Variables:
1#!/bin/bash
2
3echo "Script name: $0"
4echo "First argument: $1"
5echo "Second argument: $2"
6echo "All arguments: $@"
7echo "Number of arguments: $#"
8echo "Last command exit code: $?"
9echo "Process ID: $$"
10echo "Last background process PID: $!"
11
12# Example usage:
13# ./script.sh arg1 arg2 arg3Quoting:
Understanding quoting is critical for handling spaces and special characters:
1# Double quotes: Variable expansion happens
2NAME="Alice"
3echo "Hello, $NAME" # Output: Hello, Alice
4
5# Single quotes: No expansion, literal string
6echo 'Hello, $NAME' # Output: Hello, $NAME
7
8# No quotes: Word splitting happens
9FILES="file1.txt file2.txt"
10touch $FILES # Creates 2 files
11touch "$FILES" # Creates 1 file named "file1.txt file2.txt"
12
13# Use double quotes for variables!
14rm "$FILE" # Correct
15rm $FILE # Dangerous if FILE has spacesExit Codes:
Every command returns an exit code (0 = success, non-zero = error):
1# Check exit code
2ls /tmp
3if [ $? -eq 0 ]; then
4 echo "Command succeeded"
5fi
6
7# Using command directly in if
8if ls /nonexistent 2>/dev/null; then
9 echo "Directory exists"
10else
11 echo "Directory does not exist"
12fi
13
14# Exit from script with specific code
15exit 0 # Success
16exit 1 # Generic error
17exit 2 # Misuse of commandControl structures allow your scripts to make decisions and repeat operations, making them dynamic and powerful.
If Statements:
1# Basic if
2if [ condition ]; then
3 # commands
4fi
5
6# If-else
7if [ condition ]; then
8 # commands
9else
10 # other commands
11fi
12
13# If-elif-else
14if [ condition1 ]; then
15 # commands
16elif [ condition2 ]; then
17 # other commands
18else
19 # default commands
20fiTest Conditions:
1# Numeric comparisons
2if [ $COUNT -eq 10 ]; then # Equal
3if [ $COUNT -ne 10 ]; then # Not equal
4if [ $COUNT -gt 5 ]; then # Greater than
5if [ $COUNT -ge 5 ]; then # Greater or equal
6if [ $COUNT -lt 20 ]; then # Less than
7if [ $COUNT -le 20 ]; then # Less or equal
8
9# String comparisons
10if [ "$NAME" = "Alice" ]; then # Equal
11if [ "$NAME" != "Bob" ]; then # Not equal
12if [ -z "$NAME" ]; then # Empty string
13if [ -n "$NAME" ]; then # Not empty string
14
15# File tests
16if [ -f "$FILE" ]; then # File exists and is regular file
17if [ -d "$DIR" ]; then # Directory exists
18if [ -e "$PATH" ]; then # Path exists (file or directory)
19if [ -r "$FILE" ]; then # File is readable
20if [ -w "$FILE" ]; then # File is writable
21if [ -x "$FILE" ]; then # File is executable
22if [ -s "$FILE" ]; then # File exists and is not empty
23
24# Logical operators
25if [ $A -gt 5 ] && [ $B -lt 10 ]; then # AND
26if [ $A -gt 5 ] || [ $B -lt 10 ]; then # OR
27if [ ! -f "$FILE" ]; then # NOT
28
29# Modern [[ ]] syntax (preferred)
30if [[ $NAME == "Alice" ]]; then # Pattern matching
31if [[ $NAME =~ ^A ]]; then # Regex matching
32if [[ $COUNT -gt 5 && $COUNT -lt 20 ]]; then # Logical ANDCase Statements:
Better than multiple if-elif for matching patterns:
1case "$ENVIRONMENT" in
2 development|dev)
3 echo "Development environment"
4 DEBUG=true
5 ;;
6 staging|stage)
7 echo "Staging environment"
8 DEBUG=false
9 ;;
10 production|prod)
11 echo "Production environment"
12 DEBUG=false
13 STRICT_MODE=true
14 ;;
15 *)
16 echo "Unknown environment: $ENVIRONMENT"
17 exit 1
18 ;;
19esacFor Loops:
1# Loop over list
2for ITEM in item1 item2 item3; do
3 echo "Processing $ITEM"
4done
5
6# Loop over files
7for FILE in *.txt; do
8 echo "Found: $FILE"
9done
10
11# Loop over command output
12for USER in $(cat users.txt); do
13 echo "User: $USER"
14done
15
16# C-style for loop
17for ((i=1; i<=10; i++)); do
18 echo "Number: $i"
19done
20
21# Loop over array
22SERVERS=("web1" "web2" "db1")
23for SERVER in "${SERVERS[@]}"; do
24 echo "Deploying to $SERVER"
25doneWhile Loops:
1# Basic while loop
2COUNT=1
3while [ $COUNT -le 5 ]; do
4 echo "Count: $COUNT"
5 ((COUNT++))
6done
7
8# Read file line by line
9while IFS= read -r LINE; do
10 echo "Line: $LINE"
11done < input.txt
12
13# Infinite loop with break
14while true; do
15 echo "Running task..."
16 sleep 60
17 if [ -f /tmp/stop ]; then
18 break
19 fi
20done
21
22# Until loop (opposite of while)
23ATTEMPT=1
24until ping -c 1 server.com &>/dev/null; do
25 echo "Attempt $ATTEMPT failed, retrying..."
26 ((ATTEMPT++))
27 sleep 5
28doneLoop Control:
1# Break: exit loop
2for i in {1..10}; do
3 if [ $i -eq 5 ]; then
4 break
5 fi
6 echo $i
7done
8
9# Continue: skip to next iteration
10for i in {1..10}; do
11 if [ $i -eq 5 ]; then
12 continue
13 fi
14 echo $i
15doneFunctions are essential for writing clean, maintainable shell scripts. They allow you to organize code into logical units and avoid repetition.
Defining Functions:
1# Method 1: function keyword
2function backup_database() {
3 echo "Backing up database..."
4 # commands
5}
6
7# Method 2: () syntax (preferred)
8backup_database() {
9 echo "Backing up database..."
10 # commands
11}
12
13# Call function
14backup_databaseFunction Arguments:
Functions access arguments like scripts do:
1deploy_app() {
2 local APP_NAME="$1"
3 local VERSION="$2"
4 local ENVIRONMENT="$3"
5
6 echo "Deploying $APP_NAME v$VERSION to $ENVIRONMENT"
7
8 # Validate arguments
9 if [ -z "$APP_NAME" ]; then
10 echo "Error: APP_NAME is required"
11 return 1
12 fi
13
14 # Deployment logic here
15 return 0
16}
17
18# Call with arguments
19deploy_app "myapp" "1.2.3" "production"Return Values:
Functions return exit codes (0-255), not values:
1is_service_running() {
2 local SERVICE="$1"
3 systemctl is-active --quiet "$SERVICE"
4 return $? # 0 if running, non-zero if not
5}
6
7# Use in conditional
8if is_service_running "nginx"; then
9 echo "Nginx is running"
10fi
11
12# Capture output (not exit code)
13get_timestamp() {
14 date '+%Y-%m-%d %H:%M:%S'
15}
16
17TIMESTAMP=$(get_timestamp)
18echo "Current time: $TIMESTAMP"Local Variables:
Always use local for function variables to avoid conflicts:
1# BAD: Global variable
2process_file() {
3 FILE="$1" # This modifies global FILE!
4 echo "Processing $FILE"
5}
6
7# GOOD: Local variable
8process_file() {
9 local FILE="$1"
10 echo "Processing $FILE"
11}Function Libraries:
Create reusable function libraries:
1# lib/common.sh
2log_info() {
3 echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*"
4}
5
6log_error() {
7 echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2
8}
9
10check_root() {
11 if [ "$EUID" -ne 0 ]; then
12 log_error "This script must be run as root"
13 exit 1
14 fi
15}
16
17# Use in main script
18source lib/common.sh
19
20log_info "Starting deployment"
21check_root
22log_info "Deployment complete"Error Handling in Functions:
1backup_file() {
2 local SOURCE="$1"
3 local DEST="$2"
4
5 # Validate inputs
6 if [ ! -f "$SOURCE" ]; then
7 echo "Error: Source file does not exist: $SOURCE" >&2
8 return 1
9 fi
10
11 if [ -z "$DEST" ]; then
12 echo "Error: Destination not specified" >&2
13 return 1
14 fi
15
16 # Perform backup
17 if cp "$SOURCE" "$DEST"; then
18 echo "Backup successful: $SOURCE -> $DEST"
19 return 0
20 else
21 echo "Error: Backup failed" >&2
22 return 1
23 fi
24}
25
26# Use with error handling
27if backup_file "/etc/nginx/nginx.conf" "/backup/nginx.conf"; then
28 echo "Backup completed"
29else
30 echo "Backup failed"
31 exit 1
32fiOrganizing Large Scripts:
1#!/bin/bash
2# deploy.sh - Application deployment script
3
4# ==============================================
5# CONFIGURATION
6# ==============================================
7
8readonly APP_NAME="myapp"
9readonly DEPLOY_USER="deploy"
10readonly APP_DIR="/var/www/apps"
11
12# ==============================================
13# UTILITY FUNCTIONS
14# ==============================================
15
16log() {
17 echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
18}
19
20error_exit() {
21 echo "[ERROR] $*" >&2
22 exit 1
23}
24
25# ==============================================
26# BUSINESS LOGIC FUNCTIONS
27# ==============================================
28
29check_prerequisites() {
30 log "Checking prerequisites..."
31 command -v git >/dev/null || error_exit "git not found"
32 [ -d "$APP_DIR" ] || error_exit "App directory not found"
33}
34
35pull_code() {
36 local BRANCH="$1"
37 log "Pulling code from $BRANCH..."
38 git pull origin "$BRANCH" || return 1
39}
40
41run_tests() {
42 log "Running tests..."
43 npm test || return 1
44}
45
46deploy() {
47 local VERSION="$1"
48 log "Deploying version $VERSION..."
49 # Deployment logic
50}
51
52# ==============================================
53# MAIN EXECUTION
54# ==============================================
55
56main() {
57 log "Starting deployment"
58
59 check_prerequisites
60 pull_code "main" || error_exit "Failed to pull code"
61 run_tests || error_exit "Tests failed"
62 deploy "1.0.0" || error_exit "Deployment failed"
63
64 log "Deployment completed successfully"
65}
66
67# Run main function
68main "$@"Production scripts must handle errors gracefully and provide clear debugging information when things go wrong.
Exit on Error:
1#!/bin/bash
2
3# Exit immediately if a command fails
4set -e
5
6# Also exit on undefined variables
7set -u
8
9# Fail on pipe errors (not just last command)
10set -o pipefail
11
12# Combine all three (recommended)
13set -euo pipefail
14
15# Now any error will stop the script
16cp important.txt /backup/ # If this fails, script stops
17rm important.txt # This won't run if cp failedError Trapping:
Catch errors and run cleanup code:
1# Trap errors and run cleanup
2cleanup() {
3 local EXIT_CODE=$?
4 echo "Script failed with exit code: $EXIT_CODE"
5 # Cleanup actions
6 rm -f /tmp/lockfile
7 exit $EXIT_CODE
8}
9
10trap cleanup ERR EXIT
11
12# Your script code hereDefensive Programming:
1# Check if file exists before reading
2if [ -f "$CONFIG_FILE" ]; then
3 source "$CONFIG_FILE"
4else
5 echo "Error: Config file not found: $CONFIG_FILE" >&2
6 exit 1
7fi
8
9# Check if variable is set
10if [ -z "${DATABASE_HOST:-}" ]; then
11 echo "Error: DATABASE_HOST is not set" >&2
12 exit 1
13fi
14
15# Check command succeeded
16if ! systemctl start nginx; then
17 echo "Error: Failed to start nginx" >&2
18 exit 1
19fi
20
21# Alternative: use || for error handling
22systemctl start nginx || {
23 echo "Error: Failed to start nginx" >&2
24 exit 1
25}Logging:
Implement comprehensive logging:
1# Log to file and console
2LOG_FILE="/var/log/deploy.log"
3
4log() {
5 local LEVEL="$1"
6 shift
7 local MESSAGE="$*"
8 local TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
9
10 # Log to file
11 echo "[$TIMESTAMP] [$LEVEL] $MESSAGE" >> "$LOG_FILE"
12
13 # Also print to console
14 echo "[$LEVEL] $MESSAGE"
15
16 # Send errors to stderr
17 if [ "$LEVEL" = "ERROR" ]; then
18 echo "[$LEVEL] $MESSAGE" >&2
19 fi
20}
21
22log "INFO" "Starting deployment"
23log "ERROR" "Database connection failed"Debug Mode:
1#!/bin/bash
2
3# Enable debug mode with -x flag
4# ./script.sh -x
5
6DEBUG=false
7
8# Parse command line for debug flag
9while getopts "x" opt; do
10 case $opt in
11 x) DEBUG=true ;;
12 esac
13done
14
15# Debug function
16debug() {
17 if [ "$DEBUG" = true ]; then
18 echo "[DEBUG] $*" >&2
19 fi
20}
21
22# Usage
23debug "Variable value: $SOME_VAR"
24debug "Processing file: $FILE"
25
26# Or enable xtrace for entire script
27if [ "$DEBUG" = true ]; then
28 set -x # Print each command before executing
29fiDry Run Mode:
1DRY_RUN=false
2
3# Parse arguments
4while getopts "n" opt; do
5 case $opt in
6 n) DRY_RUN=true ;;
7 esac
8done
9
10# Execute or simulate
11execute() {
12 if [ "$DRY_RUN" = true ]; then
13 echo "[DRY RUN] Would execute: $*"
14 else
15 "$@"
16 fi
17}
18
19# Usage
20execute rm -rf /tmp/old-data
21execute systemctl restart nginxAssert Functions:
1# Assert command succeeded
2assert_success() {
3 if [ $? -ne 0 ]; then
4 echo "Assertion failed: $*" >&2
5 exit 1
6 fi
7}
8
9# Assert file exists
10assert_file_exists() {
11 if [ ! -f "$1" ]; then
12 echo "Assertion failed: File does not exist: $1" >&2
13 exit 1
14 fi
15}
16
17# Usage
18cp source.txt dest.txt
19assert_success "Failed to copy file"
20
21assert_file_exists "/etc/nginx/nginx.conf"Debugging Techniques:
1# 1. Print variables
2echo "DEBUG: Variable value is: $VAR"
3
4# 2. Use set -x for trace mode
5set -x
6# Commands will be printed before execution
7cp file1 file2
8set +x # Disable
9
10# 3. Check if variable is set
11echo "VAR is: ${VAR:-NOT SET}"
12
13# 4. Print to stderr
14echo "Debug info" >&2
15
16# 5. Use trap to print line numbers
17trap 'echo "Error on line $LINENO"' ERR
18
19# 6. Validate assumptions
20[ -d "$DIR" ] || { echo "Not a directory: $DIR"; exit 1; }
21
22# 7. Use ShellCheck (static analysis)
23# Install: sudo apt install shellcheck
24# Run: shellcheck script.shLet's put everything together to create production-ready automation scripts for real DevOps scenarios.
Automated Backup Script:
1#!/bin/bash
2# Backup script with rotation and notification
3
4set -euo pipefail
5
6readonly BACKUP_DIR="/backups"
7readonly RETENTION_DAYS=7
8readonly NOTIFICATION_EMAIL="[email protected]"
9
10backup_database() {
11 local DB_NAME="$1"
12 local BACKUP_FILE="$BACKUP_DIR/${DB_NAME}_$(date +%Y%m%d_%H%M%S).sql.gz"
13
14 mysqldump "$DB_NAME" | gzip > "$BACKUP_FILE"
15 echo "$BACKUP_FILE"
16}
17
18rotate_backups() {
19 find "$BACKUP_DIR" -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
20}
21
22send_notification() {
23 local STATUS="$1"
24 local MESSAGE="$2"
25
26 echo "$MESSAGE" | mail -s "Backup $STATUS" "$NOTIFICATION_EMAIL"
27}
28
29main() {
30 if BACKUP_FILE=$(backup_database "production_db"); then
31 rotate_backups
32 send_notification "Success" "Backup completed: $BACKUP_FILE"
33 else
34 send_notification "Failed" "Backup failed!"
35 exit 1
36 fi
37}
38
39main "$@"Deployment Script:
1#!/bin/bash
2# Zero-downtime deployment script
3
4deploy() {
5 local VERSION="$1"
6 local APP_DIR="/var/www/app"
7 local NEW_DIR="$APP_DIR-$VERSION"
8
9 # Pull new version
10 git clone -b "$VERSION" repo.git "$NEW_DIR"
11
12 # Install dependencies
13 cd "$NEW_DIR"
14 npm install --production
15
16 # Run tests
17 npm test || {
18 echo "Tests failed!"
19 rm -rf "$NEW_DIR"
20 return 1
21 }
22
23 # Symlink swap (atomic operation)
24 ln -sfn "$NEW_DIR" "$APP_DIR/current"
25
26 # Restart service
27 systemctl restart app
28
29 # Health check
30 sleep 5
31 if ! curl -f http://localhost:3000/health; then
32 # Rollback
33 ln -sfn "$APP_DIR-old" "$APP_DIR/current"
34 systemctl restart app
35 return 1
36 fi
37
38 echo "Deployment successful"
39}
40
41deploy "$1"Monitoring Script:
1#!/bin/bash
2# System monitoring with alerts
3
4check_disk_space() {
5 local THRESHOLD=80
6 local USAGE=$(df -h / | awk 'NR==2 {print $5}' | sed 's/%//')
7
8 if [ "$USAGE" -gt "$THRESHOLD" ]; then
9 echo "ALERT: Disk usage is ${USAGE}%"
10 return 1
11 fi
12 return 0
13}
14
15check_service() {
16 local SERVICE="$1"
17
18 if ! systemctl is-active --quiet "$SERVICE"; then
19 echo "ALERT: $SERVICE is down"
20 systemctl restart "$SERVICE"
21 return 1
22 fi
23 return 0
24}
25
26check_process_count() {
27 local PROCESS="$1"
28 local COUNT=$(pgrep -c "$PROCESS" || echo 0)
29
30 if [ "$COUNT" -eq 0 ]; then
31 echo "ALERT: No $PROCESS processes running"
32 return 1
33 fi
34 return 0
35}
36
37main() {
38 local ERRORS=0
39
40 check_disk_space || ((ERRORS++))
41 check_service "nginx" || ((ERRORS++))
42 check_process_count "node" || ((ERRORS++))
43
44 if [ $ERRORS -gt 0 ]; then
45 echo "Health check failed with $ERRORS errors"
46 # Send alert
47 exit 1
48 fi
49
50 echo "All checks passed"
51}
52
53mainLog Rotation and Cleanup:
1#!/bin/bash
2# Automated log management
3
4readonly LOG_DIR="/var/log/app"
5readonly ARCHIVE_DIR="/var/log/app/archive"
6readonly MAX_SIZE_MB=100
7readonly RETENTION_DAYS=30
8
9rotate_large_logs() {
10 find "$LOG_DIR" -name "*.log" -size +${MAX_SIZE_MB}M | while read -r LOG; do
11 local TIMESTAMP=$(date +%Y%m%d_%H%M%S)
12 local ARCHIVE="$ARCHIVE_DIR/$(basename "$LOG").$TIMESTAMP.gz"
13
14 # Compress and move
15 gzip -c "$LOG" > "$ARCHIVE"
16
17 # Truncate original
18 > "$LOG"
19
20 echo "Rotated: $LOG -> $ARCHIVE"
21 done
22}
23
24cleanup_old_archives() {
25 find "$ARCHIVE_DIR" -name "*.gz" -mtime +$RETENTION_DAYS -delete
26}
27
28main() {
29 mkdir -p "$ARCHIVE_DIR"
30 rotate_large_logs
31 cleanup_old_archives
32}
33
34mainComplete backup automation with compression, rotation, health checks, and notifications
1#!/bin/bash
2# Production Backup System
3# Features: Database backup, file backup, compression, rotation, monitoring, notifications
4
5set -euo pipefail
6
7# ==============================================
8# CONFIGURATION
9# ==============================================
10
11readonly SCRIPT_NAME="backup-system.sh"
12readonly VERSION="2.0.0"
13
14# Backup directories
15readonly BACKUP_ROOT="/backups"
16readonly DB_BACKUP_DIR="$BACKUP_ROOT/databases"
17readonly FILE_BACKUP_DIR="$BACKUP_ROOT/files"
18readonly LOG_DIR="/var/log/backups"
19readonly LOG_FILE="$LOG_DIR/backup-$(date +%Y%m%d).log"
20
21# Retention policies
22readonly DB_RETENTION_DAYS=7
23readonly FILE_RETENTION_DAYS=30
24
25# Databases to backup
26readonly DATABASES=("production_db" "analytics_db" "users_db")
27
28# Directories to backup
29readonly BACKUP_PATHS=(
30 "/etc/nginx"
31 "/etc/mysql"
32 "/var/www/html"
33 "/home/deploy/.ssh"
34)
35
36# Notifications
37readonly ALERT_EMAIL="[email protected]"
38readonly SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK"
39
40# Performance
41readonly COMPRESSION_LEVEL=6
42readonly PARALLEL_JOBS=2
43
44# ==============================================
45# LOGGING SYSTEM
46# ==============================================
47
48setup_logging() {
49 mkdir -p "$LOG_DIR"
50 exec 1> >(tee -a "$LOG_FILE")
51 exec 2>&1
52}
53
54log() {
55 local LEVEL="$1"
56 shift
57 local TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
58 echo "[$TIMESTAMP] [$LEVEL] $*"
59}
60
61log_header() {
62 echo "=========================================="
63 log "INFO" "$*"
64 echo "=========================================="
65}
66
67# ==============================================
68# NOTIFICATION SYSTEM
69# ==============================================
70
71send_email_alert() {
72 local SUBJECT="$1"
73 local BODY="$2"
74
75 if command -v mail >/dev/null 2>&1; then
76 echo "$BODY" | mail -s "$SUBJECT" "$ALERT_EMAIL"
77 log "INFO" "Email sent to $ALERT_EMAIL"
78 else
79 log "WARN" "Mail command not available, skipping email"
80 fi
81}
82
83send_slack_notification() {
84 local MESSAGE="$1"
85 local COLOR="${2:-good}" # good, warning, danger
86
87 if [ -n "$SLACK_WEBHOOK" ]; then
88 curl -X POST "$SLACK_WEBHOOK" \
89 -H 'Content-Type: application/json' \
90 -d "{
91 "attachments": [{
92 "color": "$COLOR",
93 "title": "Backup System",
94 "text": "$MESSAGE",
95 "footer": "$(hostname)",
96 "ts": $(date +%s)
97 }]
98 }" 2>/dev/null || log "WARN" "Failed to send Slack notification"
99 fi
100}
101
102notify_success() {
103 local MESSAGE="$1"
104 log "INFO" "$MESSAGE"
105 send_slack_notification "✅ $MESSAGE" "good"
106}
107
108notify_failure() {
109 local MESSAGE="$1"
110 log "ERROR" "$MESSAGE"
111 send_email_alert "Backup FAILED" "$MESSAGE"
112 send_slack_notification "❌ $MESSAGE" "danger"
113}
114
115# ==============================================
116# BACKUP FUNCTIONS
117# ==============================================
118
119backup_database() {
120 local DB_NAME="$1"
121 local TIMESTAMP=$(date +%Y%m%d_%H%M%S)
122 local BACKUP_FILE="$DB_BACKUP_DIR/${DB_NAME}_${TIMESTAMP}.sql.gz"
123
124 log "INFO" "Backing up database: $DB_NAME"
125
126 mkdir -p "$DB_BACKUP_DIR"
127
128 # Dump database with compression
129 if mysqldump \
130 --single-transaction \
131 --quick \
132 --lock-tables=false \
133 "$DB_NAME" | gzip -$COMPRESSION_LEVEL > "$BACKUP_FILE"; then
134
135 local SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
136 log "INFO" "Database backup completed: $BACKUP_FILE ($SIZE)"
137 echo "$BACKUP_FILE"
138 return 0
139 else
140 log "ERROR" "Database backup failed: $DB_NAME"
141 return 1
142 fi
143}
144
145backup_files() {
146 local SOURCE="$1"
147 local TIMESTAMP=$(date +%Y%m%d_%H%M%S)
148 local BASENAME=$(basename "$SOURCE" | tr '/' '_')
149 local BACKUP_FILE="$FILE_BACKUP_DIR/${BASENAME}_${TIMESTAMP}.tar.gz"
150
151 log "INFO" "Backing up files: $SOURCE"
152
153 mkdir -p "$FILE_BACKUP_DIR"
154
155 if [ ! -d "$SOURCE" ] && [ ! -f "$SOURCE" ]; then
156 log "WARN" "Source not found: $SOURCE"
157 return 1
158 fi
159
160 # Create compressed archive
161 if tar -czf "$BACKUP_FILE" -C "$(dirname "$SOURCE")" "$(basename "$SOURCE")" 2>/dev/null; then
162 local SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
163 log "INFO" "File backup completed: $BACKUP_FILE ($SIZE)"
164 echo "$BACKUP_FILE"
165 return 0
166 else
167 log "ERROR" "File backup failed: $SOURCE"
168 return 1
169 fi
170}
171
172# ==============================================
173# ROTATION AND CLEANUP
174# ==============================================
175
176rotate_backups() {
177 local BACKUP_DIR="$1"
178 local RETENTION_DAYS="$2"
179
180 log "INFO" "Rotating backups in $BACKUP_DIR (retention: $RETENTION_DAYS days)"
181
182 local COUNT=0
183 while IFS= read -r -d '' BACKUP; do
184 rm -f "$BACKUP"
185 ((COUNT++))
186 done < <(find "$BACKUP_DIR" -type f -mtime +$RETENTION_DAYS -print0)
187
188 if [ $COUNT -gt 0 ]; then
189 log "INFO" "Removed $COUNT old backup(s)"
190 else
191 log "INFO" "No old backups to remove"
192 fi
193}
194
195# ==============================================
196# VERIFICATION
197# ==============================================
198
199verify_backup() {
200 local BACKUP_FILE="$1"
201
202 if [ ! -f "$BACKUP_FILE" ]; then
203 log "ERROR" "Backup file not found: $BACKUP_FILE"
204 return 1
205 fi
206
207 # Check file is not empty
208 if [ ! -s "$BACKUP_FILE" ]; then
209 log "ERROR" "Backup file is empty: $BACKUP_FILE"
210 return 1
211 fi
212
213 # Verify archive integrity
214 if [[ "$BACKUP_FILE" == *.tar.gz ]]; then
215 if tar -tzf "$BACKUP_FILE" >/dev/null 2>&1; then
216 log "INFO" "Backup verified: $BACKUP_FILE"
217 return 0
218 else
219 log "ERROR" "Backup verification failed: $BACKUP_FILE"
220 return 1
221 fi
222 fi
223
224 # Verify gzip integrity
225 if [[ "$BACKUP_FILE" == *.gz ]]; then
226 if gzip -t "$BACKUP_FILE" 2>/dev/null; then
227 log "INFO" "Backup verified: $BACKUP_FILE"
228 return 0
229 else
230 log "ERROR" "Backup verification failed: $BACKUP_FILE"
231 return 1
232 fi
233 fi
234
235 return 0
236}
237
238# ==============================================
239# MONITORING AND REPORTING
240# ==============================================
241
242generate_report() {
243 local SUCCESS_COUNT="$1"
244 local FAILURE_COUNT="$2"
245 local TOTAL_SIZE="$3"
246
247 cat <<EOF
248
249========================================
250BACKUP REPORT
251========================================
252Date: $(date)
253Hostname: $(hostname)
254
255Status:
256 ✓ Successful: $SUCCESS_COUNT
257 ✗ Failed: $FAILURE_COUNT
258 Total Size: $TOTAL_SIZE
259
260Database Backups:
261$(ls -lh "$DB_BACKUP_DIR" 2>/dev/null | tail -n +2 | head -5 || echo " No backups found")
262
263File Backups:
264$(ls -lh "$FILE_BACKUP_DIR" 2>/dev/null | tail -n +2 | head -5 || echo " No backups found")
265
266Disk Usage:
267$(df -h "$BACKUP_ROOT" | tail -1)
268
269========================================
270EOF
271}
272
273# ==============================================
274# MAIN EXECUTION
275# ==============================================
276
277main() {
278 setup_logging
279
280 log_header "$SCRIPT_NAME v$VERSION - Starting Backup"
281
282 local SUCCESS_COUNT=0
283 local FAILURE_COUNT=0
284 local TOTAL_SIZE=0
285
286 # Database backups
287 log_header "Database Backups"
288 for DB in "${DATABASES[@]}"; do
289 if BACKUP_FILE=$(backup_database "$DB"); then
290 if verify_backup "$BACKUP_FILE"; then
291 ((SUCCESS_COUNT++))
292 SIZE=$(stat -f%z "$BACKUP_FILE" 2>/dev/null || stat -c%s "$BACKUP_FILE")
293 TOTAL_SIZE=$((TOTAL_SIZE + SIZE))
294 else
295 ((FAILURE_COUNT++))
296 fi
297 else
298 ((FAILURE_COUNT++))
299 fi
300 done
301
302 # File backups
303 log_header "File Backups"
304 for PATH in "${BACKUP_PATHS[@]}"; do
305 if BACKUP_FILE=$(backup_files "$PATH"); then
306 if verify_backup "$BACKUP_FILE"; then
307 ((SUCCESS_COUNT++))
308 SIZE=$(stat -f%z "$BACKUP_FILE" 2>/dev/null || stat -c%s "$BACKUP_FILE")
309 TOTAL_SIZE=$((TOTAL_SIZE + SIZE))
310 else
311 ((FAILURE_COUNT++))
312 fi
313 else
314 ((FAILURE_COUNT++))
315 fi
316 done
317
318 # Rotation
319 log_header "Backup Rotation"
320 rotate_backups "$DB_BACKUP_DIR" "$DB_RETENTION_DAYS"
321 rotate_backups "$FILE_BACKUP_DIR" "$FILE_RETENTION_DAYS"
322
323 # Report
324 local TOTAL_SIZE_HUMAN=$(numfmt --to=iec-i --suffix=B $TOTAL_SIZE 2>/dev/null || echo "${TOTAL_SIZE} bytes")
325 local REPORT=$(generate_report "$SUCCESS_COUNT" "$FAILURE_COUNT" "$TOTAL_SIZE_HUMAN")
326
327 echo "$REPORT"
328
329 # Notifications
330 if [ $FAILURE_COUNT -eq 0 ]; then
331 notify_success "Backup completed: $SUCCESS_COUNT successful, $TOTAL_SIZE_HUMAN"
332 exit 0
333 else
334 notify_failure "Backup completed with errors: $FAILURE_COUNT failed out of $((SUCCESS_COUNT + FAILURE_COUNT))"
335 exit 1
336 fi
337}
338
339# Execute
340main "$@"This production-ready backup system demonstrates advanced shell scripting concepts in a real-world scenario.
Key Features Demonstrated:
set -euo pipefail ensures the script fails fast on errorsProduction Best Practices:
Cron Job Setup:
1# Run daily at 2 AM
20 2 * * * /usr/local/bin/backup-system.sh
3
4# Run database backups every 6 hours
50 */6 * * * /usr/local/bin/backup-system.sh --databases-only
6
7# Run with custom retention
80 2 * * 0 /usr/local/bin/backup-system.sh --retention 14This script handles real production requirements: multiple backup types, verification, rotation, monitoring, and alerting - all critical for reliable backup systems.
Your progress is saved in this browser, on this device only.