Files
AzerothCore-RealmMaster/scripts/bash/lib/env-utils.sh
2025-12-02 21:43:05 -05:00

613 lines
16 KiB
Bash
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
#
# Environment and file utility library for AzerothCore RealmMaster scripts
# This library provides enhanced environment variable handling, file operations,
# and path management functions.
#
# Usage: source /path/to/scripts/bash/lib/env-utils.sh
#
# Prevent multiple sourcing
if [ -n "${_ENV_UTILS_LIB_LOADED:-}" ]; then
return 0
fi
_ENV_UTILS_LIB_LOADED=1
# Source common library for logging functions
ENV_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "$ENV_UTILS_DIR/common.sh" ]; then
source "$ENV_UTILS_DIR/common.sh"
elif command -v info >/dev/null 2>&1; then
# Common functions already available
:
else
# Fallback logging functions
info() { printf '\033[0;34m %s\033[0m\n' "$*"; }
warn() { printf '\033[1;33m⚠ %s\033[0m\n' "$*" >&2; }
err() { printf '\033[0;31m❌ %s\033[0m\n' "$*" >&2; }
fatal() { err "$*"; exit 1; }
fi
# =============================================================================
# ENVIRONMENT VARIABLE MANAGEMENT
# =============================================================================
# Enhanced read_env function with advanced features
# Supports multiple .env files, environment variable precedence, and validation
#
# Usage:
# value=$(env_read_with_fallback "MYSQL_PASSWORD" "default_password")
# value=$(env_read_with_fallback "PORT" "" ".env.local" "validate_port")
#
env_read_with_fallback() {
local key="$1"
local default="${2:-}"
local env_file="${3:-${ENV_PATH:-${DEFAULT_ENV_PATH:-.env}}}"
local validator_func="${4:-}"
local value=""
# 1. Check if variable is already set in environment (highest precedence)
if [ -n "${!key:-}" ]; then
value="${!key}"
else
# 2. Read from .env file if it exists
if [ -f "$env_file" ]; then
# Extract value using grep and cut, handling various formats
value="$(grep -E "^${key}=" "$env_file" 2>/dev/null | tail -n1 | cut -d'=' -f2- | tr -d '\r')"
# Remove inline comments (everything after # that's not inside quotes)
value="$(echo "$value" | sed 's/[[:space:]]*#.*//' | sed 's/[[:space:]]*$//')"
# Strip quotes if present
if [[ "$value" == \"*\" && "$value" == *\" ]]; then
# Double quotes
value="${value:1:-1}"
elif [[ "$value" == \'*\' && "$value" == *\' ]]; then
# Single quotes
value="${value:1:-1}"
fi
fi
# 3. Use default if still empty
if [ -z "${value:-}" ]; then
value="$default"
fi
fi
# 4. Validate if validator function provided
if [ -n "$validator_func" ] && command -v "$validator_func" >/dev/null 2>&1; then
if ! "$validator_func" "$value"; then
err "Validation failed for $key: $value"
return 1
fi
fi
printf '%s\n' "${value}"
}
# Read environment variable with type conversion
# Supports string, int, bool, and path types
#
# Usage:
# port=$(env_read_typed "MYSQL_PORT" "int" "3306")
# debug=$(env_read_typed "DEBUG" "bool" "false")
# path=$(env_read_typed "DATA_PATH" "path" "/data")
#
env_read_typed() {
local key="$1"
local type="$2"
local default="${3:-}"
local value
value=$(env_read_with_fallback "$key" "$default")
case "$type" in
int|integer)
if ! [[ "$value" =~ ^[0-9]+$ ]]; then
err "Environment variable $key must be an integer: $value"
return 1
fi
echo "$value"
;;
bool|boolean)
case "${value,,}" in
true|yes|1|on|enabled) echo "true" ;;
false|no|0|off|disabled) echo "false" ;;
*) err "Environment variable $key must be boolean: $value"; return 1 ;;
esac
;;
path)
# Expand relative paths to absolute
if [ -n "$value" ]; then
path_resolve_absolute "$value"
fi
;;
string|*)
echo "$value"
;;
esac
}
# Update or add environment variable in .env file with backup
# Creates backup and maintains file integrity
#
# Usage:
# env_update_value "MYSQL_PASSWORD" "new_password"
# env_update_value "DEBUG" "true" ".env.local"
# env_update_value "PORT" "8080" ".env" "true" # create backup
#
env_update_value() {
local key="$1"
local value="$2"
local env_file="${3:-${ENV_PATH:-${DEFAULT_ENV_PATH:-.env}}}"
local create_backup="${4:-false}"
[ -n "$env_file" ] || return 0
# Create backup if requested
if [ "$create_backup" = "true" ] && [ -f "$env_file" ]; then
file_create_backup "$env_file"
fi
# Create file if it doesn't exist
if [ ! -f "$env_file" ]; then
file_ensure_writable_dir "$(dirname "$env_file")"
printf '%s=%s\n' "$key" "$value" >> "$env_file"
return 0
fi
# Update existing or append new
if grep -q "^${key}=" "$env_file"; then
# Use platform-appropriate sed in-place editing
local sed_opts=""
if [[ "$OSTYPE" == "darwin"* ]]; then
sed_opts="-i ''"
else
sed_opts="-i"
fi
# Use a temporary file for safer editing
local temp_file="${env_file}.tmp.$$"
sed "s|^${key}=.*|${key}=${value}|" "$env_file" > "$temp_file" && mv "$temp_file" "$env_file"
else
printf '\n%s=%s\n' "$key" "$value" >> "$env_file"
fi
info "Updated $key in $env_file"
}
# Load multiple environment files with precedence
# Later files override earlier ones
#
# Usage:
# env_load_multiple ".env" ".env.local" ".env.production"
#
env_load_multiple() {
local files=("$@")
local loaded_count=0
for env_file in "${files[@]}"; do
if [ -f "$env_file" ]; then
info "Loading environment from: $env_file"
set -a
# shellcheck disable=SC1090
source "$env_file"
set +a
loaded_count=$((loaded_count + 1))
fi
done
if [ $loaded_count -eq 0 ]; then
warn "No environment files found: ${files[*]}"
return 1
fi
info "Loaded $loaded_count environment file(s)"
return 0
}
# =============================================================================
# PATH AND FILE UTILITIES
# =============================================================================
# Resolve path to absolute form with proper error handling
# Handles both existing and non-existing paths
#
# Usage:
# abs_path=$(path_resolve_absolute "./relative/path")
# abs_path=$(path_resolve_absolute "/already/absolute")
#
path_resolve_absolute() {
local path="$1"
local base_dir="${2:-$PWD}"
if command -v python3 >/dev/null 2>&1; then
python3 - "$base_dir" "$path" <<'PY'
import os, sys
base, path = sys.argv[1:3]
if not path:
print(os.path.abspath(base))
elif os.path.isabs(path):
print(os.path.normpath(path))
else:
print(os.path.normpath(os.path.join(base, path)))
PY
elif command -v realpath >/dev/null 2>&1; then
if [ "${path:0:1}" = "/" ]; then
echo "$path"
else
realpath -m "$base_dir/$path"
fi
else
# Fallback manual resolution
if [ "${path:0:1}" = "/" ]; then
echo "$path"
else
echo "$base_dir/$path"
fi
fi
}
# Ensure directory exists and is writable with proper permissions
# Creates parent directories if needed
#
# Usage:
# file_ensure_writable_dir "/path/to/directory"
# file_ensure_writable_dir "/path/to/directory" "0755"
#
file_ensure_writable_dir() {
local dir="$1"
local permissions="${2:-0755}"
if [ ! -d "$dir" ]; then
if mkdir -p "$dir" 2>/dev/null; then
info "Created directory: $dir"
chmod "$permissions" "$dir" 2>/dev/null || warn "Could not set permissions on $dir"
else
err "Failed to create directory: $dir"
return 1
fi
fi
if [ ! -w "$dir" ]; then
if chmod u+w "$dir" 2>/dev/null; then
info "Made directory writable: $dir"
else
err "Directory not writable and cannot fix permissions: $dir"
return 1
fi
fi
return 0
}
# Create timestamped backup of file
# Supports custom backup directory and compression
#
# Usage:
# file_create_backup "/path/to/important.conf"
# file_create_backup "/path/to/file" "/backup/dir" "gzip"
#
file_create_backup() {
local file="$1"
local backup_dir="${2:-$(dirname "$file")}"
local compression="${3:-none}"
if [ ! -f "$file" ]; then
warn "File does not exist, skipping backup: $file"
return 0
fi
file_ensure_writable_dir "$backup_dir"
local filename basename backup_file
filename=$(basename "$file")
basename="${filename%.*}"
local extension="${filename##*.}"
# Create backup filename with timestamp
if [ "$filename" = "$basename" ]; then
# No extension
backup_file="${backup_dir}/${filename}.backup.$(date +%Y%m%d_%H%M%S)"
else
# Has extension
backup_file="${backup_dir}/${basename}.backup.$(date +%Y%m%d_%H%M%S).${extension}"
fi
case "$compression" in
gzip|gz)
if gzip -c "$file" > "${backup_file}.gz"; then
info "Created compressed backup: ${backup_file}.gz"
else
err "Failed to create compressed backup: ${backup_file}.gz"
return 1
fi
;;
none|*)
if cp "$file" "$backup_file"; then
info "Created backup: $backup_file"
else
err "Failed to create backup: $backup_file"
return 1
fi
;;
esac
return 0
}
# Set file permissions safely with validation
# Handles both numeric and symbolic modes
#
# Usage:
# file_set_permissions "/path/to/file" "0644"
# file_set_permissions "/path/to/script" "u+x"
#
file_set_permissions() {
local file="$1"
local permissions="$2"
local recursive="${3:-false}"
if [ ! -e "$file" ]; then
err "File or directory does not exist: $file"
return 1
fi
local chmod_opts=""
if [ "$recursive" = "true" ] && [ -d "$file" ]; then
chmod_opts="-R"
fi
if chmod $chmod_opts "$permissions" "$file" 2>/dev/null; then
info "Set permissions $permissions on $file"
return 0
else
err "Failed to set permissions $permissions on $file"
return 1
fi
}
# =============================================================================
# CONFIGURATION FILE UTILITIES
# =============================================================================
# Read value from template file with variable expansion support
# Enhanced version supporting more template formats
#
# Usage:
# value=$(config_read_template_value "MYSQL_PASSWORD" ".env.template")
# value=$(config_read_template_value "PORT" "config.template.yml" "yaml")
#
config_read_template_value() {
local key="$1"
local template_file="${2:-${TEMPLATE_FILE:-${TEMPLATE_PATH:-.env.template}}}"
local format="${3:-env}"
if [ ! -f "$template_file" ]; then
err "Template file not found: $template_file"
return 1
fi
case "$format" in
env)
local raw_line value
raw_line=$(grep "^${key}=" "$template_file" 2>/dev/null | head -1)
if [ -z "$raw_line" ]; then
err "Key '$key' not found in template: $template_file"
return 1
fi
value="${raw_line#*=}"
value=$(echo "$value" | sed 's/^"\(.*\)"$/\1/')
# Handle ${VAR:-default} syntax by extracting the default value
if [[ "$value" =~ ^\$\{[^}]*:-([^}]*)\}$ ]]; then
value="${BASH_REMATCH[1]}"
fi
echo "$value"
;;
yaml|yml)
if command -v python3 >/dev/null 2>&1; then
python3 -c "
import yaml, sys
try:
with open('$template_file', 'r') as f:
data = yaml.safe_load(f)
# Simple key lookup - can be enhanced for nested keys
print(data.get('$key', ''))
except:
sys.exit(1)
" 2>/dev/null
else
err "python3 required for YAML template parsing"
return 1
fi
;;
*)
err "Unsupported template format: $format"
return 1
;;
esac
}
# Validate configuration against schema
# Supports basic validation rules
#
# Usage:
# config_validate_env ".env" "required:MYSQL_PASSWORD,PORT;optional:DEBUG"
#
config_validate_env() {
local env_file="$1"
local rules="${2:-}"
if [ ! -f "$env_file" ]; then
err "Environment file not found: $env_file"
return 1
fi
if [ -z "$rules" ]; then
info "No validation rules specified"
return 0
fi
local validation_failed=false
# Parse validation rules
IFS=';' read -ra rule_sets <<< "$rules"
for rule_set in "${rule_sets[@]}"; do
IFS=':' read -ra rule_parts <<< "$rule_set"
local rule_type="${rule_parts[0]}"
local variables="${rule_parts[1]}"
case "$rule_type" in
required)
IFS=',' read -ra req_vars <<< "$variables"
for var in "${req_vars[@]}"; do
if ! grep -q "^${var}=" "$env_file" || [ -z "$(env_read_with_fallback "$var" "" "$env_file")" ]; then
err "Required environment variable missing or empty: $var"
validation_failed=true
fi
done
;;
optional)
# Optional variables - just log if missing
IFS=',' read -ra opt_vars <<< "$variables"
for var in "${opt_vars[@]}"; do
if ! grep -q "^${var}=" "$env_file"; then
info "Optional environment variable not set: $var"
fi
done
;;
esac
done
if [ "$validation_failed" = "true" ]; then
err "Environment validation failed"
return 1
fi
info "Environment validation passed"
return 0
}
# =============================================================================
# SYSTEM UTILITIES
# =============================================================================
# Detect operating system and distribution
# Returns standardized OS identifier
#
# Usage:
# os=$(system_detect_os)
# if [ "$os" = "ubuntu" ]; then
# echo "Running on Ubuntu"
# fi
#
system_detect_os() {
local os="unknown"
if [ -f /etc/os-release ]; then
# Source os-release for distribution info
local id
id=$(grep '^ID=' /etc/os-release | cut -d'=' -f2 | tr -d '"')
case "$id" in
ubuntu|debian|centos|rhel|fedora|alpine|arch)
os="$id"
;;
*)
os="linux"
;;
esac
elif [[ "$OSTYPE" == "darwin"* ]]; then
os="macos"
elif [[ "$OSTYPE" == "cygwin" || "$OSTYPE" == "msys" ]]; then
os="windows"
fi
echo "$os"
}
# Check system requirements
# Validates required commands and versions
#
# Usage:
# system_check_requirements "docker:20.0,python3:3.6"
#
system_check_requirements() {
local requirements="${1:-}"
if [ -z "$requirements" ]; then
return 0
fi
local check_failed=false
IFS=',' read -ra req_list <<< "$requirements"
for requirement in "${req_list[@]}"; do
IFS=':' read -ra req_parts <<< "$requirement"
local command="${req_parts[0]}"
local min_version="${req_parts[1]:-}"
if ! command -v "$command" >/dev/null 2>&1; then
err "Required command not found: $command"
check_failed=true
continue
fi
if [ -n "$min_version" ]; then
# Basic version checking - can be enhanced
info "Found $command (version checking not fully implemented)"
else
info "Found required command: $command"
fi
done
if [ "$check_failed" = "true" ]; then
err "System requirements check failed"
return 1
fi
info "System requirements check passed"
return 0
}
# =============================================================================
# INITIALIZATION AND VALIDATION
# =============================================================================
# Validate environment utility configuration
# Checks that utilities are working correctly
#
# Usage:
# env_utils_validate
#
env_utils_validate() {
info "Validating environment utilities..."
# Test path resolution
local test_path
test_path=$(path_resolve_absolute "." 2>/dev/null)
if [ -z "$test_path" ]; then
err "Path resolution utility not working"
return 1
fi
# Test directory operations
if ! file_ensure_writable_dir "/tmp/env-utils-test.$$"; then
err "Directory utility not working"
return 1
fi
rmdir "/tmp/env-utils-test.$$" 2>/dev/null || true
info "Environment utilities validation successful"
return 0
}
# =============================================================================
# INITIALIZATION
# =============================================================================
# Library loaded successfully
# Scripts can check for $_ENV_UTILS_LIB_LOADED to verify library is loaded