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:
Automate repetitive tasks like deployments, backups, and monitoring
Manage servers, install software, configure services
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
#!/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
Comparison Operators
=, !=[ "$a" = "$b" ]-z, -n[ -z "$var" ]-eq, -ne[ $a -eq $b ]-lt, -gt[ $a -lt $b ]-f, -d[ -f file.txt ]-r, -w[ -r file.txt ]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
#!/bin/bash shebangset -e to exit on errors"$VAR"[[ ]] instead of [ ] for better featuresbash -n script.sh (syntax check)shellcheck for linting-h or --helpUseful 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
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 commands4. 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 scripts7. Sign important scripts with GPG for integrity verification
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.
• 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; }