|
#!/bin/bash |
|
|
|
# ----------------------------- |
|
# TEXT FORMATTING + FEEDBACK |
|
# ----------------------------- |
|
|
|
# Auto-detect if terminal supports colors (without forcing) |
|
_colors_enabled=0 |
|
# Check if output is to a terminal and if TERM indicates color support |
|
if [[ -t 1 ]]; then |
|
case "$TERM" in |
|
xterm*|rxvt*|screen*|tmux*|vt100*|ansi|cygwin|linux|vscode*) |
|
_colors_enabled=1 |
|
;; |
|
esac |
|
# Some terminals report as dumb but still support color |
|
if [[ "$TERM" == "dumb" && -n "$COLORTERM" ]]; then |
|
_colors_enabled=1 |
|
fi |
|
# Check for NO_COLOR environment variable (respecting color disabling convention) |
|
if [[ -n "$NO_COLOR" ]]; then |
|
_colors_enabled=0 |
|
fi |
|
fi |
|
|
|
# ANSI color codes - simple variables for best compatibility |
|
_COLOR_BLACK=30 |
|
_COLOR_RED=31 |
|
_COLOR_GREEN=32 |
|
_COLOR_YELLOW=33 |
|
_COLOR_BLUE=34 |
|
_COLOR_MAGENTA=35 |
|
_COLOR_CYAN=36 |
|
_COLOR_WHITE=37 |
|
_COLOR_GREY=90 |
|
|
|
# Cross-platform echo with escape sequences |
|
_echo() { |
|
# printf is more portable for escape sequences than echo -e |
|
printf "%b\n" "$*" |
|
} |
|
|
|
# A simplified formatting function |
|
# Usage: format_text [options] "text" |
|
# Options: |
|
# --color COLOR : Set text color (black|red|green|yellow|blue|magenta|cyan|white|grey) |
|
# --bold : Make text bold |
|
# --underline : Underline text |
|
# --dim : Dim text |
|
# --indent N : Indent text by N spaces (default 2) |
|
# --prefix CHAR : Add prefix character before text |
|
# --prefix-color C : Set prefix color |
|
format_text() { |
|
# Default values |
|
local color="" |
|
local bold=0 |
|
local underline=0 |
|
local dim=0 |
|
local indent=0 |
|
local prefix="" |
|
local prefix_color="" |
|
local text="" |
|
|
|
# Parse arguments |
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
--color) |
|
color="$2" |
|
shift 2 |
|
;; |
|
--bold) |
|
bold=1 |
|
shift |
|
;; |
|
--underline) |
|
underline=1 |
|
shift |
|
;; |
|
--dim) |
|
dim=1 |
|
shift |
|
;; |
|
--indent) |
|
indent="$2" |
|
shift 2 |
|
;; |
|
--prefix) |
|
prefix="$2" |
|
shift 2 |
|
;; |
|
--prefix-color) |
|
prefix_color="$2" |
|
shift 2 |
|
;; |
|
*) |
|
# Last argument is the text |
|
text="$1" |
|
shift |
|
;; |
|
esac |
|
done |
|
|
|
# Build format string |
|
local format="" |
|
local color_code="" |
|
local prefix_format="" |
|
local indentation="" |
|
|
|
# Apply color if enabled |
|
if [[ "$_colors_enabled" -eq 1 ]]; then |
|
# Get color code |
|
case "$color" in |
|
black) color_code="$_COLOR_BLACK" ;; |
|
red) color_code="$_COLOR_RED" ;; |
|
green) color_code="$_COLOR_GREEN" ;; |
|
yellow) color_code="$_COLOR_YELLOW" ;; |
|
blue) color_code="$_COLOR_BLUE" ;; |
|
magenta) color_code="$_COLOR_MAGENTA" ;; |
|
cyan) color_code="$_COLOR_CYAN" ;; |
|
white) color_code="$_COLOR_WHITE" ;; |
|
grey) color_code="$_COLOR_GREY" ;; |
|
esac |
|
|
|
# Build format string |
|
[[ -n "$color_code" ]] && format+="\033[${color_code}m" |
|
[[ "$bold" -eq 1 ]] && format+="\033[1m" |
|
[[ "$underline" -eq 1 ]] && format+="\033[4m" |
|
[[ "$dim" -eq 1 ]] && format+="\033[2m" |
|
fi |
|
|
|
# Build indentation |
|
if [[ "$indent" -gt 0 ]]; then |
|
indentation="$(printf "%$((indent * 2))s" "")" |
|
fi |
|
|
|
# Handle prefix |
|
if [[ -n "$prefix" ]]; then |
|
if [[ "$_colors_enabled" -eq 1 && -n "$prefix_color" ]]; then |
|
local prefix_code="" |
|
case "$prefix_color" in |
|
black) prefix_code="$_COLOR_BLACK" ;; |
|
red) prefix_code="$_COLOR_RED" ;; |
|
green) prefix_code="$_COLOR_GREEN" ;; |
|
yellow) prefix_code="$_COLOR_YELLOW" ;; |
|
blue) prefix_code="$_COLOR_BLUE" ;; |
|
magenta) prefix_code="$_COLOR_MAGENTA" ;; |
|
cyan) prefix_code="$_COLOR_CYAN" ;; |
|
white) prefix_code="$_COLOR_WHITE" ;; |
|
grey) prefix_code="$_COLOR_GREY" ;; |
|
esac |
|
|
|
if [[ -n "$prefix_code" ]]; then |
|
prefix="\033[${prefix_code}m$prefix\033[0m" |
|
[[ -n "$format" ]] && prefix="$prefix$format" |
|
fi |
|
elif [[ "$_colors_enabled" -eq 1 && -n "$color_code" ]]; then |
|
prefix="\033[${color_code}m$prefix" |
|
fi |
|
text="$prefix $text" |
|
fi |
|
|
|
# Output formatted text |
|
if [[ -n "$format" ]]; then |
|
_echo "${indentation}${format}${text}\033[0m" |
|
else |
|
_echo "${indentation}${text}" |
|
fi |
|
} |
|
|
|
# Shorthand helper functions |
|
bold() { |
|
format_text --bold "$@" |
|
} |
|
|
|
underline() { |
|
format_text --underline "$@" |
|
} |
|
|
|
dim() { |
|
format_text --dim "$@" |
|
} |
|
|
|
red() { |
|
format_text --color red "$@" |
|
} |
|
|
|
green() { |
|
format_text --color green "$@" |
|
} |
|
|
|
yellow() { |
|
format_text --color yellow "$@" |
|
} |
|
|
|
blue() { |
|
format_text --color blue "$@" |
|
} |
|
|
|
grey() { |
|
format_text --color grey "$@" |
|
} |
|
|
|
# Styled helpers |
|
arrow() { |
|
format_text --color blue --prefix "➜" "$@" |
|
} |
|
|
|
bullet() { |
|
format_text --color grey --prefix "•" "$@" |
|
} |
|
|
|
numbered() { |
|
local n="$1" |
|
shift |
|
format_text --color grey --prefix "$(printf "%2d." "$n")" "$@" |
|
} |
|
|
|
info() { |
|
format_text --color white "$@" |
|
} |
|
|
|
success() { |
|
format_text --color green --prefix "✔" "$@" |
|
} |
|
|
|
warn() { |
|
format_text --color yellow --prefix "!" "$@" |
|
} |
|
|
|
error() { |
|
format_text --color red --prefix "✘" "$@" |
|
} |
|
|
|
fatal() { |
|
format_text --color red --prefix "FATAL" "$@" |
|
exit 1 |
|
} |
|
|
|
# ----------------------------- |
|
# HEADERS AND STRUCTURE |
|
# ----------------------------- |
|
|
|
# Store settings to be displayed after the next header |
|
SETTING_KEYS=() |
|
SETTING_VALUES=() |
|
_has_pending_settings=false |
|
|
|
# Add a setting to be displayed with the next header |
|
add_setting() { |
|
local key="$1" |
|
local value="$2" |
|
SETTING_KEYS+=("$key") |
|
SETTING_VALUES+=("$value") |
|
_has_pending_settings=true |
|
} |
|
|
|
# Clear all pending settings |
|
clear_settings() { |
|
SETTING_KEYS=() |
|
SETTING_VALUES=() |
|
_has_pending_settings=false |
|
} |
|
|
|
# Internal function to print pending settings |
|
_print_pending_settings() { |
|
echo |
|
for i in "${!SETTING_KEYS[@]}"; do |
|
printf " $(dim "%-20s") %s\n" "${SETTING_KEYS[$i]}:" "${SETTING_VALUES[$i]}" |
|
done |
|
clear_settings |
|
} |
|
|
|
# Base header function - internal use only |
|
_header() { |
|
local level="" |
|
local title="" |
|
local show_settings=false |
|
|
|
# Parse arguments |
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
--settings) |
|
show_settings=true |
|
shift |
|
;; |
|
1|2|3) |
|
level="$1" |
|
shift |
|
;; |
|
*) |
|
# Last argument is the text |
|
title="$1" |
|
shift |
|
;; |
|
esac |
|
done |
|
|
|
echo |
|
|
|
case "$level" in |
|
1) echo "$(bold "$(underline "$title")")" ;; |
|
2) echo "$(bold "$title")" ;; |
|
3) echo "$(dim "• $title")" ;; |
|
esac |
|
|
|
if [ "$show_settings" = true ] || [ "$_has_pending_settings" = true ]; then |
|
_print_pending_settings |
|
fi |
|
|
|
# Add extra newline for h1 and h2 |
|
if [ "$level" -lt 3 ]; then |
|
echo |
|
fi |
|
} |
|
|
|
# Header 1 - Main section header |
|
h1() { |
|
_header 1 "$@" |
|
} |
|
|
|
# Header 2 - Subsection header |
|
h2() { |
|
_header 2 "$@" |
|
} |
|
|
|
# Header 3 - Minor section header |
|
h3() { |
|
_header 3 "$@" |
|
} |
|
|
|
# ----------------------------- |
|
# PROMPTS |
|
# ----------------------------- |
|
|
|
confirm() { |
|
read -r -p "$(yellow "$1 [y/N]: ")" response |
|
case "$response" in |
|
[yY][eE][sS]|[yY]) true ;; |
|
*) false ;; |
|
esac |
|
} |
|
|
|
# Get user input with optional default value |
|
# Usage: input "What is your name?" "John Doe" |
|
input() { |
|
local prompt="$1" |
|
local default="$2" |
|
local response |
|
|
|
if [ -n "$default" ]; then |
|
read -r -p "$(yellow "$prompt [$default]: ")" response |
|
echo "${response:-$default}" |
|
else |
|
read -r -p "$(yellow "$prompt: ")" response |
|
echo "$response" |
|
fi |
|
} |
|
|
|
# Select from a list of options |
|
# Usage: select_option "Select environment" "dev" "staging" "prod" |
|
select_option() { |
|
local prompt="$1" |
|
shift |
|
local options=("$@") |
|
local selected |
|
|
|
echo "$prompt:" |
|
select selected in "${options[@]}"; do |
|
if [ -n "$selected" ]; then |
|
echo "$selected" |
|
break |
|
fi |
|
done |
|
} |
|
|
|
# ----------------------------- |
|
# UTILITIES |
|
# ----------------------------- |
|
|
|
# Check if command exists |
|
cmd_exists() { |
|
command -v "$1" >/dev/null 2>&1 |
|
} |
|
|
|
# Check if required command is available |
|
# Usage: require_command "terraform" |
|
require_command() { |
|
if ! command -v "$1" &> /dev/null; then |
|
echo "ERROR: Required command '$1' is not installed or not in PATH" >&2 |
|
exit 1 |
|
fi |
|
} |
|
|
|
# Run a command and exit if it fails |
|
run_or_die() { |
|
"$@" |
|
local status=$? |
|
if [ $status -ne 0 ]; then |
|
fatal "Command failed: $*" |
|
fi |
|
return $status |
|
} |
|
|
|
# Run command with retries |
|
# Usage: retry 3 some_command |
|
retry() { |
|
local retries="$1" |
|
shift |
|
local count=0 |
|
local wait=5 |
|
|
|
until "$@"; do |
|
exit=$? |
|
count=$((count + 1)) |
|
|
|
if [ "$count" -lt "$retries" ]; then |
|
warn "Command failed. Attempt $count/$retries. Retrying in ${wait}s..." |
|
sleep "$wait" |
|
else |
|
fatal "Command failed after $retries attempts" |
|
fi |
|
done |
|
return 0 |
|
} |
|
|
|
# Check if script is run as root |
|
require_root() { |
|
if [ "$EUID" -ne 0 ]; then |
|
fatal "This script must be run as root" |
|
fi |
|
} |
|
|
|
# Get absolute path |
|
get_abs_path() { |
|
local path="$1" |
|
echo "$(cd "$(dirname "$path")" && pwd)/$(basename "$path")" |
|
} |
|
|
|
# Check if directory is empty |
|
is_dir_empty() { |
|
local dir="$1" |
|
[ -z "$(ls -A "$dir" 2>/dev/null)" ] |
|
} |
|
|
|
# Create directory if it doesn't exist |
|
ensure_dir() { |
|
local dir="$1" |
|
if [ ! -d "$dir" ]; then |
|
mkdir -p "$dir" |
|
fi |
|
} |
|
|
|
# Check if string contains substring |
|
# Usage: if string_contains "hello world" "world"; then echo "yes"; fi |
|
string_contains() { |
|
local string="$1" |
|
local substring="$2" |
|
[[ "$string" == *"$substring"* ]] |
|
} |
|
|
|
# Spinner (for background jobs) |
|
# Usage: |
|
# long_running_command & spinner |
|
# # or |
|
# (long_running_command) & spinner |
|
spinner() { |
|
local pid=$! |
|
local delay=0.1 |
|
local spinstr='|/-\' |
|
while ps -p "$pid" > /dev/null 2>&1; do |
|
local temp=${spinstr#?} |
|
printf " [%c] " "$spinstr" |
|
local spinstr=$temp${spinstr%"$temp"} |
|
sleep $delay |
|
printf "\b\b\b\b\b\b" |
|
done |
|
printf " \b\b\b\b" |
|
} |
|
|
|
# ----------------------------- |
|
# VALIDATION |
|
# ----------------------------- |
|
|
|
# Validate IP address |
|
is_valid_ip() { |
|
local ip="$1" |
|
local stat=1 |
|
|
|
if [[ "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then |
|
OIFS=$IFS |
|
IFS='.' |
|
read -r -a ip_array <<< "$ip" |
|
IFS=$OIFS |
|
[[ ${ip_array[0]} -le 255 && ${ip_array[1]} -le 255 && \ |
|
${ip_array[2]} -le 255 && ${ip_array[3]} -le 255 ]] |
|
stat=$? |
|
fi |
|
return $stat |
|
} |
|
|
|
# Validate URL |
|
is_valid_url() { |
|
local url="$1" |
|
local regex='(https?|ftp|file)://[-[:alnum:]\+&@#/%?=~_|!:,.;]*[-[:alnum:]\+&@#/%=~_|]' |
|
[[ "$url" =~ $regex ]] |
|
} |
|
|
|
# ----------------------------- |
|
# LOGGING |
|
# ----------------------------- |
|
|
|
logfile="/tmp/script.log" |
|
|
|
log() { |
|
echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "$logfile" |
|
} |
|
|
|
# Rotate log file if it exceeds size (in MB) |
|
rotate_log() { |
|
local max_size="$1" |
|
local size_mb |
|
|
|
if [ -f "$logfile" ]; then |
|
size_mb=$(du -m "$logfile" | cut -f1) |
|
if [ "$size_mb" -gt "$max_size" ]; then |
|
mv "$logfile" "${logfile}.$(date +%Y%m%d_%H%M%S)" |
|
touch "$logfile" |
|
fi |
|
fi |
|
} |
|
|
|
# ----------------------------- |
|
# DEBUG |
|
# ----------------------------- |
|
|
|
debug() { |
|
[ "$DEBUG" == "1" ] && echo -e "$(blue "[DEBUG]") $1" |
|
} |
|
|
|
# Print stack trace |
|
print_stack_trace() { |
|
local frame=0 |
|
while caller "$frame"; do |
|
((frame++)) |
|
done |
|
} |
|
|
|
# Set debug trap to print commands |
|
enable_debug_mode() { |
|
if [ "$DEBUG" == "1" ]; then |
|
set -x |
|
trap 'debug "Line $LINENO: $BASH_COMMAND"' DEBUG |
|
fi |
|
} |
|
|
|
# ----------------------------- |
|
# CLEANUP |
|
# ----------------------------- |
|
|
|
# Array to store cleanup functions |
|
declare -a cleanup_functions |
|
|
|
# Add a function to the cleanup stack that will be executed on script exit |
|
# Usage: add_cleanup "rm -f /tmp/tempfile.txt" |
|
# Params: |
|
# $1: The command to execute during cleanup (as a string) |
|
# Example: |
|
# add_cleanup "rm -f \$tempfile" |
|
# add_cleanup "kill \$server_pid" |
|
add_cleanup() { |
|
cleanup_functions+=("$1") |
|
} |
|
|
|
# Run all registered cleanup functions in reverse order |
|
# This function is automatically called by the EXIT trap |
|
# and should not be called manually |
|
# Usage: Automatic via trap |
|
cleanup() { |
|
# Execute cleanup functions in reverse order (last added, first executed) |
|
for ((i=${#cleanup_functions[@]}-1; i>=0; i--)); do |
|
eval "${cleanup_functions[i]}" |
|
done |
|
} |
|
|
|
# Set trap for cleanup to ensure resources are properly released |
|
# when the script exits for any reason |
|
trap cleanup EXIT |