Functions & Arguments: Modular Bash Scripting

Transform your Bash scripts from linear commands to modular, reusable, and maintainable automation tools. Master functions and argument handling to create professional-grade DevOps scripts.

F
Function Basics

Define, call, and organize functions

Syntax, scope, return values

Local vs global variables

function name() { commands; }
A
Arguments & Parameters

Pass data to functions

Positional, named, default args

shift, getopts, arrays

function foo() { echo $1 $2; }
M
Modular Patterns

Real DevOps patterns

Error handling, logging

Library functions, sourcing

source lib.sh; main() { }

Why Functions Matter for DevOps

Functions turn your scripts from spaghetti code into well-organized, reusable components. Benefits:

Reusability: Write once, use everywhere
Maintainability: Fix bugs in one place
Readability: Self-documenting code
Testability: Test functions independently
Modularity: Build complex systems from simple parts
Collaboration: Share function libraries across teams

Part 1: Function Fundamentals

Basic Function Syntax

function greet() {
local name="$1"
echo "Hello, $name!"
return 0
}
# Call the function
greet "Alice"

Different Ways to Define Functions

#!/bin/bash

# Method 1: function keyword (most readable)
function backup_files() {
    echo "Backing up files..."
    # Implementation
}

# Method 2: Parentheses (POSIX style)
restart_service() {
    echo "Restarting service..."
    # Implementation
}

# Method 3: Mixed style
function check_disk_space {
    echo "Checking disk space..."
    # Implementation
}

# Method 4: One-liner simple functions
log_info() { echo "[INFO] $1"; }
log_error() { echo "[ERROR] $1" >&2; }
log_debug() { [[ $DEBUG ]] && echo "[DEBUG] $1"; }

# Method 5: Function that calls another function
full_backup() {
    log_info "Starting full backup"
    backup_files
    log_info "Backup complete"
}

# Call functions
backup_files
restart_service
check_disk_space
log_info "Process started"
log_error "Something went wrong"
full_backup

Function Scope and Variables

#!/bin/bash

# Global variable
GLOBAL_VAR="I am global"

function demonstrate_scope() {
    # Local variable (only exists in function)
    local LOCAL_VAR="I am local"
    
    # This modifies global variable (BAD PRACTICE)
    GLOBAL_VAR="Modified inside function"
    
    # Create new global (also BAD PRACTICE)
    NEW_GLOBAL="Created inside function"
    
    echo "Inside function:"
    echo "  LOCAL_VAR: $LOCAL_VAR"
    echo "  GLOBAL_VAR: $GLOBAL_VAR"
    echo "  NEW_GLOBAL: $NEW_GLOBAL"
}

# Call function
demonstrate_scope

echo ""
echo "Outside function:"
echo "  LOCAL_VAR: ${LOCAL_VAR:-undefined}"
echo "  GLOBAL_VAR: $GLOBAL_VAR"
echo "  NEW_GLOBAL: ${NEW_GLOBAL:-undefined}"

# Better practice: Pass values as arguments, return via echo
function calculate_sum() {
    local a=$1
    local b=$2
    local sum=$((a + b))
    echo "$sum"  # "Return" value
}

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

# Even better: Use reference variables (bash 4.3+)
function calculate_stats() {
    local input_array=("$@")
    local -n sum_ref=$1  # Reference to variable outside
    local -n avg_ref=$2  # Another reference
    
    sum_ref=0
    for num in "${input_array[@]}"; do
        sum_ref=$((sum_ref + num))
    done
    avg_ref=$((sum_ref / ${#input_array[@]}))
}

NUMBERS=(10 20 30 40 50)
TOTAL=0
AVERAGE=0
calculate_stats NUMBERS TOTAL AVERAGE
echo "Total: $TOTAL, Average: $AVERAGE"
Scope Warning: Always use local for function variables to avoid polluting the global namespace and causing hard-to-debug issues.

Part 2: Function Arguments & Parameters

$0
Function Name
$1
First Argument
$2
Second Argument
$@
All Arguments
$#
Argument Count

Positional Arguments

#!/bin/bash

# Basic positional arguments
function greet_user() {
    local name=$1
    local time_of_day=${2:-"Morning"}  # Default value
    
    echo "Good $time_of_day, $name!"
}

greet_user "Alice"
greet_user "Bob" "Evening"
greet_user "Charlie" "Afternoon"

# Multiple arguments
function create_user() {
    local username=$1
    local full_name=$2
    local shell=${3:-"/bin/bash"}
    local home_dir=${4:-"/home/$username"}
    
    echo "Creating user: $username"
    echo "  Full name: $full_name"
    echo "  Shell: $shell"
    echo "  Home: $home_dir"
    # sudo useradd -m -s "$shell" -c "$full_name" "$username"
}

create_user "alice" "Alice Johnson"
create_user "bob" "Bob Smith" "/bin/zsh" "/opt/bob"

# Process all arguments
function process_files() {
    echo "Processing $# files:"
    
    local count=1
    for file in "$@"; do
        echo "  $count. $file"
        # Process each file
        ((count++))
    done
}

process_files file1.txt file2.txt file3.txt

# Shift through arguments
function parse_options() {
    while [[ $# -gt 0 ]]; do
        case $1 in
            -v|--verbose)
                VERBOSE=true
                shift
                ;;
            -f|--file)
                FILENAME=$2
                shift 2
                ;;
            *)
                echo "Unknown option: $1"
                exit 1
                ;;
        esac
    done
    
    echo "Verbose: ${VERBOSE:-false}"
    echo "Filename: ${FILENAME:-not set}"
}

parse_options --verbose --file data.txt

Named Arguments with getopts

#!/bin/bash

# Professional argument parsing
function parse_arguments() {
    local OPTIND
    local options
    
    # Default values
    local verbose=false
    local output_file=""
    local count=1
    
    while getopts ":vf:o:c:h" opt; do
        case $opt in
            v)
                verbose=true
                ;;
            f)
                input_file="$OPTARG"
                ;;
            o)
                output_file="$OPTARG"
                ;;
            c)
                count="$OPTARG"
                if ! [[ $count =~ ^[0-9]+$ ]]; then
                    echo "ERROR: Count must be a number" >&2
                    return 1
                fi
                ;;
            h)
                show_help
                return 0
                ;;
            \?)
                echo "Invalid option: -$OPTARG" >&2
                return 1
                ;;
            :)
                echo "Option -$OPTARG requires an argument" >&2
                return 1
                ;;
        esac
    done
    
    shift $((OPTIND - 1))
    
    # Remaining arguments (non-options)
    local remaining_args=("$@")
    
    # Return values via global (for this example)
    PARSE_RESULT_VERBOSE=$verbose
    PARSE_RESULT_OUTPUT=$output_file
    PARSE_RESULT_COUNT=$count
    PARSE_RESULT_INPUT=${input_file:-""}
    PARSE_RESULT_REMAINING=("${remaining_args[@]}")
}

function show_help() {
    cat << EOF
Usage: $0 [OPTIONS] [FILES...]

Options:
  -v            Verbose output
  -f FILE       Input file
  -o FILE       Output file
  -c COUNT      Number of iterations
  -h            Show this help message

Examples:
  $0 -v -f input.txt -o output.txt
  $0 -c 5 file1 file2
EOF
}

# Test the function
parse_arguments -v -f input.txt -o output.txt -c 3 file1 file2 file3

echo "Verbose: $PARSE_RESULT_VERBOSE"
echo "Input file: $PARSE_RESULT_INPUT"
echo "Output file: $PARSE_RESULT_OUTPUT"
echo "Count: $PARSE_RESULT_COUNT"
echo "Remaining: ${PARSE_RESULT_REMAINING[*]}"

Advanced Argument Patterns

#!/bin/bash

# Pattern 1: Required vs optional arguments
function deploy_app() {
    # Required arguments
    if [[ $# -lt 2 ]]; then
        echo "Usage: deploy_app   [options...]"
        return 1
    fi
    
    local environment=$1
    local version=$2
    shift 2  # Remove first two arguments
    
    # Optional arguments
    local rollback=false
    local dry_run=false
    local force=false
    
    while [[ $# -gt 0 ]]; do
        case $1 in
            --rollback) rollback=true ;;
            --dry-run) dry_run=true ;;
            --force) force=true ;;
            *) echo "Unknown option: $1"; return 1 ;;
        esac
        shift
    done
    
    echo "Deploying version $version to $environment"
    echo "  Rollback: $rollback"
    echo "  Dry run: $dry_run"
    echo "  Force: $force"
}

deploy_app staging v1.2.3
deploy_app production v1.2.4 --dry-run --force

# Pattern 2: Array arguments
function process_items() {
    local items=("$@")  # All arguments as array
    
    echo "Processing ${#items[@]} items:"
    for item in "${items[@]}"; do
        echo "  - $item"
    done
}

process_items "apple" "banana" "cherry"
process_items "server1" "server2" "server3" "server4"

# Pattern 3: Key-value arguments
function configure_service() {
    declare -A config
    
    while [[ $# -gt 0 ]]; do
        if [[ $1 == *=* ]]; then
            key="${1%%=*}"
            value="${1#*=}"
            config["$key"]="$value"
        else
            config["$1"]=true
        fi
        shift
    done
    
    echo "Configuration:"
    for key in "${!config[@]}"; do
        echo "  $key = ${config[$key]}"
    done
}

configure_service port=8080 host=localhost ssl=true debug

# Pattern 4: Subcommand pattern (like git)
function app_cli() {
    local command=$1
    shift
    
    case $command in
        start)
            start_server "$@"
            ;;
        stop)
            stop_server "$@"
            ;;
        restart)
            restart_server "$@"
            ;;
        status)
            check_status "$@"
            ;;
        *)
            echo "Unknown command: $command"
            echo "Available: start, stop, restart, status"
            return 1
            ;;
    esac
}

function start_server() {
    local port=${1:-8080}
    echo "Starting server on port $port"
}

function stop_server() {
    echo "Stopping server"
}

# Test subcommands
app_cli start 9090
app_cli status

Part 3: Return Values and Exit Codes

Understanding Return Values

Method Description Use Case Example return N Exit code (0-255) Success/failure status return 0 (success) echo "value" Output capture Return data/strings result=$(func) declare -n Reference variables Multiple returns (bash 4.3+) func $result_ref global variable Set global variable Avoid (unless necessary) RESULT="value"
#!/bin/bash

# Method 1: Exit codes (0 = success, non-zero = error)
function check_service() {
    local service_name=$1
    
    if systemctl is-active --quiet "$service_name"; then
        return 0  # Service is running
    else
        return 1  # Service is not running
    fi
}

check_service nginx
if [ $? -eq 0 ]; then
    echo "Nginx is running"
else
    echo "Nginx is not running"
fi

# Method 2: Capture output
function get_system_info() {
    local hostname=$(hostname)
    local uptime=$(uptime -p)
    local memory=$(free -h | awk '/^Mem:/ {print $2}')
    
    # Return multiple values as JSON/CSV
    echo "hostname:$hostname,uptime:$uptime,memory:$memory"
}

INFO=$(get_system_info)
echo "System info: $INFO"

# Parse the returned data
IFS=',' read -ra PARTS <<< "$INFO"
for part in "${PARTS[@]}"; do
    IFS=':' read -r key value <<< "$part"
    echo "$key: $value"
done

# Method 3: Multiple return values via arrays
function get_stats() {
    local numbers=("$@")
    local sum=0
    local min=${numbers[0]}
    local max=${numbers[0]}
    
    for num in "${numbers[@]}"; do
        sum=$((sum + num))
        if [ $num -lt $min ]; then min=$num; fi
        if [ $num -gt $max ]; then max=$num; fi
    done
    
    local avg=$((sum / ${#numbers[@]}))
    
    # Return as array
    echo "$sum $avg $min $max"
}

DATA=(10 20 30 40 50)
STATS=($(get_stats "${DATA[@]}"))
echo "Sum: ${STATS[0]}, Avg: ${STATS[1]}, Min: ${STATS[2]}, Max: ${STATS[3]}"

# Method 4: Using reference variables (bash 4.3+)
function calculate_metrics() {
    local numbers=("$@")
    local -n sum_ref=$1  # Reference to variable
    local -n avg_ref=$2  # Another reference
    
    sum_ref=0
    for num in "${numbers[@]:2}"; do  # Skip first two (the references)
        sum_ref=$((sum_ref + num))
    done
    avg_ref=$((sum_ref / (${#numbers[@]} - 2)))
}

DATA=(10 20 30 40 50)
TOTAL=0
AVERAGE=0
calculate_metrics TOTAL AVERAGE "${DATA[@]}"
echo "Total: $TOTAL, Average: $AVERAGE"

# Method 5: Complex return as associative array
function get_server_stats() {
    declare -A stats
    
    stats["cpu"]=$(grep 'cpu ' /proc/stat | awk '{usage=($2+$4)*100/($2+$4+$5)} END {print usage "%"}')
    stats["memory"]=$(free -m | awk '/^Mem:/ {print $3 "/" $2 "MB"}')
    stats["disk"]=$(df -h / | awk 'NR==2 {print $3 "/" $2}')
    stats["uptime"]=$(uptime -p)
    
    # Return by echoing array declaration
    declare -p stats
}

# Evaluate the returned array declaration
eval "$(get_server_stats)"
echo "CPU: ${stats[cpu]}"
echo "Memory: ${stats[memory]}"
echo "Disk: ${stats[disk]}"
echo "Uptime: ${stats[uptime]}"

Part 4: Practical DevOps Function Libraries

Common Utility Functions

#!/bin/bash
# lib-utils.sh - Common DevOps utility functions

set -euo pipefail

# Color output functions
function log_info() {
    echo -e "\033[0;32m[INFO]\033[0m $1"
}

function log_warn() {
    echo -e "\033[1;33m[WARN]\033[0m $1" >&2
}

function log_error() {
    echo -e "\033[0;31m[ERROR]\033[0m $1" >&2
}

function log_debug() {
    if [[ ${DEBUG:-false} == true ]]; then
        echo -e "\033[0;36m[DEBUG]\033[0m $1"
    fi
}

# Validation functions
function validate_number() {
    local value=$1
    local min=${2:-""}
    local max=${3:-""}
    
    if ! [[ $value =~ ^[0-9]+$ ]]; then
        log_error "Value must be a number: $value"
        return 1
    fi
    
    if [[ -n $min ]] && [ $value -lt $min ]; then
        log_error "Value must be >= $min: $value"
        return 1
    fi
    
    if [[ -n $max ]] && [ $value -gt $max ]; then
        log_error "Value must be <= $max: $value"
        return 1
    fi
}

function validate_file() {
    local file=$1
    local check_readable=${2:-false}
    local check_writable=${3:-false}
    
    if [[ ! -e $file ]]; then
        log_error "File does not exist: $file"
        return 1
    fi
    
    if [[ ! -f $file ]]; then
        log_error "Not a regular file: $file"
        return 1
    fi
    
    if [[ $check_readable == true ]] && [[ ! -r $file ]]; then
        log_error "File not readable: $file"
        return 1
    fi
    
    if [[ $check_writable == true ]] && [[ ! -w $file ]]; then
        log_error "File not writable: $file"
        return 1
    fi
}

function validate_directory() {
    local dir=$1
    local check_writable=${2:-false}
    
    if [[ ! -d $dir ]]; then
        log_error "Not a directory: $dir"
        return 1
    fi
    
    if [[ $check_writable == true ]] && [[ ! -w $dir ]]; then
        log_error "Directory not writable: $dir"
        return 1
    fi
}

# Network functions
function check_port() {
    local host=${1:-"localhost"}
    local port=$2
    local timeout=${3:-5}
    
    if timeout $timeout bash -c "cat < /dev/null > /dev/tcp/$host/$port" 2>/dev/null; then
        log_debug "Port $port on $host is open"
        return 0
    else
        log_debug "Port $port on $host is closed"
        return 1
    fi
}

function wait_for_service() {
    local host=$1
    local port=$2
    local timeout=${3:-60}
    local interval=${4:-2}
    
    local start_time=$(date +%s)
    local end_time=$((start_time + timeout))
    
    log_info "Waiting for $host:$port (timeout: ${timeout}s)"
    
    while [[ $(date +%s) -lt $end_time ]]; do
        if check_port "$host" "$port" 1; then
            log_info "Service $host:$port is ready"
            return 0
        fi
        sleep $interval
        echo -n "."
    done
    
    log_error "Timeout waiting for $host:$port"
    return 1
}

# System functions
function get_system_load() {
    local load1 load5 load15
    read -r load1 load5 load15 < /proc/loadavg
    echo "1min:$load1,5min:$load5,15min:$load15"
}

function get_memory_usage() {
    local total used free
    read -r total used free <<< $(free -m | awk '/^Mem:/ {print $2, $3, $4}')
    local percent=$((used * 100 / total))
    echo "$percent% (${used}MB/${total}MB)"
}

function get_disk_usage() {
    local mount=$1
    local usage=$(df -h "$mount" | awk 'NR==2 {print $5}')
    local available=$(df -h "$mount" | awk 'NR==2 {print $4}')
    echo "$usage used, $available available"
}

# String manipulation functions
function string_contains() {
    local string=$1
    local substring=$2
    
    if [[ $string == *"$substring"* ]]; then
        return 0
    else
        return 1
    fi
}

function string_starts_with() {
    local string=$1
    local prefix=$2
    
    if [[ $string == "$prefix"* ]]; then
        return 0
    else
        return 1
    fi
}

function string_ends_with() {
    local string=$1
    local suffix=$2
    
    if [[ $string == *"$suffix" ]]; then
        return 0
    else
        return 1
    fi
}

function string_to_lower() {
    echo "$1" | tr '[:upper:]' '[:lower:]'
}

function string_to_upper() {
    echo "$1" | tr '[:lower:]' '[:upper:]'
}

# Array functions
function array_contains() {
    local array_name=$1
    local value=$2
    local -n arr=$array_name
    
    for element in "${arr[@]}"; do
        if [[ $element == "$value" ]]; then
            return 0
        fi
    done
    return 1
}

function array_join() {
    local delimiter=$1
    shift
    local array=("$@")
    local result=""
    
    for element in "${array[@]}"; do
        if [[ -z $result ]]; then
            result="$element"
        else
            result="$result$delimiter$element"
        fi
    done
    echo "$result"
}

# Export functions for use in other scripts
export -f log_info log_warn log_error log_debug
export -f validate_number validate_file validate_directory
export -f check_port wait_for_service
export -f get_system_load get_memory_usage get_disk_usage

Deployment Functions Library

#!/bin/bash
# lib-deploy.sh - Deployment functions for DevOps

set -euo pipefail

# Source utilities if needed
if [[ -f lib-utils.sh ]]; then
    source lib-utils.sh
fi

function deploy_preflight_check() {
    local environment=$1
    local version=$2
    
    log_info "Running preflight checks for $version deployment to $environment"
    
    # Check disk space
    local disk_usage=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
    if [ $disk_usage -gt 90 ]; then
        log_error "Disk usage too high: ${disk_usage}%"
        return 1
    fi
    
    # Check memory
    local mem_free=$(free -m | awk '/^Mem:/ {print $7}')
    if [ $mem_free -lt 512 ]; then
        log_warn "Low memory: ${mem_free}MB free"
    fi
    
    # Check required services
    local required_services=("docker" "nginx" "postgresql")
    for service in "${required_services[@]}"; do
        if ! systemctl is-active --quiet "$service"; then
            log_error "Required service not running: $service"
            return 1
        fi
    done
    
    # Validate version format
    if ! [[ $version =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
        log_error "Invalid version format: $version (expected vX.Y.Z)"
        return 1
    fi
    
    log_info "Preflight checks passed"
    return 0
}

function deploy_backup() {
    local backup_dir=$1
    local app_dir=$2
    
    log_info "Creating backup"
    
    validate_directory "$backup_dir" true || return 1
    validate_directory "$app_dir" true || return 1
    
    local timestamp=$(date +%Y%m%d_%H%M%S)
    local backup_file="$backup_dir/backup_$timestamp.tar.gz"
    
    if tar -czf "$backup_file" -C "$app_dir" . 2>/dev/null; then
        local size=$(du -h "$backup_file" | cut -f1)
        log_info "Backup created: $backup_file ($size)"
        
        # Clean old backups (keep last 7)
        find "$backup_dir" -name "backup_*.tar.gz" -mtime +7 -delete
    else
        log_error "Backup failed"
        return 1
    fi
}

function deploy_pull_code() {
    local repo_url=$1
    local branch=$2
    local target_dir=$3
    
    log_info "Pulling code from $repo_url ($branch)"
    
    if [[ -d "$target_dir/.git" ]]; then
        cd "$target_dir"
        if ! git pull origin "$branch"; then
            log_error "Git pull failed"
            return 1
        fi
    else
        if ! git clone -b "$branch" "$repo_url" "$target_dir"; then
            log_error "Git clone failed"
            return 1
        fi
    fi
    
    log_info "Code updated successfully"
}

function deploy_install_deps() {
    local app_dir=$1
    
    log_info "Installing dependencies"
    cd "$app_dir"
    
    # Node.js projects
    if [[ -f "package.json" ]]; then
        log_info "Installing npm dependencies"
        if ! npm ci --production; then
            log_error "npm install failed"
            return 1
        fi
    fi
    
    # Python projects
    if [[ -f "requirements.txt" ]]; then
        log_info "Installing Python dependencies"
        if ! pip install -r requirements.txt; then
            log_error "pip install failed"
            return 1
        fi
    fi
    
    # Ruby projects
    if [[ -f "Gemfile" ]]; then
        log_info "Installing Ruby dependencies"
        if ! bundle install --deployment; then
            log_error "bundle install failed"
            return 1
        fi
    fi
    
    log_info "Dependencies installed"
}

function deploy_run_migrations() {
    local app_dir=$1
    local environment=$2
    
    log_info "Running database migrations"
    cd "$app_dir"
    
    # Django migrations
    if [[ -f "manage.py" ]]; then
        export DJANGO_SETTINGS_MODULE="config.settings.$environment"
        if ! python manage.py migrate; then
            log_error "Django migrations failed"
            return 1
        fi
    fi
    
    # Rails migrations
    if [[ -f "db/migrate" ]]; then
        export RAILS_ENV="$environment"
        if ! bundle exec rails db:migrate; then
            log_error "Rails migrations failed"
            return 1
        fi
    fi
    
    log_info "Migrations completed"
}

function deploy_restart_services() {
    local services=("$@")
    
    log_info "Restarting services"
    
    for service in "${services[@]}"; do
        log_info "Restarting $service"
        if ! systemctl restart "$service"; then
            log_error "Failed to restart $service"
            return 1
        fi
        
        # Wait for service to start
        local attempts=0
        local max_attempts=10
        
        while [ $attempts -lt $max_attempts ]; do
            if systemctl is-active --quiet "$service"; then
                log_info "$service is running"
                break
            fi
            ((attempts++))
            sleep 2
        done
        
        if [ $attempts -eq $max_attempts ]; then
            log_error "$service failed to start"
            return 1
        fi
    done
    
    log_info "Services restarted successfully"
}

function deploy_health_check() {
    local url=$1
    local timeout=${2:-30}
    
    log_info "Running health check: $url"
    
    local start_time=$(date +%s)
    local end_time=$((start_time + timeout))
    
    while [[ $(date +%s) -lt $end_time ]]; do
        if curl -s -f "$url" > /dev/null; then
            log_info "Health check passed"
            return 0
        fi
        sleep 2
        echo -n "."
    done
    
    log_error "Health check failed: $url not responding"
    return 1
}

function deploy_rollback() {
    local backup_dir=$1
    local app_dir=$2
    
    log_warn "Initiating rollback"
    
    # Find latest backup
    local latest_backup=$(find "$backup_dir" -name "backup_*.tar.gz" -type f | sort -r | head -1)
    
    if [[ -z $latest_backup ]]; then
        log_error "No backup found for rollback"
        return 1
    fi
    
    log_info "Rolling back to: $(basename "$latest_backup")"
    
    # Stop services
    systemctl stop app.service 2>/dev/null || true
    
    # Restore backup
    if tar -xzf "$latest_backup" -C "$app_dir" --strip-components=0 2>/dev/null; then
        log_info "Backup restored"
        
        # Restart services
        deploy_restart_services "app.service"
        
        # Health check
        deploy_health_check "http://localhost:8080/health"
        
        log_info "Rollback completed successfully"
        return 0
    else
        log_error "Rollback failed"
        return 1
    fi
}

# Main deployment function
function deploy_application() {
    local environment=$1
    local version=$2
    shift 2
    
    # Parse options
    local rollback=false
    local dry_run=false
    local force=false
    
    while [[ $# -gt 0 ]]; do
        case $1 in
            --rollback) rollback=true ;;
            --dry-run) dry_run=true ;;
            --force) force=true ;;
            *) log_error "Unknown option: $1"; return 1 ;;
        esac
        shift
    done
    
    log_info "Starting deployment: $version to $environment"
    log_info "Options: rollback=$rollback, dry-run=$dry_run, force=$force"
    
    if [[ $dry_run == true ]]; then
        log_info "Dry run completed successfully"
        return 0
    fi
    
    if [[ $rollback == true ]]; then
        deploy_rollback "/backups" "/var/www/app"
        return $?
    fi
    
    # Normal deployment flow
    deploy_preflight_check "$environment" "$version" || return 1
    deploy_backup "/backups" "/var/www/app" || return 1
    deploy_pull_code "git@github.com:company/app.git" "main" "/var/www/app" || return 1
    deploy_install_deps "/var/www/app" || return 1
    deploy_run_migrations "/var/www/app" "$environment" || return 1
    deploy_restart_services "app.service" "nginx.service" || return 1
    deploy_health_check "http://localhost:8080/health" || return 1
    
    log_info "Deployment completed successfully: $version to $environment"
    return 0
}

Part 5: Sourcing and Modular Scripts

Creating Function Libraries

#!/bin/bash
# main.sh - Main script using function libraries

set -euo pipefail

# Source function libraries
LIB_DIR="$(dirname "$0")/lib"

# Load all library files
for lib_file in "$LIB_DIR"/*.sh; do
    if [[ -f $lib_file ]]; then
        source "$lib_file"
        log_debug "Loaded library: $(basename "$lib_file")"
    fi
done

# Or load specific libraries
source "$LIB_DIR/lib-utils.sh"
source "$LIB_DIR/lib-deploy.sh"
source "$LIB_DIR/lib-monitoring.sh"

# Configuration
CONFIG_FILE="${CONFIG_FILE:-config.sh}"
if [[ -f $CONFIG_FILE ]]; then
    source "$CONFIG_FILE"
fi

# Main function
main() {
    local command=${1:-"help"}
    
    case $command in
        deploy)
            local environment=${2:-"staging"}
            local version=${3:-"latest"}
            shift 3
            deploy_application "$environment" "$version" "$@"
            ;;
        backup)
            local target=${2:-"/var/www/app"}
            local destination=${3:-"/backups"}
            deploy_backup "$destination" "$target"
            ;;
        monitor)
            check_port "localhost" 8080
            get_system_load
            get_memory_usage
            ;;
        health)
            deploy_health_check "http://localhost:8080/health"
            ;;
        help|*)
            echo "Usage: $0 {deploy|backup|monitor|health|help}"
            echo ""
            echo "Commands:"
            echo "  deploy    Deploy application"
            echo "  backup [target] [dest]  Create backup"
            echo "  monitor                 Check system health"
            echo "  health                  Check application health"
            echo "  help                    Show this help"
            ;;
    esac
}

# Only run main if script is executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi

Configuration File Example

#!/bin/bash
# config.sh - Configuration file

# Application settings
APP_NAME="myapp"
APP_VERSION="v1.2.3"
APP_DIR="/var/www/app"

# Database settings
DB_HOST="localhost"
DB_PORT="5432"
DB_NAME="app_db"
DB_USER="app_user"

# Deployment settings
DEPLOY_ENVIRONMENT="production"
BACKUP_DIR="/backups"
LOG_DIR="/var/log/app"

# Service settings
SERVICES=("app.service" "nginx.service" "postgresql.service")

# Network settings
API_PORT="8080"
HEALTH_CHECK_URL="http://localhost:8080/health"

# Monitoring thresholds
CPU_THRESHOLD=80
MEMORY_THRESHOLD=90
DISK_THRESHOLD=85

# Logging
LOG_LEVEL="INFO"
DEBUG=false

# Export variables for use in functions
export APP_NAME APP_VERSION APP_DIR
export DB_HOST DB_PORT DB_NAME DB_USER
export DEPLOY_ENVIRONMENT BACKUP_DIR LOG_DIR
export SERVICES API_PORT HEALTH_CHECK_URL
export CPU_THRESHOLD MEMORY_THRESHOLD DISK_THRESHOLD
export LOG_LEVEL DEBUG

Best Practices Checklist

Use local for all function variables
Validate function arguments at the beginning
Return meaningful exit codes (0 for success)
Use descriptive function names (verbs for actions)
Keep functions small and focused (single responsibility)
Document functions with comments
Source library files at the top of scripts
Use set -euo pipefail for error handling
Test functions independently
Handle errors gracefully within functions
Use default parameter values when appropriate
Avoid side effects (modifying globals)
Use declare -f to check if function exists
Organize related functions into library files
Common Function Mistakes:
1. Not using local for function variables
2. Modifying global variables unintentionally
3. Not validating input arguments
4. Returning strings via return instead of echo
5. Using exit instead of return in functions
6. Creating functions with side effects
7. Not handling errors within functions
8. Functions that are too long/complex
Advanced Function Techniques:
• Use trap for cleanup in functions
• Create decorator/wrapper functions
• Implement function composition
• Use eval for dynamic function calls (carefully!)
• Create self-documenting functions with declare -f
• Implement memoization for expensive function calls
• Use typeset -f to list all defined functions

Function-Driven DevOps

Functions transform Bash from a simple scripting language into a powerful automation tool. By organizing your code into reusable functions, you create a library of DevOps utilities that can be shared across teams and projects.

Remember: Start by identifying repeated patterns in your scripts. Extract them into functions. Build libraries. Share them. Soon you'll have a powerful toolbox that makes complex automations simple.

Practice Exercise: Take an existing script you use and refactor it into functions. Create separate library files for utilities, deployment functions, and monitoring functions. Make the main script a simple orchestrator that calls these functions.

Quick Reference:
Define: function name() { commands; }
Call: name arg1 arg2
Arguments: $1, $2, $@, $#
Local vars: local var="value"
Return: return 0 (exit code)
Output: echo "value" (capture with $())
Source: source lib.sh
Check: declare -f function_name