Simple Bash scripts use variables and loops. Advanced Bash scripts use arrays to manage lists of data, associative arrays for key-value storage, and select menus for interactive user choices. These patterns separate novice scripts from professional automation tools. In this lesson, you'll level up your Bash skills with the techniques used in production deployment scripts.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Create and manipulate indexed arrays
  • Use associative arrays for key-value data
  • Build interactive select menus
  • Use here documents for multi-line text
  • Understand process substitution
  • Create advanced interactive scripts

2. Why This Matters

Real-world scenario: You need to deploy an application to 20 servers. You have a list of server names, their IP addresses, and their roles (web, db, cache). Without arrays, you'd have 20 variables. With arrays, you manage everything in one data structure.

When to use each type:

  • Indexed arrays: Lists of items (servers, files, users)
  • Associative arrays: Key-value pairs (server->IP, name->role)
  • Select menus: Interactive user choices

3. Core Concepts

Indexed Arrays (Lists)

#!/bin/bash
# Indexed arrays - ordered lists

# Create arrays (multiple ways)
servers=("web-01" "web-02" "db-01" "cache-01")
files=(*.txt)  # Create from glob pattern
empty=()       # Empty array

# Access elements (zero-indexed)
echo "First server: ${servers[0]}"      # web-01
echo "Second server: ${servers[1]}"     # web-02
echo "All servers: ${servers[@]}"       # web-01 web-02 db-01 cache-01
echo "Array length: ${#servers[@]}"     # 4

# Modify arrays
servers[4]="cache-02"                    # Add at index 4
servers+=("monitor-01")                  # Append to end
unset servers[2]                         # Remove index 2 (db-01)

# Slice arrays (start:length)
echo "First two: ${servers[@]:0:2}"

# Loop through arrays
echo "=== Server List ==="
for server in "${servers[@]}"; do
    echo "Server: $server"
done

# Loop with index
for i in "${!servers[@]}"; do
    echo "Index $i: ${servers[$i]}"
done
Working with indexed arrays

Associative Arrays (Dictionaries/Maps)

#!/bin/bash
# Associative arrays - key-value pairs (Bash 4.0+)

# Declare associative array
declare -A server_ip
declare -A config

# Add key-value pairs
server_ip["web-01"]="10.0.1.10"
server_ip["web-02"]="10.0.1.11"
server_ip["db-01"]="10.0.2.10"

# Or declare all at once
declare -A server_role=(
    ["web-01"]="webserver"
    ["web-02"]="webserver"
    ["db-01"]="database"
)

# Access values
echo "web-01 IP: ${server_ip["web-01"]}"
echo "db-01 role: ${server_role["db-01"]}"

# Get all keys or values
echo "All servers: ${!server_ip[@]}"
echo "All IPs: ${server_ip[@]}"

# Loop through associative array
echo "=== Server Configuration ==="
for server in "${!server_ip[@]}"; do
    echo "$server -> ${server_ip[$server]} (${server_role[$server]})"
done

# Check if key exists
if [[ -v server_ip["web-03"] ]]; then
    echo "web-03 exists"
else
    echo "web-03 not found"
fi

# Remove entry
unset server_ip["web-02"]
Working with associative arrays

Select Menus (Interactive User Choice)

#!/bin/bash
# Select menu - interactive choice menu

# Simple select menu
options=("Start Service" "Stop Service" "Restart Service" "View Status" "Exit")

echo "Choose an option:"
select choice in "${options[@]}"; do
    case $choice in
        "Start Service")
            echo "Starting service..."
            # systemctl start myservice
            ;;
        "Stop Service")
            echo "Stopping service..."
            ;;
        "Restart Service")
            echo "Restarting service..."
            ;;
        "View Status")
            echo "Service is running"
            ;;
        "Exit")
            echo "Goodbye!"
            break
            ;;
        *)
            echo "Invalid option $REPLY"
            ;;
    esac
done

# Select with server selection
declare -A servers=(
    ["web-01"]="10.0.1.10"
    ["web-02"]="10.0.1.11"
    ["db-01"]="10.0.2.10"
)

PS3="Select a server to ping: "  # Prompt
echo "Available servers:"
select server in "${!servers[@]}" "Quit"; do
    if [[ "$server" == "Quit" ]]; then
        echo "Exiting..."
        break
    elif [[ -n "$server" ]]; then
        echo "Pinging ${server} at ${servers[$server]}..."
        ping -c 2 "${servers[$server]}"
    else
        echo "Invalid selection"
    fi
done
Interactive select menus

Here Documents (Multi-line Text)

#!/bin/bash
# Here documents - multi-line text blocks

# Basic here document
cat << EOF
This is a multi-line
block of text
that preserves formatting
and line breaks.
EOF

# Here document with variables
name="John"
cat << EOF
Hello $name,
This is your report.
Today is $(date).
EOF

# Here document with command substitution
cat << EOF
Current directory: $(pwd)
User: $(whoami)
Files: $(ls | wc -l)
EOF

# Here document with tab suppression (<<-)
cat <<- EOF
	This line has a leading tab
	This line is indented
	But the output won't show the tabs
EOF

# Create file from here document
cat > config.yml << 'EOF'
# This is a YAML configuration file
database:
  host: localhost
  port: 5432
  user: admin
  password: ${PASSWORD}  # This won't expand (quoted EOF)
app:
  name: myapp
  version: 1.0
EOF

# Multi-line variable using here document
read -r -d '' help_text << 'HELP'
Usage: myscript [OPTIONS]

Options:
  -h, --help     Show this help
  -v, --verbose  Enable verbose output
  -f, --file     Specify input file

Examples:
  myscript -f data.txt
  myscript --verbose
HELP

echo "$help_text"
Here documents for multi-line text

Process Substitution

#!/bin/bash
# Process substitution - treat command output as a file

# Compare two directories
diff <(ls /etc) <(ls /usr/local/etc)

# Count lines in command output
wc -l <(grep -r "ERROR" /var/log/)

# Read from process substitution
while IFS= read -r line; do
    echo "Processing: $line"
done < <(ps aux | grep nginx)

# Combine multiple command outputs
paste <(ls -1 dir1/) <(ls -1 dir2/)

# Real-world example: Compare server configs
diff <(ssh user@server1 'cat /etc/nginx/nginx.conf') \
     <(ssh user@server2 'cat /etc/nginx/nginx.conf')

# Another useful pattern: Join files
join <(sort file1.txt) <(sort file2.txt)
Process substitution patterns

4. Complete Project: Server Management CLI

#!/bin/bash
# server_manager.sh - Advanced server management CLI

declare -A SERVERS=(
    ["web-01"]="10.0.1.10"
    ["web-02"]="10.0.1.11"
    ["web-03"]="10.0.1.12"
    ["db-01"]="10.0.2.10"
    ["db-02"]="10.0.2.11"
    ["cache-01"]="10.0.3.10"
)

declare -A SERVER_ROLES=(
    ["web-01"]="web"
    ["web-02"]="web"
    ["web-03"]="web"
    ["db-01"]="database"
    ["db-02"]="database"
    ["cache-01"]="cache"
)

# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# Function: Show help
show_help() {
    cat << 'HELP'
Server Management CLI

COMMANDS:
  list              List all servers
  ping <server>     Ping a server
  ssh <server>      SSH into a server
  status <server>   Check server status
  roles             Show servers by role
  health            Run health check on all servers
  exec <server> <cmd> Execute command on server
  help              Show this help
  exit              Exit program

EXAMPLES:
  server_manager.sh list
  server_manager.sh ping web-01
  server_manager.sh exec web-01 "df -h"
HELP
}

# Function: List all servers
list_servers() {
    echo -e "${BLUE}Available Servers:${NC}"
    echo "----------------------------------------"
    printf "%-15s %-15s %-10s\n" "SERVER" "IP ADDRESS" "ROLE"
    echo "----------------------------------------"
    for server in "${!SERVERS[@]}"; do
        printf "%-15s %-15s %-10s\n" "$server" "${SERVERS[$server]}" "${SERVER_ROLES[$server]}"
    done
}

# Function: Ping server
ping_server() {
    local server="$1"
    local ip="${SERVERS[$server]}"
    
    if [[ -z "$ip" ]]; then
        echo -e "${RED}Error: Server '$server' not found${NC}"
        return 1
    fi
    
    echo -e "${YELLOW}Pinging $server ($ip)...${NC}"
    ping -c 3 "$ip"
}

# Function: SSH to server
ssh_server() {
    local server="$1"
    local ip="${SERVERS[$server]}"
    
    if [[ -z "$ip" ]]; then
        echo -e "${RED}Error: Server '$server' not found${NC}"
        return 1
    fi
    
    echo -e "${GREEN}Connecting to $server ($ip)...${NC}"
    ssh "$USER@$ip"
}

# Function: Check server status
check_status() {
    local server="$1"
    local ip="${SERVERS[$server]}"
    
    if [[ -z "$ip" ]]; then
        echo -e "${RED}Error: Server '$server' not found${NC}"
        return 1
    fi
    
    if ping -c 1 -W 2 "$ip" > /dev/null 2>&1; then
        echo -e "${GREEN}✓ $server is ONLINE${NC}"
        return 0
    else
        echo -e "${RED}✗ $server is OFFLINE${NC}"
        return 1
    fi
}

# Function: Show servers by role
show_by_role() {
    local role
    
    echo -e "${BLUE}Select a role:${NC}"
    select role in "web" "database" "cache" "all"; do
        case $role in
            web|database|cache)
                echo -e "\n${YELLOW}Servers with role '$role':${NC}"
                for server in "${!SERVERS[@]}"; do
                    if [[ "${SERVER_ROLES[$server]}" == "$role" ]]; then
                        echo "  - $server (${SERVERS[$server]})"
                    fi
                done
                break
                ;;
            all)
                list_servers
                break
                ;;
            *)
                echo -e "${RED}Invalid option${NC}"
                ;;
        esac
    done
}

# Function: Run health check on all servers
health_check() {
    echo -e "${BLUE}Running health check on all servers...${NC}\n"
    
    local online=0
    local offline=0
    
    for server in "${!SERVERS[@]}"; do
        if check_status "$server" > /dev/null 2>&1; then
            echo -e "${GREEN}✓ $server${NC}"
            ((online++))
        else
            echo -e "${RED}✗ $server${NC}"
            ((offline++))
        fi
    done
    
    echo -e "\n${BLUE}Summary:${NC} $online online, $offline offline"
}

# Function: Execute remote command
exec_command() {
    local server="$1"
    local cmd="$2"
    local ip="${SERVERS[$server]}"
    
    if [[ -z "$ip" ]]; then
        echo -e "${RED}Error: Server '$server' not found${NC}"
        return 1
    fi
    
    if [[ -z "$cmd" ]]; then
        echo -e "${RED}Error: No command specified${NC}"
        return 1
    fi
    
    echo -e "${YELLOW}Executing on $server ($ip): $cmd${NC}"
    ssh "$USER@$ip" "$cmd"
}

# Main menu
main() {
    # If arguments provided, run in command-line mode
    if [[ $# -gt 0 ]]; then
        case "$1" in
            list) list_servers ;;
            ping) ping_server "$2" ;;
            ssh) ssh_server "$2" ;;
            status) check_status "$2" ;;
            roles) show_by_role ;;
            health) health_check ;;
            exec) exec_command "$2" "$3" ;;
            help) show_help ;;
            *) echo -e "${RED}Unknown command: $1${NC}"; show_help ;;
        esac
        exit 0
    fi
    
    # Interactive mode
    echo -e "${GREEN}=== Server Management CLI ===${NC}"
    echo -e "Type 'help' for commands, 'exit' to quit\n"
    
    while true; do
        read -p "server-manager> " cmd args
        
        case "$cmd" in
            list) list_servers ;;
            ping) ping_server "$args" ;;
            ssh) ssh_server "$args" ;;
            status) check_status "$args" ;;
            roles) show_by_role ;;
            health) health_check ;;
            exec) 
                read -r server command <<< "$args"
                exec_command "$server" "$command"
                ;;
            help) show_help ;;
            exit|quit) echo "Goodbye!"; break ;;
            "") continue ;;
            *) echo -e "${RED}Unknown command: $cmd${NC}";;
        esac
        echo ""
    done
}

# Run main function with all arguments
main "$@"
Complete server management CLI with arrays and select menus

5. Common Errors & Solutions

6. Summary Checklist

7. Next Steps

Next lesson: Bash Lesson 2.4: Error Handling & Debugging

You'll learn to:

  • Use set -e, set -x, set -u
  • Implement trap for cleanup
  • Debug with shellcheck
  • Add proper error handling

Alex Rivera

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen January 21, 2025

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment