Introduction to Bash Scripting for DevOps

Master Bash scripting to automate repetitive tasks, deploy applications, and manage infrastructure. This beginner's guide covers everything from writing your first script to creating production-ready automation for DevOps workflows.

Why Bash Scripting Matters for DevOps

Bash is the universal language of Linux systems. As a DevOps engineer, you'll use Bash for:

Automation

Automate repetitive tasks like deployments, backups, and monitoring

🛠️
System Administration

Manage servers, install software, configure services

🔧
CI/CD Pipelines

Create build scripts, run tests, deploy applications

Your First Bash Script

Step 1: Create the Script

# Create a new file
nano hello.sh

# Add this content:
#!/bin/bash
# This is a comment
echo "Hello, DevOps World!"
echo "Today is $(date)"
echo "You are: $(whoami)"
echo "Current directory: $(pwd)"

Step 2: Make it Executable

# Add execute permission
chmod +x hello.sh

# Check permissions
ls -l hello.sh
# Output: -rwxr-xr-x 1 user group 123 Dec  7 10:30 hello.sh

Step 3: Run the Script

# Method 1: Direct execution
./hello.sh

# Method 2: Using bash interpreter
bash hello.sh

# Method 3: Source the script (runs in current shell)
source hello.sh
# or
. hello.sh

# Output:
# Hello, DevOps World!
# Today is Sat Dec  7 10:30:45 UTC 2025
# You are: user
# Current directory: /home/user
Pro Tip: Always start your scripts with #!/bin/bash (shebang). This tells the system which interpreter to use. Use #!/usr/bin/env bash for better portability across systems.

Essential Bash Syntax

1. Variables

#!/bin/bash

# Define variables
NAME="DevOps Engineer"
AGE=30
SERVER_IP="192.168.1.100"

# Use variables
echo "Hello, $NAME"
echo "You are $AGE years old"
echo "Server IP: ${SERVER_IP}"  # Braces for clarity

# Read-only variables
readonly API_KEY="abc123"
# API_KEY="xyz"  # This would cause error

# Export to environment (available to child processes)
export DATABASE_URL="postgres://localhost/mydb"

# Default values
BACKUP_DIR=${BACKUP_DIR:-"/var/backups"}  # Use default if not set
echo "Backup directory: $BACKUP_DIR"

2. User Input

#!/bin/bash

# Simple prompt
echo "What's your name?"
read USERNAME
echo "Hello, $USERNAME!"

# Prompt with message
read -p "Enter your age: " AGE
echo "You are $AGE years old"

# Silent input (for passwords)
read -sp "Enter password: " PASSWORD
echo  # New line after silent input
echo "Password accepted"

# Read multiple values
read -p "Enter IP and port: " IP PORT
echo "Connecting to $IP:$PORT"

# Set timeout
read -t 10 -p "Quick! Enter something: " QUICK_INPUT
if [ -z "$QUICK_INPUT" ]; then
    echo "Too slow!"
fi

3. Command Line Arguments

#!/bin/bash
# save as: deploy.sh

# Special variables for arguments
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments: $@"
echo "Number of arguments: $#"

# Example usage: ./deploy.sh staging app1
ENVIRONMENT=$1
APP_NAME=$2

echo "Deploying $APP_NAME to $ENVIRONMENT environment"

# Shift arguments (remove first)
shift
echo "Remaining arguments: $@"

# Loop through all arguments
for ARG in "$@"; do
    echo "Processing: $ARG"
done

Conditional Statements

# Basic if statement
if [ condition ]; then
# commands if true
elif [ another_condition ]; then
# commands if this true
else
# commands if all false
fi

Comparison Operators

Type Operator Example Description String =, != [ "$a" = "$b" ] Equal, Not equal String -z, -n [ -z "$var" ] Empty, Not empty Numeric -eq, -ne [ $a -eq $b ] Equal, Not equal Numeric -lt, -gt [ $a -lt $b ] Less than, Greater than File -f, -d [ -f file.txt ] File exists, Directory exists File -r, -w [ -r file.txt ] Readable, Writable

Practical Examples

#!/bin/bash

# 1. Check if file exists
FILE="/etc/passwd"
if [ -f "$FILE" ]; then
    echo "$FILE exists"
else
    echo "$FILE not found"
fi

# 2. Check user input
read -p "Enter y/n: " ANSWER
if [ "$ANSWER" = "y" ] || [ "$ANSWER" = "Y" ]; then
    echo "You said yes"
elif [ "$ANSWER" = "n" ] || [ "$ANSWER" = "N" ]; then
    echo "You said no"
else
    echo "Invalid input"
fi

# 3. Check disk space
DISK_USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
if [ "$DISK_USAGE" -gt 90 ]; then
    echo "WARNING: Disk usage is ${DISK_USAGE}%"
fi

# 4. Case statement (alternative to multiple if-elif)
read -p "Enter command (start|stop|restart|status): " CMD
case $CMD in
    start)
        echo "Starting service..."
        ;;
    stop)
        echo "Stopping service..."
        ;;
    restart)
        echo "Restarting service..."
        ;;
    status)
        echo "Checking status..."
        ;;
    *)
        echo "Unknown command: $CMD"
        ;;
esac

Loops for Automation

1. For Loop

#!/bin/bash

# Loop through list
for COLOR in red green blue yellow; do
    echo "Color: $COLOR"
done

# Loop through files
for FILE in *.txt; do
    echo "Processing: $FILE"
    wc -l "$FILE"
done

# C-style for loop
for ((i=1; i<=5; i++)); do
    echo "Iteration: $i"
done

# Loop through command output
for USER in $(cut -d: -f1 /etc/passwd | head -10); do
    echo "User: $USER"
done

2. While Loop

#!/bin/bash

# Basic while loop
COUNT=1
while [ $COUNT -le 5 ]; do
    echo "Count: $COUNT"
    COUNT=$((COUNT + 1))
done

# Read file line by line
while read LINE; do
    echo "Line: $LINE"
done < /etc/hosts

# Infinite loop with break
while true; do
    read -p "Enter command (quit to exit): " CMD
    if [ "$CMD" = "quit" ]; then
        break
    fi
    echo "You entered: $CMD"
done

3. Until Loop

#!/bin/bash

# Until loop (runs while condition is false)
SERVER_UP=false
ATTEMPTS=0

until [ $SERVER_UP = true ] || [ $ATTEMPTS -ge 5 ]; do
    echo "Checking server... Attempt: $((ATTEMPTS + 1))"
    
    # Simulate server check
    if curl -s http://localhost:8080 > /dev/null; then
        SERVER_UP=true
        echo "Server is UP!"
    else
        echo "Server is DOWN"
        ATTEMPTS=$((ATTEMPTS + 1))
        sleep 2
    fi
done

Functions for Reusable Code

#!/bin/bash

# Define function
greet_user() {
    local NAME=$1  # Local variable
    local TIME=${2:-"Morning"}  # Default value
    
    echo "Good $TIME, $NAME!"
    return 0  # Success status
}

# Call function
greet_user "Alice"
greet_user "Bob" "Evening"

# Function with return value
add_numbers() {
    local SUM=$(( $1 + $2 ))
    echo $SUM  # "Return" value
}

RESULT=$(add_numbers 10 20)
echo "Sum: $RESULT"

# Check if function exists
if declare -f "greet_user" > /dev/null; then
    echo "greet_user function exists"
fi

# Error handling in functions
backup_file() {
    local FILE=$1
    local BACKUP_DIR="/backups"
    
    if [ ! -f "$FILE" ]; then
        echo "Error: File not found: $FILE" >&2
        return 1
    fi
    
    if [ ! -d "$BACKUP_DIR" ]; then
        mkdir -p "$BACKUP_DIR" || {
            echo "Error: Cannot create backup directory" >&2
            return 2
        }
    fi
    
    cp "$FILE" "$BACKUP_DIR/"
    echo "Backup created: $BACKUP_DIR/$(basename $FILE)"
}

Error Handling & Debugging

Exit Codes

#!/bin/bash

# Every command returns exit code (0 = success, non-zero = error)
ls /tmp
echo "Exit code: $?"  # Check previous command's exit code

# Test commands
if ls /nonexistent 2>/dev/null; then
    echo "File exists"
else
    echo "File not found (exit code: $?)"
fi

# Set script to exit on error
set -e  # Exit immediately if any command fails
# set -o pipefail  # Also fail if any command in pipeline fails

# Trap signals
cleanup() {
    echo "Cleaning up..."
    rm -f /tmp/tempfile.*
    echo "Cleanup complete"
}

# Call cleanup on exit
trap cleanup EXIT

# Or trap specific signals
trap 'echo "Interrupted!"; exit 1' INT TERM

Debugging Scripts

#!/bin/bash

# Method 1: Verbose mode
set -x  # Print each command before executing
echo "Debugging with set -x"
set +x  # Turn off debugging

# Method 2: Run with debug flag
# bash -x script.sh

# Method 3: Debug specific section
echo "Starting..."
set -x
DEBUG_SECTION="true"
if [ "$DEBUG_SECTION" = "true" ]; then
    echo "Debug section"
    ls -la
fi
set +x
echo "Finished..."

# Method 4: Print variable values
VAR="test"
echo "VAR=$VAR" >&2  # Print to stderr

# Method 5: Use bash debugging tools
# Install: bashdb or shellcheck for linting

Practical DevOps Scripts

1. System Health Check

#!/bin/bash
# system-health.sh

set -e

echo "=== System Health Check ==="
echo "Timestamp: $(date)"
echo ""

# 1. Check disk space
echo "1. Disk Space:"
df -h | grep -E "^/dev/|Filesystem"

# 2. Check memory
echo -e "\n2. Memory Usage:"
free -h

# 3. Check CPU load
echo -e "\n3. CPU Load:"
uptime

# 4. Check running services
echo -e "\n4. Critical Services:"
SERVICES=("ssh" "nginx" "mysql" "docker")
for SERVICE in "${SERVICES[@]}"; do
    if systemctl is-active --quiet "$SERVICE"; then
        echo "  ✅ $SERVICE: RUNNING"
    else
        echo "  ❌ $SERVICE: STOPPED"
    fi
done

# 5. Check network
echo -e "\n5. Network Connections:"
ss -tuln | grep -E ":80|:443|:22"

# 6. Check logs for errors
echo -e "\n6. Recent Errors in logs:"
journalctl --since "1 hour ago" -p err | tail -5

echo -e "\n=== Health Check Complete ==="

2. Backup Script

#!/bin/bash
# backup.sh

set -e

# Configuration
BACKUP_DIR="/var/backups"
SOURCE_DIRS=("/etc" "/var/www" "/home")
BACKUP_NAME="backup-$(date +%Y%m%d-%H%M%S).tar.gz"
LOG_FILE="/var/log/backup.log"

# Functions
log_message() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

check_disk_space() {
    local AVAILABLE=$(df "$BACKUP_DIR" | awk 'NR==2 {print $4}')
    if [ "$AVAILABLE" -lt 1048576 ]; then  # Less than 1GB
        log_message "ERROR: Insufficient disk space"
        return 1
    fi
}

# Main
log_message "=== Starting Backup ==="

# Check prerequisites
if [ ! -d "$BACKUP_DIR" ]; then
    mkdir -p "$BACKUP_DIR"
    log_message "Created backup directory: $BACKUP_DIR"
fi

check_disk_space

# Create backup
log_message "Creating backup: $BACKUP_NAME"
tar -czf "$BACKUP_DIR/$BACKUP_NAME" "${SOURCE_DIRS[@]}" 2>/dev/null

if [ $? -eq 0 ]; then
    BACKUP_SIZE=$(du -h "$BACKUP_DIR/$BACKUP_NAME" | cut -f1)
    log_message "Backup created: $BACKUP_NAME ($BACKUP_SIZE)"
    
    # Remove old backups (keep last 7 days)
    find "$BACKUP_DIR" -name "backup-*.tar.gz" -mtime +7 -delete
    log_message "Cleaned up old backups"
else
    log_message "ERROR: Backup failed"
    exit 1
fi

log_message "=== Backup Complete ==="

3. Deployment Script

#!/bin/bash
# deploy.sh

set -e

# Configuration
APP_NAME="myapp"
ENVIRONMENT=${1:-"staging"}  # Default to staging
DEPLOY_DIR="/var/www/$APP_NAME"
BACKUP_DIR="/var/backups/$APP_NAME"
GIT_REPO="https://github.com/user/$APP_NAME.git"
BRANCH="main"

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# Functions
print_status() {
    echo -e "${GREEN}[✓]${NC} $1"
}

print_error() {
    echo -e "${RED}[✗]${NC} $1" >&2
}

print_warning() {
    echo -e "${YELLOW}[!]${NC} $1"
}

deploy() {
    echo "=== Deploying $APP_NAME to $ENVIRONMENT ==="
    
    # 1. Pre-deployment checks
    if [ ! -d "$DEPLOY_DIR" ]; then
        print_status "Creating deploy directory"
        mkdir -p "$DEPLOY_DIR"
    fi
    
    # 2. Backup current version
    if [ -d "$DEPLOY_DIR/.git" ]; then
        print_status "Backing up current version"
        BACKUP_FILE="$BACKUP_DIR/backup-$(date +%Y%m%d-%H%M%S).tar.gz"
        mkdir -p "$BACKUP_DIR"
        tar -czf "$BACKUP_FILE" -C "$DEPLOY_DIR" .
    fi
    
    # 3. Pull latest code
    print_status "Pulling latest code"
    cd "$DEPLOY_DIR"
    
    if [ -d ".git" ]; then
        git pull origin "$BRANCH"
    else
        git clone -b "$BRANCH" "$GIT_REPO" .
    fi
    
    # 4. Install dependencies
    print_status "Installing dependencies"
    if [ -f "package.json" ]; then
        npm install --production
    fi
    
    if [ -f "requirements.txt" ]; then
        pip install -r requirements.txt
    fi
    
    # 5. Run migrations (if needed)
    if [ -f "manage.py" ]; then
        print_status "Running database migrations"
        python manage.py migrate
    fi
    
    # 6. Restart service
    print_status "Restarting application"
    systemctl restart "$APP_NAME" || true
    
    # 7. Health check
    print_status "Running health check"
    sleep 5
    if curl -s http://localhost:8080/health > /dev/null; then
        print_status "Deployment successful!"
    else
        print_error "Health check failed"
        # Optional: Rollback
        return 1
    fi
}

# Main execution
main() {
    case "$ENVIRONMENT" in
        staging|production)
            deploy
            ;;
        *)
            print_error "Unknown environment: $ENVIRONMENT"
            print_error "Usage: $0 [staging|production]"
            exit 1
            ;;
    esac
}

# Run main function
main "$@"

Best Practices Checklist

Always start with #!/bin/bash shebang
Use set -e to exit on errors
Quote all variables: "$VAR"
Use [[ ]] instead of [ ] for better features
Validate user input and arguments
Add comments for complex logic
Use functions for reusable code
Implement error handling and logging
Test scripts with bash -n script.sh (syntax check)
Use shellcheck for linting
Add usage instructions with -h or --help
Use meaningful variable names
Avoid hardcoded values - use variables or config files
Test with different scenarios and edge cases

Useful One-Liners for DevOps

# Find and delete old log files
find /var/log -name "*.log" -mtime +30 -delete

# Count files by type
find . -type f | sed 's/.*\.//' | sort | uniq -c

# Monitor directory for changes
while inotifywait -r -e modify,create,delete /path/to/watch; do echo "Change detected"; done

# Create multiple directories
mkdir -p /path/to/{dir1,dir2,dir3}/{sub1,sub2}

# Generate random password
openssl rand -base64 32

# Kill processes by name
pkill -f "process_name"

# Find large files
find / -type f -size +100M 2>/dev/null | xargs ls -lh

# Backup with rsync
rsync -avz --delete /source/ user@remote:/backup/

# Check website status
curl -s -o /dev/null -w "%{http_code}" https://example.com

# Disk usage by directory
du -h --max-depth=1 / | sort -hr

# Monitor CPU by process
ps aux --sort=-%cpu | head -10

# Extract IPs from log
grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b" access.log | sort | uniq
Security Best Practices:
1. Never run scripts as root without understanding what they do
2. Validate all user input to prevent injection attacks
3. Use sudo only when necessary, with specific commands
4. Store sensitive data (passwords, API keys) in environment variables or secure vaults
5. Set proper file permissions on scripts (755 for executable, 644 for configs)
6. Use shellcheck to find security issues in scripts
7. Sign important scripts with GPG for integrity verification
Next Steps to Master Bash:
1. Learn advanced topics: arrays, associative arrays, regex
2. Study process management: jobs, signals, traps
3. Master text processing: awk, sed, grep patterns
4. Practice with real DevOps scenarios
5. Read existing scripts from open source projects
6. Contribute to automation scripts in your organization
7. Explore alternatives for complex tasks: Python, Go

Your DevOps Automation Journey Starts Here

Bash scripting is not just about writing code - it's about thinking in automation. Every manual task you do more than once is a candidate for scripting.

Remember: Start small. Automate one task. Make it reliable. Then automate another. Soon you'll have a toolkit that makes you 10x more productive.

Practice Exercise: Take a task you do daily (like checking logs or deploying updates) and write a script for it. Make it better each day by adding error handling, logging, and user options.

Quick Reference:
• Create script: nano script.sh
• Make executable: chmod +x script.sh
• Run script: ./script.sh
• Debug: bash -x script.sh
• Check syntax: bash -n script.sh
• Lint: shellcheck script.sh
• Variables: VAR="value", echo "$VAR"
• Condition: if [ condition ]; then ... fi
• Loop: for i in list; do ... done
• Function: name() { commands; }