Master the building blocks of Bash scripting. Learn how to store data in variables, make decisions with conditionals, and automate repetitive tasks with loops. Essential skills for any DevOps engineer.
Store and manipulate data
Types: Strings, Numbers, Arrays
Scopes: Local, Global, Environment
Make decisions in scripts
if, elif, else, case
Comparison operators
Automate repetitive tasks
for, while, until
Loop control: break, continue
Part 1: Variables Deep Dive
Variable Types and Declaration
#!/bin/bash
# 1. Basic variables
STRING_VAR="Hello World"
NUMBER_VAR=42
BOOLEAN_VAR=true
echo "String: $STRING_VAR"
echo "Number: $NUMBER_VAR"
echo "Boolean: $BOOLEAN_VAR"
# 2. Variable expansion
FILE="backup.tar.gz"
echo "Filename: ${FILE}"
echo "Without extension: ${FILE%.*}" # backup.tar
echo "Extension only: ${FILE##*.}" # gz
echo "Basename: ${FILE##*/}" # backup.tar.gz
# 3. Default values
CONFIG_FILE=${CONFIG_FILE:-"/etc/app.conf"}
BACKUP_DIR=${BACKUP_DIR:-"$HOME/backups"}
echo "Config: $CONFIG_FILE"
echo "Backup: $BACKUP_DIR"
# 4. Command substitution
CURRENT_DATE=$(date)
FILE_COUNT=$(ls | wc -l)
SYSTEM_UPTIME=$(uptime -p)
echo "Date: $CURRENT_DATE"
echo "Files: $FILE_COUNT"
echo "Uptime: $SYSTEM_UPTIME"
# 5. Arithmetic operations
COUNT=10
COUNT=$((COUNT + 5))
MULTIPLY=$((COUNT * 2))
DIVIDE=$((MULTIPLY / 3))
MODULO=$((COUNT % 3))
echo "Count: $COUNT"
echo "Multiply: $MULTIPLY"
echo "Divide: $DIVIDE"
echo "Modulo: $MODULO"
# 6. Increment/Decrement
((COUNT++)) # Increment
((COUNT--)) # Decrement
((COUNT += 5)) # Add 5
((COUNT -= 2)) # Subtract 2
Arrays: Storing Multiple Values
#!/bin/bash
# 1. Indexed arrays
SERVERS=("web1" "web2" "db1" "cache1")
echo "All servers: ${SERVERS[@]}"
echo "First server: ${SERVERS[0]}"
echo "Server count: ${#SERVERS[@]}"
# Add elements
SERVERS+=("web3")
SERVERS[4]="db2"
# Loop through array
for SERVER in "${SERVERS[@]}"; do
echo "Server: $SERVER"
done
# 2. Associative arrays (bash 4+)
declare -A CONFIG
CONFIG["hostname"]="server1"
CONFIG["port"]="8080"
CONFIG["environment"]="production"
echo "Hostname: ${CONFIG[hostname]}"
echo "Port: ${CONFIG[port]}"
# Loop through associative array
for KEY in "${!CONFIG[@]}"; do
echo "$KEY = ${CONFIG[$KEY]}"
done
# 3. Read array from file
readarray -t LOG_FILES < <(find /var/log -name "*.log" | head -5)
echo "Log files: ${LOG_FILES[@]}"
# 4. Array slicing
ALL_NUMBERS=(1 2 3 4 5 6 7 8 9 10)
FIRST_THREE=("${ALL_NUMBERS[@]:0:3}")
LAST_THREE=("${ALL_NUMBERS[@]: -3}")
MIDDLE=("${ALL_NUMBERS[@]:3:4}")
echo "First three: ${FIRST_THREE[@]}"
echo "Last three: ${LAST_THREE[@]}"
echo "Middle: ${MIDDLE[@]}"
Special Variables and Parameters
$0echo "Script: $0"$1, $2, ...echo "First: $1, Second: $2"$#echo "Args count: $#"$@for arg in "$@"; do echo "$arg"; done$*echo "All args: $*"$?ls /tmp; echo "Exit: $?"$$echo "PID: $$"$!sleep 10 & echo "Job PID: $!"Part 2: Conditionals Mastery
Basic if Statements
#!/bin/bash
# 1. Simple if
if [ -f "/etc/passwd" ]; then
echo "File exists"
fi
# 2. if-else
if [ "$USER" = "root" ]; then
echo "Running as root"
else
echo "Running as $USER"
fi
# 3. if-elif-else
read -p "Enter score (0-100): " SCORE
if [ "$SCORE" -ge 90 ]; then
GRADE="A"
elif [ "$SCORE" -ge 80 ]; then
GRADE="B"
elif [ "$SCORE" -ge 70 ]; then
GRADE="C"
elif [ "$SCORE" -ge 60 ]; then
GRADE="D"
else
GRADE="F"
fi
echo "Grade: $GRADE"
# 4. Nested if
if [ -d "/var/www" ]; then
echo "Web directory exists"
if [ -w "/var/www" ]; then
echo "And is writable"
else
echo "But not writable"
fi
fi
Advanced Conditional Operators
-a or &&[ "$a" -gt 0 -a "$a" -lt 10 ]-o or ||[ "$a" -eq 0 -o "$b" -eq 0 ]![ ! -f "$file" ]-eq, -ne[ "$a" -eq "$b" ]-lt, -gt[ "$a" -lt "$b" ]-le, -ge[ "$a" -le "$b" ]-z, -n[ -z "$var" ]=, !=[ "$str1" = "$str2" ]Case Statements: Elegant Multiple Conditions
#!/bin/bash
# 1. Basic case
read -p "Enter action (start|stop|restart|status): " ACTION
case $ACTION in
start)
echo "Starting service..."
systemctl start nginx
;;
stop)
echo "Stopping service..."
systemctl stop nginx
;;
restart)
echo "Restarting service..."
systemctl restart nginx
;;
status)
echo "Checking status..."
systemctl status nginx
;;
*) # Default case
echo "Unknown action: $ACTION"
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac
# 2. Pattern matching
read -p "Enter filename: " FILENAME
case $FILENAME in
*.txt)
echo "Text file"
;;
*.jpg|*.jpeg|*.png|*.gif)
echo "Image file"
;;
*.sh)
echo "Shell script"
chmod +x "$FILENAME"
;;
*)
echo "Unknown file type"
;;
esac
# 3. Multiple patterns with fallthrough
read -p "Enter HTTP status code: " CODE
case $CODE in
200)
echo "OK - Success"
;;
301|302)
echo "Redirect"
;;
400|401|403|404)
echo "Client Error"
;;
500|501|502|503)
echo "Server Error"
;;
[1-9][0-9][0-9])
echo "Other HTTP code"
;;
*)
echo "Invalid code"
;;
esac
Modern [[ ]] Double Bracket Syntax
#!/bin/bash
# Use [[ ]] instead of [ ] for better features
# 1. String comparison (no quotes needed)
if [[ $USER == root ]]; then
echo "Running as root"
fi
# 2. Pattern matching
FILENAME="backup_20251207.tar.gz"
if [[ $FILENAME == backup_*.tar.gz ]]; then
echo "Valid backup file"
fi
# 3. Regex matching
EMAIL="user@example.com"
if [[ $EMAIL =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "Valid email"
fi
# 4. Logical operators
if [[ -f "/etc/passwd" && -r "/etc/passwd" ]]; then
echo "File exists and is readable"
fi
# 5. Compound conditions
NUMBER=15
if [[ $NUMBER -gt 10 && $NUMBER -lt 20 ]]; then
echo "Number between 10 and 20"
fi
# 6. String operations
STR="Hello World"
if [[ ${#STR} -gt 5 ]]; then
echo "String length: ${#STR}"
fi
if [[ $STR == *World* ]]; then
echo "Contains 'World'"
fi
Part 3: Loops in Depth
For Loops: Iterating Over Collections
#!/bin/bash
# 1. Basic for loop
for COLOR in red green blue yellow; do
echo "Color: $COLOR"
done
# 2. Iterate over array
SERVERS=("web1" "web2" "db1" "cache1")
for SERVER in "${SERVERS[@]}"; do
echo "Pinging $SERVER..."
ping -c 1 "$SERVER" > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "$SERVER is UP"
else
echo "$SERVER is DOWN"
fi
done
# 3. C-style for loop
for ((i=1; i<=10; i++)); do
echo "Iteration $i"
done
# 4. Loop with step
for ((i=0; i<=100; i+=10)); do
echo "Progress: $i%"
done
# 5. Loop through command output
for USER in $(cut -d: -f1 /etc/passwd | head -10); do
echo "User: $USER"
id "$USER"
done
# 6. Loop through files
for FILE in /var/log/*.log; do
if [ -f "$FILE" ]; then
echo "Processing: $(basename "$FILE")"
wc -l "$FILE"
fi
done
# 7. Loop with index
FRUITS=("apple" "banana" "cherry" "date")
for i in "${!FRUITS[@]}"; do
echo "Index $i: ${FRUITS[$i]}"
done
While Loops: Condition-Based Iteration
#!/bin/bash
# 1. Basic while loop
COUNT=1
while [ $COUNT -le 5 ]; do
echo "Count: $COUNT"
((COUNT++))
done
# 2. Read file line by line
while read LINE; do
echo "Line: $LINE"
done < /etc/hosts
# 3. Process command output
df -h | while read FILESYSTEM SIZE USED AVAIL USE MOUNT; do
if [[ $USE > 80% ]]; then
echo "WARNING: $FILESYSTEM is $USE full"
fi
done
# 4. Infinite loop with break
while true; do
read -p "Enter command (q to quit): " CMD
if [[ $CMD == "q" ]]; then
echo "Goodbye!"
break
fi
echo "You entered: $CMD"
done
# 5. Wait for condition
SERVER_UP=false
ATTEMPT=1
MAX_ATTEMPTS=10
while [ $SERVER_UP = false ] && [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
echo "Checking server... Attempt $ATTEMPT/$MAX_ATTEMPTS"
if curl -s http://localhost:8080 > /dev/null; then
SERVER_UP=true
echo "Server is UP!"
else
echo "Server not responding"
((ATTEMPT++))
sleep 2
fi
done
# 6. Process multiple files
find /tmp -name "*.tmp" -type f | while read FILE; do
echo "Cleaning: $FILE"
rm -f "$FILE"
done
Until Loops: Execute Until Condition Met
#!/bin/bash
# 1. Basic until loop (opposite of while)
COUNT=10
until [ $COUNT -lt 1 ]; do
echo "Countdown: $COUNT"
((COUNT--))
done
echo "Blast off!"
# 2. Wait for service to start
PORT_OPEN=false
until [ $PORT_OPEN = true ]; do
echo "Checking port 8080..."
if nc -z localhost 8080 2>/dev/null; then
PORT_OPEN=true
echo "Port 8080 is open"
else
echo "Port not ready, waiting..."
sleep 2
fi
done
# 3. Process until user quits
INPUT=""
until [[ $INPUT == "quit" ]]; do
read -p "Enter data (quit to exit): " INPUT
if [[ $INPUT != "quit" ]]; then
echo "You entered: $INPUT"
# Process input here
fi
done
# 4. Until with timeout
TIMEOUT=30
START_TIME=$(date +%s)
CONNECTED=false
until [ $CONNECTED = true ] || [ $(($(date +%s) - START_TIME)) -gt $TIMEOUT ]; do
echo "Trying to connect..."
if mysql -h localhost -u root -e "SELECT 1" 2>/dev/null; then
CONNECTED=true
echo "Connected to database"
else
echo "Connection failed, retrying..."
sleep 2
fi
done
if [ $CONNECTED = false ]; then
echo "ERROR: Connection timeout after $TIMEOUT seconds"
fi
Loop Control: break and continue
#!/bin/bash
# 1. break - exit loop early
for i in {1..10}; do
if [ $i -eq 5 ]; then
echo "Breaking at $i"
break
fi
echo "Iteration: $i"
done
# 2. continue - skip to next iteration
for i in {1..10}; do
if [ $((i % 2)) -eq 0 ]; then
continue # Skip even numbers
fi
echo "Odd number: $i"
done
# 3. Nested loops with labeled break
for i in {1..3}; do
echo "Outer loop: $i"
for j in {1..3}; do
if [ $j -eq 2 ]; then
break 2 # Break out of 2 levels
fi
echo " Inner loop: $j"
done
done
# 4. Practical example: Find first match
FILES=("/etc/passwd" "/etc/group" "/nonexistent" "/etc/hosts")
for FILE in "${FILES[@]}"; do
if [ ! -f "$FILE" ]; then
echo "ERROR: $FILE not found"
continue
fi
echo "Processing $FILE"
if grep -q "root" "$FILE"; then
echo "Found 'root' in $FILE"
break # Stop after first match
fi
done
# 5. Skip errors with continue
for HOST in web1 web2 db1 invalid cache1; do
echo "Testing $HOST..."
if ! ping -c 1 "$HOST" > /dev/null 2>&1; then
echo " Skipping $HOST (unreachable)"
continue
fi
echo " $HOST is reachable"
# Further processing...
done
Practical DevOps Examples
Example 1: Configuration Validator
#!/bin/bash
# config-validator.sh
set -e
echo "=== Configuration Validator ==="
# Arrays of required and optional settings
REQUIRED_SETTINGS=("DB_HOST" "DB_PORT" "API_KEY")
OPTIONAL_SETTINGS=("LOG_LEVEL" "CACHE_TTL" "DEBUG_MODE")
# Check required settings
MISSING_SETTINGS=()
for SETTING in "${REQUIRED_SETTINGS[@]}"; do
if [ -z "${!SETTING}" ]; then
MISSING_SETTINGS+=("$SETTING")
fi
done
if [ ${#MISSING_SETTINGS[@]} -gt 0 ]; then
echo "ERROR: Missing required settings:"
for SETTING in "${MISSING_SETTINGS[@]}"; do
echo " - $SETTING"
done
exit 1
fi
# Validate settings
ERRORS=()
# Validate DB_PORT is number
if ! [[ $DB_PORT =~ ^[0-9]+$ ]]; then
ERRORS+=("DB_PORT must be a number")
elif [ $DB_PORT -lt 1 ] || [ $DB_PORT -gt 65535 ]; then
ERRORS+=("DB_PORT must be between 1-65535")
fi
# Validate LOG_LEVEL if set
if [ -n "$LOG_LEVEL" ]; then
case $LOG_LEVEL in
debug|info|warn|error)
# Valid level
;;
*)
ERRORS+=("LOG_LEVEL must be: debug, info, warn, or error")
;;
esac
fi
# Report results
if [ ${#ERRORS[@]} -eq 0 ]; then
echo "✓ All configuration settings are valid"
# Show configuration
echo ""
echo "Configuration Summary:"
for SETTING in "${REQUIRED_SETTINGS[@]}" "${OPTIONAL_SETTINGS[@]}"; do
if [ -n "${!SETTING}" ]; then
echo " $SETTING=${!SETTING}"
fi
done
else
echo "ERROR: Configuration validation failed:"
for ERROR in "${ERRORS[@]}"; do
echo " - $ERROR"
done
exit 1
fi
Example 2: Log File Processor
#!/bin/bash
# log-processor.sh
set -e
LOG_DIR="/var/log/app"
OUTPUT_FILE="/tmp/log-analysis-$(date +%Y%m%d).csv"
ERROR_THRESHOLD=10
echo "=== Log File Processor ==="
# Check if log directory exists
if [ ! -d "$LOG_DIR" ]; then
echo "ERROR: Log directory $LOG_DIR not found"
exit 1
fi
# Initialize counters
declare -A ERROR_COUNTS
declare -A IP_COUNTS
TOTAL_LINES=0
ERROR_LINES=0
# Process each log file
for LOG_FILE in "$LOG_DIR"/*.log; do
if [ ! -f "$LOG_FILE" ]; then
continue
fi
echo "Processing: $(basename "$LOG_FILE")"
# Read file line by line
while read -r LINE; do
((TOTAL_LINES++))
# Count errors
if [[ $LINE =~ ERROR|FATAL ]]; then
((ERROR_LINES++))
# Extract error type
if [[ $LINE =~ \[([A-Z_]+)\] ]]; then
ERROR_TYPE="${BASH_REMATCH[1]}"
((ERROR_COUNTS["$ERROR_TYPE"]++))
fi
# Extract IP address
if [[ $LINE =~ ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) ]]; then
IP="${BASH_REMATCH[1]}"
((IP_COUNTS["$IP"]++))
fi
fi
done < "$LOG_FILE"
done
# Generate report
echo ""
echo "=== Analysis Report ==="
echo "Total lines processed: $TOTAL_LINES"
echo "Error lines: $ERROR_LINES"
echo "Error rate: $((ERROR_LINES * 100 / TOTAL_LINES))%"
if [ $ERROR_LINES -gt $ERROR_THRESHOLD ]; then
echo "⚠ WARNING: Error count exceeds threshold ($ERROR_THRESHOLD)"
fi
echo ""
echo "Error types:"
for ERROR_TYPE in "${!ERROR_COUNTS[@]}"; do
echo " $ERROR_TYPE: ${ERROR_COUNTS[$ERROR_TYPE]}"
done | sort -nr -k2
echo ""
echo "Top IPs with errors:"
for IP in "${!IP_COUNTS[@]}"; do
echo " $IP: ${IP_COUNTS[$IP]}"
done | sort -nr -k2 | head -5
# Export to CSV
echo "timestamp,total_lines,error_lines,error_rate" > "$OUTPUT_FILE"
echo "$(date),$TOTAL_LINES,$ERROR_LINES,$((ERROR_LINES * 100 / TOTAL_LINES))" >> "$OUTPUT_FILE"
echo ""
echo "Report saved to: $OUTPUT_FILE"
Example 3: Server Deployment Automation
#!/bin/bash
# server-deploy.sh
set -e
# Configuration
SERVER_LIST=("web1.example.com" "web2.example.com" "db1.example.com")
DEPLOY_USER="deploy"
APP_DIR="/var/www/app"
BACKUP_DIR="/backups"
MAX_RETRIES=3
# Colors for output
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
print_status() { echo -e "${GREEN}[✓]${NC} $1"; }
print_error() { echo -e "${RED}[✗]${NC} $1" >&2; }
print_warning() { echo -e "${YELLOW}[!]${NC} $1"; }
deploy_to_server() {
local SERVER=$1
local ATTEMPT=1
while [ $ATTEMPT -le $MAX_RETRIES ]; do
echo "Deploying to $SERVER (Attempt $ATTEMPT/$MAX_RETRIES)"
# SSH command with timeout
if ssh -o ConnectTimeout=10 -o BatchMode=yes "$DEPLOY_USER@$SERVER" "
set -e
echo 'Creating backup...'
BACKUP_NAME=\"app-backup-\$(date +%Y%m%d-%H%M%S).tar.gz\"
tar -czf \"$BACKUP_DIR/\$BACKUP_NAME\" -C \"$APP_DIR\" .
echo 'Pulling latest code...'
cd \"$APP_DIR\"
git pull origin main
echo 'Installing dependencies...'
npm install --production
echo 'Running migrations...'
npm run migrate
echo 'Restarting service...'
sudo systemctl restart app
"; then
print_status "Deployment to $SERVER successful"
return 0
else
print_warning "Deployment to $SERVER failed (Attempt $ATTEMPT)"
((ATTEMPT++))
sleep 2
fi
done
print_error "Failed to deploy to $SERVER after $MAX_RETRIES attempts"
return 1
}
# Main deployment
FAILED_SERVERS=()
for SERVER in "${SERVER_LIST[@]}"; do
echo ""
echo "=== Processing $SERVER ==="
# Check server connectivity
if ! ping -c 1 -W 2 "$SERVER" > /dev/null 2>&1; then
print_error "$SERVER is unreachable"
FAILED_SERVERS+=("$SERVER")
continue
fi
# Deploy to server
if deploy_to_server "$SERVER"; then
print_status "$SERVER deployment completed"
else
FAILED_SERVERS+=("$SERVER")
fi
done
# Summary
echo ""
echo "=== Deployment Summary ==="
if [ ${#FAILED_SERVERS[@]} -eq 0 ]; then
print_status "All servers deployed successfully!"
else
print_error "Deployment failed on: ${FAILED_SERVERS[*]}"
exit 1
fi
Advanced Patterns
Best Practices Checklist
"$VAR"[[ ]] instead of [ ] for conditionals$(command) over backticksdeclare for typed variablesarr=()local for function variables${VAR:-default}break and continue for loop controlcase over multiple if-elif for string matching$((expression))1. Unquoted variables leading to word splitting
2. Using
= for string comparison instead of == in [[ ]]3. Forgetting that
[ ] requires spaces: [ "$a" = "$b" ]4. Using
-a/-o inside [[ ]] (use &&/|| instead)5. Not handling empty arrays properly
6. Infinite loops without exit conditions
7. Not checking command exit codes
8. Using variables before they're set
• Use built-in string operations instead of external commands
• Avoid unnecessary subshells
$(command)• Use process substitution
<(command) for command output• Prefer shell builtins over external utilities when possible
• Use arrays instead of parsing strings multiple times
• Minimize file I/O in loops
Mastering Control Structures
Variables, loops, and conditionals are the foundation of all programming. In Bash scripting for DevOps, mastering these concepts means you can automate almost anything.
Remember: The best way to learn is by doing. Take a manual process you do regularly and automate it using these building blocks. Start simple, test thoroughly, and iterate.
Practice Exercise: Create a script that monitors system resources (CPU, memory, disk) and sends alerts when thresholds are exceeded. Use variables for thresholds, conditionals for checking, and loops for continuous monitoring.
Variables:
VAR="value", ${VAR}, $(command), $((math))Arrays:
arr=("a" "b"), ${arr[@]}, ${#arr[@]}Conditionals:
if [[ condition ]]; then ... fiCase:
case $var in pattern) ... ;; esacFor loop:
for i in list; do ... doneWhile loop:
while condition; do ... doneUntil loop:
until condition; do ... doneLoop control:
break, continue