Skip to content
beginner Phase 3 · Shell and Automation

Bash Scripting Fundamentals

Write and execute Bash scripts with proper shebangs, comments, and basic structure for automation tasks.

50m
0 problems
Topic Progress 0%

Script Basics and Variables

Script Basics and Variables

Shebang and Basic Structure

#!/bin/bash
set -euo pipefail   # Strict mode: exit on error, undefined vars, pipe failures

# Your script content here
echo "Hello, $USER"

set -euo pipefail explained:

  • -e: Exit immediately if a command exits with non-zero status
  • -u: Treat unset variables as errors
  • -o pipefail: A pipeline fails if any command fails (not just the last)

Variables

# Assigning variables (NO spaces around =)
NAME="production"
ENVIRONMENT="staging"
PORT=8080

# Using variables
echo "Deploying to $NAME"
echo "Environment: ${ENVIRONMENT}"
echo "Port: $PORT"

# String concatenation
FULL_NAME="${ENVIRONMENT}-${NAME}"
echo "Full: $FULL_NAME"

# Default values
echo "${DB_HOST:-localhost}"        # Use 'localhost' if DB_HOST is unset
echo "${DB_PORT:=5432}"             # Set to 5432 if unset
echo "${CONFIG_FILE:?Config not set}"  # Exit with error if unset

# Command substitution
echo "Today is $(date +%Y-%m-%d)"
TIMESTAMP=$(date +%s)
LINE_COUNT=$(wc -l < /etc/passwd)
echo "Password file has $LINE_COUNT lines"

# Arithmetic expansion
COUNT=5
NEXT=$((COUNT + 1))
echo "Next: $NEXT"

# Arrays
declare -a SERVERS=("web1" "web2" "web3")
echo "First server: ${SERVERS[0]}"
echo "All servers: ${SERVERS[@]}"
echo "Number of servers: ${#SERVERS[@]}"

# Iterate over array
for server in "${SERVERS[@]}"; do
    echo "Pinging $server..."
    ping -c 1 -W 1 "$server" &>/dev/null && echo "$server is up" || echo "$server is down"
done

Conditionals and Control Flow

Conditionals and Control Flow

if/elif/else

#!/bin/bash
set -euo pipefail

ENV="production"

# Basic if/else
if [ "$ENV" = "production" ]; then
    echo "Running in production mode"
elif [ "$ENV" = "staging" ]; then
    echo "Running in staging mode"
else
    echo "Running in development mode"
fi

# Test conditions
FILE="/etc/nginx/nginx.conf"

if [ -f "$FILE" ]; then
    echo "$FILE exists and is a regular file"
fi

if [ -d "/var/log" ]; then
    echo "/var/log is a directory"
fi

if [ -r "$FILE" ]; then
    echo "$FILE is readable"
fi

if [ -w "$FILE" ]; then
    echo "$FILE is writable"
fi

if [ -x "/usr/bin/docker" ]; then
    echo "Docker is installed"
fi

if [ -s "$FILE" ]; then
    echo "$FILE has content (non-empty)"
fi

# String comparisons
if [ "$ENV" != "development" ]; then
    echo "Not in development"
fi

if [ -z "$EMPTY_VAR" ]; then
    echo "Variable is empty or unset"
fi

if [ -n "$ENV" ]; then
    echo "Variable is set and non-empty"
fi

# Numeric comparisons
PORT=8080
if [ "$PORT" -gt 1024 ]; then
    echo "Non-privileged port"
fi

# -eq  equal
# -ne  not equal
# -gt  greater than
# -ge  greater or equal
# -lt  less than
# -le  less or equal

# Logical operators
if [ "$ENV" = "production" ] && [ -f "/etc/nginx/nginx.conf" ]; then
    echo "Production with nginx"
fi

if [ "$ENV" = "development" ] || [ "$ENV" = "staging" ]; then
    echo "Non-production environment"
fi

# Case statement
DEPLOY_TARGET="api"
case "$DEPLOY_TARGET" in
    api)
        echo "Deploying API service"
        ;;
    web)
        echo "Deploying web frontend"
        ;;
    worker)
        echo "Deploying background worker"
        ;;
    *)
        echo "Unknown deploy target: $DEPLOY_TARGET"
        exit 1
        ;;
esac

Loops and Iteration

Loops and Iteration

For Loops

#!/bin/bash
set -euo pipefail

# Iterate over a list
for env in dev staging production; do
    echo "Deploying to $env"
    # kubectl apply --env=$env ...
done

# Iterate over files
for file in /var/log/*.log; do
    if [ -f "$file" ]; then
        SIZE=$(du -h "$file" | cut -f1)
        echo "$file: $SIZE"
    fi
done

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

# Iterate over numbers
for port in {8080..8090}; do
    echo "Checking port $port..."
    ss -tln | grep -q ":$port " && echo "Port $port in use" || echo "Port $port available"
done

# Iterate over command output
for user in $(grep '/bin/bash' /etc/passwd | cut -d: -f1); do
    echo "User with bash: $user"
done

# Parallel execution with xargs
find /var/log -name '*.log' -mtime +30 | xargs -P 4 -I {} gzip {}

While Loops

# Read file line by line
while IFS= read -r line; do
    echo "Processing: $line"
done < servers.txt

# Read command output line by line
kubectl get pods --all-namespaces | while read -r namespace pod status rest; do
    if [ "$status" != "Running" ]; then
        echo "Pod $pod in $namespace is $status"
    fi
done

# Infinite loop with break
while true; do
    if curl -sf http://localhost:8080/health > /dev/null 2>&1; then
        echo "Service is ready"
        break
    fi
    echo "Waiting for service..."
    sleep 5
done

# Wait for condition with timeout
TIMEOUT=60
ELAPSED=0
while [ $ELAPSED -lt $TIMEOUT ]; do
    if docker inspect mycontainer > /dev/null 2>&1; then
        echo "Container is running"
        break
    fi
    sleep 1
    ELAPSED=$((ELAPSED + 1))
done

if [ $ELAPSED -ge $TIMEOUT ]; then
    echo "Timeout waiting for container"
    exit 1
fi

Loop Control

# break - exit the loop
for server in web1 web2 web3 web4; do
    if ! ping -c 1 -W 1 "$server" &>/dev/null; then
        echo "$server is down, stopping check"
        break
    fi
    echo "$server is up"
done

# continue - skip to next iteration
for file in /tmp/*.tmp; do
    [ -f "$file" ] || continue    # Skip if not a file
    echo "Processing $file"
done

# Nested loops with labels
OUTER:
for i in {1..3}; do
    for j in {1..3}; do
        if [ $((i * j)) -gt 4 ]; then
            echo "Breaking outer at i=$i, j=$j"
            break OUTER
        fi
        echo "i=$i, j=$j, product=$((i * j))"
    done
done

Functions and Error Handling

Functions and Error Handling

Functions

#!/bin/bash
set -euo pipefail

# Function definition
check_service() {
    local host="$1"
    local port="$2"
    local timeout="${3:-5}"

    if timeout "$timeout" bash -c "echo >/dev/tcp/$host/$port" 2>/dev/null; then
        echo "Service $host:$port is UP"
        return 0
    else
        echo "Service $host:$port is DOWN"
        return 1
    fi
}

# Call function
check_service "localhost" "8080"
check_service "db.example.com" "5432" 10

# Function with output capture
get_container_ip() {
    local container_name="$1"
    docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container_name" 2>/dev/null
}

IP=$(get_container_ip "myapp")
echo "Container IP: $IP"

# Return values vs output
# Use 'return' for status codes (0-255)
# Use 'echo' for data output
# Use 'printf' for complex output

Error Handling

#!/bin/bash
set -euo pipefail

# Trap errors
trap 'echo "ERROR on line $LINENO. Exit code: $?"' ERR

# Trap exit (cleanup)
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT

echo "Working in $TMPDIR"

# Retry logic
retry() {
    local max_attempts=$1
    local delay=$2
    shift 2
    local cmd=("$@")

    local attempt=1
    while [ $attempt -le $max_attempts ]; do
        if "${cmd[@]}"; then
            return 0
        fi
        echo "Attempt $attempt failed. Retrying in ${delay}s..."
        sleep "$delay"
        attempt=$((attempt + 1))
    done

    echo "All $max_attempts attempts failed"
    return 1
}

# Usage
retry 3 5 curl -sf http://localhost:8080/health

# Logging functions
log_info()  { echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO:  $*"; }
log_warn()  { echo "[$(date '+%Y-%m-%d %H:%M:%S')] WARN:  $*" >&2; }
log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2; }

log_info "Deployment started"
log_warn "Disk usage at 80%"
log_error "Service failed to start"

# Validate inputs
validate_port() {
    local port="$1"
    if ! [[ "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
        log_error "Invalid port: $port"
        return 1
    fi
    return 0
}

Command-Line Arguments

#!/bin/bash
set -euo pipefail

# Parse named arguments
while [[ $# -gt 0 ]]; do
    case $1 in
        -e|--env)
            ENV="$2"
            shift 2
            ;;
        -p|--port)
            PORT="$2"
            shift 2
            ;;
        -h|--help)
            echo "Usage: $0 -e <env> -p <port>"
            echo "  -e, --env     Environment (dev|staging|prod)"
            echo "  -p, --port    Port number"
            exit 0
            ;;
        *)
            log_error "Unknown option: $1"
            exit 1
            ;;
    esac
done

# Set defaults
ENV="${ENV:-development}"
PORT="${PORT:-8080}"

echo "Environment: $ENV, Port: $PORT"

Complete DevOps Script Example

#!/bin/bash
set -euo pipefail

log_info()  { echo "[$(date '+%H:%M:%S')] INFO:  $*"; }
log_error() { echo "[$(date '+%H:%M:%S')] ERROR: $*" >&2; }

# Configuration
APP_NAME="myapp"
DEPLOY_ENV="${1:-staging}"
HEALTH_URL="http://localhost:8080/health"

# Deploy function
deploy() {
    log_info "Building $APP_NAME for $DEPLOY_ENV..."
    docker build -t "$APP_NAME:$DEPLOY_ENV" .

    log_info "Stopping old container..."
    docker stop "$APP_NAME" 2>/dev/null || true
    docker rm "$APP_NAME" 2>/dev/null || true

    log_info "Starting new container..."
    docker run -d --name "$APP_NAME" -p 8080:8080 "$APP_NAME:$DEPLOY_ENV"

    log_info "Waiting for health check..."
    local retries=30
    while [ $retries -gt 0 ]; do
        if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then
            log_info "Deployment successful!"
            return 0
        fi
        retries=$((retries - 1))
        sleep 2
    done

    log_error "Health check failed after 60s"
    docker logs "$APP_NAME" | tail -20
    return 1
}

# Rollback function
rollback() {
    log_error "Rolling back..."
    docker stop "$APP_NAME" 2>/dev/null || true
    docker rm "$APP_NAME" 2>/dev/null || true
    docker run -d --name "$APP_NAME" -p 8080:8080 "$APP_NAME:previous"
}

trap rollback ERR

# Execute
deploy
log_info "Deploy complete"