Bash Installer Template
Overview
Complete Bash Installer Template
#!/bin/bash
# {{app_name}} Installer Script
# Version: {{app_version}}
# Generated by I.S.A.A.C. v{{isaac_version}}
# Platform: Linux/macOS
# Generated: {{generation_timestamp}}
set -euo pipefail
# =============================================================================
# CONFIGURATION VARIABLES
# =============================================================================
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LOG_FILE="/tmp/{{app_name}}_install_$(date +%Y%m%d_%H%M%S).log"
readonly TEMP_DIR="/tmp/{{app_name}}_install_$$"
readonly BACKUP_DIR="/tmp/{{app_name}}_backup_$(date +%Y%m%d_%H%M%S)"
# Application configuration
readonly APP_NAME="{{app_name}}"
readonly APP_VERSION="{{app_version}}"
readonly APP_DESCRIPTION="{{app_description}}"
readonly INSTALL_PATH="{{install_path}}"
readonly SERVICE_USER="{{service_user}}"
readonly SERVICE_GROUP="{{service_group}}"
# Default configuration values
{{#each default_config}}
{{name}}="{{value}}"
{{/each}}
# Installation flags
INTERACTIVE_MODE=true
SKIP_DEPENDENCIES=false
DRY_RUN=false
VERBOSE=false
FORCE_INSTALL=false
# =============================================================================
# UTILITY FUNCTIONS
# =============================================================================
# Logging functions
log_info() {
local message="$1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[INFO] $timestamp - $message" | tee -a "$LOG_FILE"
}
log_warn() {
local message="$1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[WARN] $timestamp - $message" | tee -a "$LOG_FILE" >&2
}
log_error() {
local message="$1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[ERROR] $timestamp - $message" | tee -a "$LOG_FILE" >&2
}
log_debug() {
if [[ "$VERBOSE" == "true" ]]; then
local message="$1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[DEBUG] $timestamp - $message" | tee -a "$LOG_FILE"
fi
}
# Progress indicator
show_progress() {
local current=$1
local total=$2
local description="$3"
local width=50
local percentage=$((current * 100 / total))
local filled=$((current * width / total))
local empty=$((width - filled))
printf "\r["
printf "%*s" $filled | tr ' ' '█'
printf "%*s" $empty | tr ' ' '░'
printf "] %d%% - %s" $percentage "$description"
if [ $current -eq $total ]; then
echo ""
fi
}
# Spinner for long-running operations
show_spinner() {
local pid=$1
local description="$2"
local spin='-\|/'
local i=0
while kill -0 $pid 2>/dev/null; do
i=$(( (i+1) %4 ))
printf "\r${spin:$i:1} $description..."
sleep 0.1
done
wait $pid
local exit_code=$?
if [ $exit_code -eq 0 ]; then
printf "\r✓ $description completed\n"
else
printf "\r✗ $description failed\n"
fi
return $exit_code
}
# System detection
detect_os() {
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
echo "linux"
elif [[ "$OSTYPE" == "darwin"* ]]; then
echo "macos"
else
echo "unknown"
fi
}
detect_distribution() {
if [[ -f /etc/os-release ]]; then
. /etc/os-release
echo "$ID"
elif [[ -f /etc/redhat-release ]]; then
echo "centos"
else
echo "unknown"
fi
}
# Version comparison
version_compare() {
local version1="$1"
local operator="$2"
local version2="$3"
# Use sort -V for version comparison
case "$operator" in
">=")
[[ "$(printf '%s\n' "$version2" "$version1" | sort -V | head -n1)" == "$version2" ]]
;;
"<=")
[[ "$(printf '%s\n' "$version1" "$version2" | sort -V | head -n1)" == "$version1" ]]
;;
">")
[[ "$(printf '%s\n' "$version2" "$version1" | sort -V | head -n1)" == "$version2" ]] && [[ "$version1" != "$version2" ]]
;;
"<")
[[ "$(printf '%s\n' "$version1" "$version2" | sort -V | head -n1)" == "$version1" ]] && [[ "$version1" != "$version2" ]]
;;
"==")
[[ "$version1" == "$version2" ]]
;;
*)
return 1
;;
esac
}
# =============================================================================
# USER INTERFACE FUNCTIONS
# =============================================================================
show_welcome() {
clear
cat << 'EOF'
╔══════════════════════════════════════════════════════════════╗
║ {{app_name}} Installer ║
║ Version {{app_version}} ║
╠══════════════════════════════════════════════════════════════╣
║ ║
║ {{app_description}} ║
║ ║
║ This installer will guide you through the setup process. ║
║ You can exit at any time with Ctrl+C. ║
║ ║
║ Estimated installation time: {{estimated_time}} ║
║ Required disk space: {{required_space}} ║
║ ║
╚══════════════════════════════════════════════════════════════╝
EOF
echo ""
if [[ "$INTERACTIVE_MODE" == "true" ]]; then
read -p "Press Enter to continue or Ctrl+C to exit..."
fi
}
show_menu() {
local title="$1"
shift
local options=("$@")
echo ""
echo "┌─ $title ─────────────────────────────────────────────────────┐"
echo "│ │"
for i in "${!options[@]}"; do
printf "│ %d) %-54s │\n" $((i+1)) "${options[$i]}"
done
echo "│ │"
echo "│ q) Quit installer │"
echo "│ h) Help │"
echo "└──────────────────────────────────────────────────────────────┘"
echo ""
}
get_menu_choice() {
local max_choice=$1
local choice
while true; do
read -p "Enter your choice [1-$max_choice, q, h]: " choice
case $choice in
[1-9]|[1-9][0-9])
if [ "$choice" -ge 1 ] && [ "$choice" -le "$max_choice" ]; then
return $choice
else
echo "Invalid choice. Please enter a number between 1 and $max_choice."
fi
;;
q|Q)
echo "Installation cancelled by user."
cleanup_and_exit 0
;;
h|H)
show_help
;;
*)
echo "Invalid input. Please enter a number, 'q' to quit, or 'h' for help."
;;
esac
done
}
# Input validation functions
validate_port() {
local port=$1
if ! [[ "$port" =~ ^[0-9]+$ ]]; then
echo "Error: Port must be a number"
return 1
fi
if [ "$port" -lt 1024 ] || [ "$port" -gt 65535 ]; then
echo "Error: Port must be between 1024 and 65535"
return 1
fi
if netstat -ln 2>/dev/null | grep -q ":$port "; then
echo "Warning: Port $port appears to be in use"
if [[ "$INTERACTIVE_MODE" == "true" ]]; then
read -p "Continue anyway? [y/N]: " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
return 1
fi
fi
fi
return 0
}
get_validated_input() {
local prompt="$1"
local validator="$2"
local default="$3"
local value
while true; do
if [ -n "$default" ]; then
read -p "$prompt [$default]: " value
value=${value:-$default}
else
read -p "$prompt: " value
fi
if [ -z "$value" ] && [ -z "$default" ]; then
echo "This field is required. Please enter a value."
continue
fi
if $validator "$value"; then
echo "$value"
return 0
fi
echo "Please try again."
done
}
# =============================================================================
# SYSTEM CHECKS AND VALIDATION
# =============================================================================
check_system_requirements() {
log_info "Checking system requirements..."
local os_type=$(detect_os)
local distribution=$(detect_distribution)
log_info "Detected OS: $os_type"
log_info "Detected distribution: $distribution"
# Check OS compatibility
case "$os_type" in
"linux")
log_info "Linux system detected - compatible"
;;
"macos")
log_info "macOS system detected - compatible"
local macos_version=$(sw_vers -productVersion)
if ! version_compare "$macos_version" ">=" "{{min_macos_version}}"; then
log_error "macOS {{min_macos_version}} or later is required (found: $macos_version)"
return 1
fi
;;
*)
log_error "Unsupported operating system: $os_type"
return 1
;;
esac
# Check architecture
local arch=$(uname -m)
case "$arch" in
"x86_64"|"amd64")
log_info "Architecture: x86_64 - compatible"
;;
"arm64"|"aarch64")
log_info "Architecture: ARM64 - compatible"
;;
*)
log_warn "Untested architecture: $arch"
;;
esac
# Check required tools
local required_tools=({{#each required_tools}}"{{this}}"{{#unless @last}} {{/unless}}{{/each}})
for tool in "${required_tools[@]}"; do
if ! command -v "$tool" >/dev/null 2>&1; then
log_error "Required tool not found: $tool"
return 1
fi
done
# Check disk space
local available_space=$(df "$INSTALL_PATH" 2>/dev/null | awk 'NR==2 {print $4}' || echo "0")
local required_space={{required_disk_space_kb}}
if [ "$available_space" -lt "$required_space" ]; then
log_error "Insufficient disk space. Required: ${required_space}KB, Available: ${available_space}KB"
return 1
fi
log_info "System requirements check passed"
return 0
}
check_permissions() {
log_info "Checking permissions..."
# Check if running as root (should not be)
if [[ $EUID -eq 0 ]]; then
log_error "This installer should not be run as root"
return 1
fi
# Check sudo access
if ! sudo -n true 2>/dev/null; then
log_info "Sudo access required for system-level operations"
if [[ "$INTERACTIVE_MODE" == "true" ]]; then
echo "Please enter your password for sudo access:"
sudo -v
else
log_error "Sudo access required but not available in non-interactive mode"
return 1
fi
fi
# Check write permissions for install directory
local install_parent=$(dirname "$INSTALL_PATH")
if [[ ! -w "$install_parent" ]]; then
log_error "No write permission for installation directory: $install_parent"
return 1
fi
log_info "Permission check passed"
return 0
}
# =============================================================================
# CONFIGURATION COLLECTION
# =============================================================================
collect_configuration() {
if [[ "$INTERACTIVE_MODE" != "true" ]]; then
log_info "Using default configuration (non-interactive mode)"
return 0
fi
log_info "Collecting configuration parameters..."
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ Configuration Setup ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
{{#each configurable_parameters}}
{{#if (eq type "choice")}}
# {{description}}
echo "{{prompt}}:"
{{#each choices}}
echo " {{@index}}) {{label}} - {{description}}"
{{/each}}
while true; do
read -p "Choice [{{default_index}}]: " choice
choice=${choice:-{{default_index}}}
case $choice in
{{#each choices}}
{{@index}}) {{../name}}="{{value}}"; break ;;
{{/each}}
*) echo "Invalid choice. Please try again." ;;
esac
done
{{else}}
# {{description}}
{{name}}=$(get_validated_input "{{prompt}}" "{{#if validation.pattern}}validate_pattern '{{validation.pattern}}'{{else}}validate_{{type}}{{/if}}" "{{default}}")
{{/if}}
{{/each}}
log_info "Configuration collection completed"
}
show_configuration_summary() {
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ Configuration Summary ║"
echo "╠══════════════════════════════════════════════════════════════╣"
echo "║ ║"
printf "║ Application: %-45s ║\n" "$APP_NAME v$APP_VERSION"
printf "║ Installation Path: %-37s ║\n" "$INSTALL_PATH"
{{#each configurable_parameters}}
printf "║ {{display_name}}: %-{{padding}}s ║\n" "${{name}}"
{{/each}}
echo "║ ║"
echo "╠══════════════════════════════════════════════════════════════╣"
echo "║ ║"
echo "║ The installer will now proceed with these settings. ║"
echo "║ This process may take several minutes to complete. ║"
echo "║ ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
if [[ "$INTERACTIVE_MODE" == "true" ]]; then
read -p "Proceed with installation? [Y/n]: " confirm
if [[ "$confirm" =~ ^[Nn]$ ]]; then
echo "Installation cancelled by user."
cleanup_and_exit 0
fi
fi
}
# =============================================================================
# INSTALLATION FUNCTIONS
# =============================================================================
install_dependencies() {
if [[ "$SKIP_DEPENDENCIES" == "true" ]]; then
log_info "Skipping dependency installation"
return 0
fi
log_info "Installing dependencies..."
local os_type=$(detect_os)
local distribution=$(detect_distribution)
case "$os_type" in
"linux")
install_linux_dependencies "$distribution"
;;
"macos")
install_macos_dependencies
;;
*)
log_error "Unsupported OS for dependency installation: $os_type"
return 1
;;
esac
}
install_linux_dependencies() {
local distribution="$1"
# Detect package manager
if command -v apt-get >/dev/null 2>&1; then
PACKAGE_MANAGER="apt"
UPDATE_CMD="sudo apt-get update"
INSTALL_CMD="sudo apt-get install -y"
elif command -v yum >/dev/null 2>&1; then
PACKAGE_MANAGER="yum"
UPDATE_CMD="sudo yum update -y"
INSTALL_CMD="sudo yum install -y"
elif command -v pacman >/dev/null 2>&1; then
PACKAGE_MANAGER="pacman"
UPDATE_CMD="sudo pacman -Sy"
INSTALL_CMD="sudo pacman -S --noconfirm"
else
log_error "No supported package manager found"
return 1
fi
log_info "Using package manager: $PACKAGE_MANAGER"
# Update package database
log_info "Updating package database..."
if [[ "$DRY_RUN" != "true" ]]; then
$UPDATE_CMD
fi
# Install dependencies
{{#each linux_dependencies}}
log_info "Installing {{name}}..."
if [[ "$DRY_RUN" != "true" ]]; then
if ! $INSTALL_CMD "{{package_name}}"; then
log_error "Failed to install {{name}}"
return 1
fi
# Verify installation
if ! {{verify_command}}; then
log_error "Installation verification failed for {{name}}"
return 1
fi
fi
{{/each}}
log_info "Linux dependencies installed successfully"
}
install_macos_dependencies() {
# Ensure Homebrew is available
if ! command -v brew >/dev/null 2>&1; then
log_info "Installing Homebrew..."
if [[ "$DRY_RUN" != "true" ]]; then
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
fi
# Update Homebrew
log_info "Updating Homebrew..."
if [[ "$DRY_RUN" != "true" ]]; then
brew update
fi
# Install dependencies
{{#each macos_dependencies}}
log_info "Installing {{name}}..."
if [[ "$DRY_RUN" != "true" ]]; then
if ! brew install "{{formula}}"; then
log_error "Failed to install {{name}}"
return 1
fi
# Verify installation
if ! {{verify_command}}; then
log_error "Installation verification failed for {{name}}"
return 1
fi
fi
{{/each}}
log_info "macOS dependencies installed successfully"
}
# =============================================================================
# CLEANUP AND ERROR HANDLING
# =============================================================================
cleanup_temp_files() {
if [[ -d "$TEMP_DIR" ]]; then
log_debug "Cleaning up temporary directory: $TEMP_DIR"
rm -rf "$TEMP_DIR"
fi
}
cleanup_and_exit() {
local exit_code=${1:-0}
log_info "Cleaning up and exiting..."
cleanup_temp_files
if [[ $exit_code -eq 0 ]]; then
log_info "Installation completed successfully!"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ Installation Complete! ║"
echo "╠══════════════════════════════════════════════════════════════╣"
echo "║ ║"
echo "║ {{app_name}} has been successfully installed. ║"
echo "║ ║"
echo "║ Service status: systemctl status {{app_name}} ║"
echo "║ Logs location: {{log_path}} ║"
echo "║ Configuration: {{config_path}} ║"
echo "║ ║"
echo "╚══════════════════════════════════════════════════════════════╝"
else
log_error "Installation failed with exit code: $exit_code"
echo "Check the log file for details: $LOG_FILE"
fi
exit $exit_code
}
# Error handler
handle_error() {
local exit_code=$?
local line_number=$1
log_error "Installation failed at line $line_number with exit code $exit_code"
if [[ "$INTERACTIVE_MODE" == "true" ]]; then
echo ""
echo "Installation failed. Would you like to:"
echo "1) View the error log"
echo "2) Attempt to rollback changes"
echo "3) Exit"
read -p "Choice [3]: " choice
choice=${choice:-3}
case $choice in
1) less "$LOG_FILE" ;;
2) rollback_installation ;;
esac
fi
cleanup_and_exit $exit_code
}
# Set error trap
trap 'handle_error $LINENO' ERR
# =============================================================================
# MAIN INSTALLATION WORKFLOW
# =============================================================================
main() {
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--non-interactive)
INTERACTIVE_MODE=false
shift
;;
--skip-deps)
SKIP_DEPENDENCIES=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
--verbose)
VERBOSE=true
shift
;;
--force)
FORCE_INSTALL=true
shift
;;
--help)
show_help
exit 0
;;
*)
log_error "Unknown option: $1"
show_help
exit 1
;;
esac
done
# Create temporary directory
mkdir -p "$TEMP_DIR"
# Start installation
log_info "Starting {{app_name}} installation..."
log_info "Log file: $LOG_FILE"
# Installation steps
show_welcome
check_system_requirements
check_permissions
collect_configuration
show_configuration_summary
# Main installation phases
local total_steps={{total_installation_steps}}
local current_step=0
{{#each installation_steps}}
current_step=$((current_step + 1))
show_progress $current_step $total_steps "{{description}}"
{{function_name}}
{{/each}}
# Final validation
current_step=$((current_step + 1))
show_progress $current_step $total_steps "Validating installation"
validate_installation
# Success
cleanup_and_exit 0
}
# Execute main function if script is run directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fiTemplate Customization Points
Variable Substitution
Configurable Parameters
Platform-Specific Sections
Error Handling Features
Usage Examples
Basic Usage
Advanced Configuration
Last updated