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