Bash Installer Template

Overview

The Bash Installer Template provides a comprehensive foundation for creating robust, interactive installation scripts for Linux and macOS systems. This template includes error handling, progress tracking, user interaction, and system service management.

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 "$@"
fi

Template Customization Points

Variable Substitution

  • {{app_name}} - Application name

  • {{app_version}} - Application version

  • {{app_description}} - Application description

  • {{install_path}} - Installation directory path

  • {{service_user}} - System user for service

  • {{estimated_time}} - Estimated installation time

  • {{required_space}} - Required disk space

Configurable Parameters

The template supports dynamic parameter collection based on the manifest configuration:

  • Text input with validation

  • Numeric input with range validation

  • Choice selection with predefined options

  • Boolean flags with yes/no prompts

Platform-Specific Sections

  • Linux dependency installation with package manager detection

  • macOS dependency installation with Homebrew

  • Service management (systemd for Linux, launchd for macOS)

  • Path and permission handling per platform

Error Handling Features

  • Comprehensive error trapping and logging

  • Interactive error recovery options

  • Automatic cleanup on failure

  • Rollback capabilities for partial installations

This template provides a solid foundation for creating professional, user-friendly installation scripts that handle the complexity of cross-platform deployment while maintaining excellent user experience and reliability.

Usage Examples

Basic Usage

Advanced Configuration

Last updated