feat(installer): Rework install.sh for non-interactive mode and improved UX

Performs a major refactoring of 'install.sh' to introduce non-interactive installation via command-line arguments (e.g., '--overwrite-config'). Enhances the interactive menu with detailed package information (display name, description, version, update status) and improves config file handling with diff previews. Updates 'packages.conf' format to support new package metadata and uses short, lowercase keys.
This commit is contained in:
2025-09-25 15:43:12 +02:00
parent d9760b9072
commit a16b8aa42b
2 changed files with 130 additions and 125 deletions

View File

@@ -2,38 +2,26 @@
# --- Main Script --- # --- Main Script ---
# This script presents a menu of software packages defined in a remote # This script presents a menu of software packages, or installs them
# configuration file. The user can select one or more packages, and the # non-interactively via command-line arguments. It downloads files from a
# script will download the corresponding files. It includes a feature to show # remote configuration, shows a diff for config updates, and checks versions.
# a diff and ask for confirmation before overwriting existing config files.
# It can also check for updates to already-installed scripts.
# --- Functions --- # --- Functions ---
# A simple function to log messages with a consistent format.
log() {
echo "[$1] $2"
}
# Get the version from a local script file. # Get the version from a local script file.
# It reads the first 5 lines and extracts the version number.
get_local_version() { get_local_version() {
local file_path="$1" local file_path="$1"
if [[ -f "${file_path}" ]]; then if [[ -f "${file_path}" ]]; then
# Grep for the version line, then use awk to get the last field.
head -n 5 "${file_path}" | grep -m 1 "^# Version:" | awk '{print $NF}' head -n 5 "${file_path}" | grep -m 1 "^# Version:" | awk '{print $NF}'
else else
echo "0.0.0" # Return a base version if file doesn't exist. echo "0.0.0" # Return a base version if file doesn't exist.
fi fi
} }
# Compare two version strings (e.g., "1.2.0" vs "1.10.0"). # Compare two version strings. Returns 0 if v1 is newer.
# Returns 0 if v1 is newer, 1 if they are the same or v2 is newer.
is_version_greater() { is_version_greater() {
local v1=$1 local v1=$1
local v2=$2 local v2=$2
# Use sort's version sorting capability to find the "highest" version.
# If the highest version is v1, then v1 > v2.
if [[ "$(printf '%s\n' "$v1" "$v2" | sort -V | head -n 1)" != "$v1" ]]; then if [[ "$(printf '%s\n' "$v1" "$v2" | sort -V | head -n 1)" != "$v1" ]]; then
return 0 # v1 is greater return 0 # v1 is greater
else else
@@ -41,181 +29,199 @@ is_version_greater() {
fi fi
} }
# New function to process a single selected package. # Process a single selected package.
process_package() { process_package() {
local choice="$1" local choice_key="$1"
# Check if the choice is a valid package name. local force_overwrite="$2" # Expects "true" or "false"
if [[ -z "${SCRIPT_PACKAGES[$choice]}" ]]; then
log "❌" "Invalid package name provided: '${choice}'" if [[ -z "${SCRIPT_PACKAGES[$choice_key]}" ]]; then
echo "[❌] Invalid package name provided: '${choice_key}'"
return return
fi fi
echo echo
log "⬇️" "Processing package: '${choice}'..." echo "[⬇️] Processing package: '${choice_key}'..."
# Get the config value and split it into version and URLs # Parse the new config format
config_value="${SCRIPT_PACKAGES[$choice]}" config_value="${SCRIPT_PACKAGES[$choice_key]}"
remote_version=$(echo "${config_value}" | cut -d'|' -f1) display_name=$(echo "${config_value}" | cut -d'|' -f1)
urls_to_download=$(echo "${config_value}" | cut -d'|' -f2-) remote_version=$(echo "${config_value}" | cut -d'|' -f2)
description=$(echo "${config_value}" | cut -d'|' -f3)
urls_to_download=$(echo "${config_value}" | cut -d'|' -f4-)
read -r -a urls_to_download_array <<< "$urls_to_download" read -r -a urls_to_download_array <<< "$urls_to_download"
for url in "${urls_to_download_array[@]}"; do for url in "${urls_to_download_array[@]}"; do
filename=$(basename "${url}") filename=$(basename "${url}")
# If it's a .conf file AND it already exists, ask to overwrite. # Handle config file overwrites
if [[ "${filename}" == *.conf && -f "${filename}" ]]; then if [[ "${filename}" == *.conf && -f "${filename}" ]]; then
log "->" "Found existing config file: '${filename}'." if [[ "$force_overwrite" == "true" ]]; then
tmp_file=$(mktemp) echo "[⚠️] Overwriting '${filename}' due to --overwrite-config flag."
if ! curl -fsSL -o "${filename}" "${url}"; then
echo "[❌] Error: Failed to download '${filename}'."
fi
continue
fi
echo "[->] Found existing config file: '${filename}'."
tmp_file=$(mktemp)
if curl -fsSL -o "${tmp_file}" "${url}"; then if curl -fsSL -o "${tmp_file}" "${url}"; then
log "🔎" "Comparing versions..." echo "[🔎] Comparing versions..."
echo "-------------------- DIFF START --------------------" echo "-------------------- DIFF START --------------------"
if command -v colordiff &> /dev/null; then if command -v colordiff &> /dev/null; then
colordiff -u "${filename}" "${tmp_file}" colordiff -u "${filename}" "${tmp_file}"
else else
# Attempt to use diff's color option, which is common.
diff --color=always -u "${filename}" "${tmp_file}" 2>/dev/null || diff -u "${filename}" "${tmp_file}" diff --color=always -u "${filename}" "${tmp_file}" 2>/dev/null || diff -u "${filename}" "${tmp_file}"
fi fi
echo "--------------------- DIFF END ---------------------" echo "--------------------- DIFF END ---------------------"
read -p "Do you want to overwrite '${filename}'? (y/N) " -n 1 -r REPLY read -p "Do you want to overwrite '${filename}'? (y/N) " -n 1 -r REPLY
echo echo
if [[ $REPLY =~ ^[Yy]$ ]]; then if [[ $REPLY =~ ^[Yy]$ ]]; then
mv "${tmp_file}" "${filename}" mv "${tmp_file}" "${filename}"
log "✅" "Updated '${filename}'." echo "[✅] Updated '${filename}'."
else else
rm "${tmp_file}" rm "${tmp_file}"
log "🤷" "Kept existing version of '${filename}'." echo "[🤷] Kept existing version of '${filename}'."
fi fi
else else
log "❌" "Error: Failed to download new version of '${filename}' for comparison." echo "[❌] Error downloading new version of '${filename}' for comparison."
rm -f "${tmp_file}" rm -f "${tmp_file}"
fi fi
else else
# Original download logic for all other files. # Original download logic for all other files.
log "->" "Downloading '${filename}'..." echo "[->] Downloading '${filename}'..."
if curl -fsSL -o "${filename}" "${url}"; then if curl -fsSL -o "${filename}" "${url}"; then
log "✅" "Successfully downloaded '${filename}'." echo "[✅] Successfully downloaded '${filename}'."
if [[ "${filename}" == *.sh || "${filename}" == *.bash ]]; then if [[ "${filename}" == *.sh || "${filename}" == *.bash ]]; then
chmod +x "${filename}" chmod +x "${filename}"
log "🤖" "Made '${filename}' executable." echo "[🤖] Made '${filename}' executable."
fi fi
else else
log "❌" "Error: Failed to download '${filename}'." echo "[❌] Error: Failed to download '${filename}'."
fi fi
fi fi
done done
log "📦" "Package processing complete for '${choice}'." echo "[📦] Package processing complete for '${choice_key}'."
} }
# --- Main Logic --- # --- Main Logic ---
# Generate a unique temporary filename with a timestamp.
conf_file="packages.conf.$(date +%Y%m%d%H%M%S)" conf_file="packages.conf.$(date +%Y%m%d%H%M%S)"
# Set up a trap to delete the temporary file on exit.
trap 'rm -f "${conf_file}"' EXIT trap 'rm -f "${conf_file}"' EXIT
# Download the configuration file. echo "[🔄] Downloading configuration file..."
log "🔄" "Downloading configuration file..."
if ! curl -fsSL -o "${conf_file}" "https://git.technopunk.space/tomi/Scripts/raw/branch/main/packages.conf"; then if ! curl -fsSL -o "${conf_file}" "https://git.technopunk.space/tomi/Scripts/raw/branch/main/packages.conf"; then
log "❌" "Error: Failed to download packages.conf. Exiting." echo "[❌] Error: Failed to download packages.conf. Exiting."
exit 1 exit 1
fi fi
log "✅" "Configuration file downloaded successfully." echo "[✅] Configuration file downloaded successfully."
# Source the configuration file to load the SCRIPT_PACKAGES associative array.
source "${conf_file}" source "${conf_file}"
# --- Update Check & User Interface --- # --- Argument Parsing for Non-Interactive Mode ---
if [ "$#" -gt 0 ]; then
declare -a packages_to_install
overwrite_configs=false
for arg in "$@"; do
case $arg in
--overwrite-config)
overwrite_configs=true
;;
-*)
echo "[❌] Unknown flag: $arg" >&2
exit 1
;;
*)
packages_to_install+=("$arg")
;;
esac
done
# Create an array of options from the package names. if [ ${#packages_to_install[@]} -eq 0 ]; then
# We will modify this array to show installation and update status. echo "[❌] Flag provided with no package names. Exiting."
declare -a options exit 1
package_keys=("${!SCRIPT_PACKAGES[@]}")
log "🔎" "Checking for updates..."
for key in "${package_keys[@]}"; do
# The config format is now "VERSION|URL1 URL2..."
config_value="${SCRIPT_PACKAGES[$key]}"
remote_version=$(echo "${config_value}" | cut -d'|' -f1)
# Get just the URLs and assume the first URL is the main script to check.
urls=$(echo "${config_value}" | cut -d'|' -f2-)
read -r -a url_array <<< "$urls"
main_script_filename=$(basename "${url_array[0]}")
# Get the local version of the main script file.
local_version=$(get_local_version "${main_script_filename}")
status=""
if [[ -f "${main_script_filename}" ]]; then
status=" (Installed: v${local_version})"
# Compare versions
if is_version_greater "$remote_version" "$local_version"; then
status+=" [Update available: v${remote_version}]"
fi
fi fi
options+=("${key}${status}")
done
options+=("Quit") # Add a Quit option to the menu. echo "[🚀] Running in non-interactive mode."
for pkg_key in "${packages_to_install[@]}"; do
if [[ -n "${SCRIPT_PACKAGES[$pkg_key]}" ]]; then
process_package "$pkg_key" "$overwrite_configs"
else
echo "[⚠️] Unknown package: '$pkg_key'. Skipping."
fi
done
echo "[🏁] Non-interactive run complete."
exit 0
fi
# --- User Interaction --- # --- Interactive Mode ---
declare -a ordered_keys
package_keys_sorted=($(for k in "${!SCRIPT_PACKAGES[@]}"; do echo $k; done | sort))
ordered_keys=("${package_keys_sorted[@]}")
# Manually display the options with numbers. # --- Display Menu ---
echo echo
echo "-------------------------------------" echo "-------------------------------------"
echo " Script Downloader " echo " Script Downloader "
echo "-------------------------------------" echo "-------------------------------------"
for i in "${!options[@]}"; do echo "[🔎] Checking for updates..."
printf "%d) %s\n" "$((i+1))" "${options[$i]}"
done
echo echo
# Prompt the user for one or more choices. for i in "${!ordered_keys[@]}"; do
key="${ordered_keys[$i]}"
config_value="${SCRIPT_PACKAGES[$key]}"
display_name=$(echo "${config_value}" | cut -d'|' -f1)
remote_version=$(echo "${config_value}" | cut -d'|' -f2)
description=$(echo "${config_value}" | cut -d'|' -f3)
urls=$(echo "${config_value}" | cut -d'|' -f4-)
read -r -a url_array <<< "$urls"
main_script_filename=$(basename "${url_array[0]}")
local_version=$(get_local_version "${main_script_filename}")
# Print main package line
echo -e "\033[1m$((i+1))) $key - $display_name (v$remote_version)\033[0m"
# Print description
echo " $description"
# Print status
if [[ -f "${main_script_filename}" ]]; then
if is_version_greater "$remote_version" "$local_version"; then
echo -e " \033[33m[Update available: v${local_version} -> v${remote_version}]\033[0m"
else
echo -e " \033[32m[Installed: v${local_version}]\033[0m"
fi
fi
echo
done
quit_num=$((${#ordered_keys[@]} + 1))
echo -e "\033[1m${quit_num}) Quit\033[0m"
echo
# --- Handle User Input ---
read -p "Please enter your choice(s) (e.g., 1 3 4), or press Enter to quit: " -r -a user_choices read -p "Please enter your choice(s) (e.g., 1 3 4), or press Enter to quit: " -r -a user_choices
# If no choices are made, exit gracefully.
if [ ${#user_choices[@]} -eq 0 ]; then if [ ${#user_choices[@]} -eq 0 ]; then
log "👋" "No selection made. Exiting." echo "[👋] No selection made. Exiting."
exit 0 exit 0
fi fi
# Loop through the user's selections and process each one.
for choice_num in "${user_choices[@]}"; do for choice_num in "${user_choices[@]}"; do
# Validate that the input is a number.
if ! [[ "$choice_num" =~ ^[0-9]+$ ]]; then if ! [[ "$choice_num" =~ ^[0-9]+$ ]]; then
log "⚠️" "Skipping invalid input: '${choice_num}'. Not a number." echo "[⚠️] Skipping invalid input: '${choice_num}'. Not a number."
continue continue
fi fi
if [ "$choice_num" -eq "$quit_num" ]; then
# Convert selection number to array index (0-based). echo "[👋] Quit selected. Exiting."
index=$((choice_num - 1))
# Validate that the index is within the bounds of the options array.
if [[ -z "${options[$index]}" ]]; then
log "⚠️" "Skipping invalid choice: '${choice_num}'. Out of range."
continue
fi
# Get the choice text from the array.
choice_with_status="${options[$index]}"
# Strip the status message to get the package key.
choice=$(echo "${choice_with_status}" | sed 's/ (.*//')
# Handle the "Quit" option.
if [[ "${choice}" == "Quit" ]]; then
log "👋" "Quit selected. Exiting now."
exit 0 exit 0
fi fi
index=$((choice_num - 1))
# Process the selected package. if [[ -z "${ordered_keys[$index]}" ]]; then
process_package "${choice}" echo "[⚠️] Skipping invalid choice: '${choice_num}'. Out of range."
continue
fi
choice_key="${ordered_keys[$index]}"
process_package "$choice_key" "false" # Never force overwrite in interactive mode
done done
echo echo
log "🏁" "All selected packages have been processed." echo "[🏁] All selected packages have been processed."

View File

@@ -1,17 +1,16 @@
#!/bin/bash #!/bin/bash
# #
# This file contains the configuration for the script downloader. # This file contains the configuration for the script downloader.
# The `SCRIPT_PACKAGES` associative array maps a package name to a # The `SCRIPT_PACKAGES` associative array maps a short package name
# pipe-separated string: "<version>|<space-separated list of URLs>". # to a pipe-separated string with the following format:
# "<Display Name>|<Version>|<Description>|<Space-separated list of URLs>"
declare -A SCRIPT_PACKAGES declare -A SCRIPT_PACKAGES
# The version should match the "# Version: x.x.x" line in the main script file. # Format: short_name="Display Name|Version|Description|URL1 URL2..."
SCRIPT_PACKAGES["Aurora Suite"]="2.1.0|https://git.technopunk.space/tomi/Scripts/raw/branch/main/aurora/aurora.sh https://git.technopunk.space/tomi/Scripts/raw/branch/main/aurora/aurora.conf" SCRIPT_PACKAGES["aurora"]="Aurora Suite|2.1.0|A collection of scripts for managing Aurora database instances.|https://git.technopunk.space/tomi/Scripts/raw/branch/main/aurora/aurora.sh https://git.technopunk.space/tomi/Scripts/raw/branch/main/aurora/aurora.conf"
SCRIPT_PACKAGES["Backup Suite"]="1.0.5|https://git.technopunk.space/tomi/Scripts/raw/branch/main/backup/backup.sh https://git.technopunk.space/tomi/Scripts/raw/branch/main/backup/backup.conf" SCRIPT_PACKAGES["backup"]="Backup Suite|1.0.5|A comprehensive script for backing up system files and databases.|https://git.technopunk.space/tomi/Scripts/raw/branch/main/backup/backup.sh https://git.technopunk.space/tomi/Scripts/raw/branch/main/backup/backup.conf"
SCRIPT_PACKAGES["Monitor Suite"]="1.0.5|https://git.technopunk.space/tomi/Scripts/raw/branch/main/monitor/monitor.sh https://git.technopunk.space/tomi/Scripts/raw/branch/main/monitor/monitor.conf" SCRIPT_PACKAGES["monitor"]="Monitor Suite|1.0.5|Scripts for monitoring system health and performance metrics.|https://git.technopunk.space/tomi/Scripts/raw/branch/main/monitor/monitor.sh https://git.technopunk.space/tomi/Scripts/raw/branch/main/monitor/monitor.conf"
SCRIPT_PACKAGES["Key Manager"]="1.2.1|https://git.technopunk.space/tomi/Scripts/raw/branch/main/hdb_keymanager.sh" SCRIPT_PACKAGES["keymanager"]="Key Manager|1.2.1|A utility for managing HDB user keys for SAP HANA.|https://git.technopunk.space/tomi/Scripts/raw/branch/main/hdb_keymanager.sh"
SCRIPT_PACKAGES["File Cleaner"]="1.1.0|https://git.technopunk.space/tomi/Scripts/raw/branch/main/clean.sh" SCRIPT_PACKAGES["cleaner"]="File Cleaner|1.1.0|A simple script to clean up temporary files and logs.|https://git.technopunk.space/tomi/Scripts/raw/branch/main/clean.sh"
SCRIPT_PACKAGES["HANA Tool"]="1.5.0|https://git.technopunk.space/tomi/Scripts/raw/branch/main/hanatool.sh" SCRIPT_PACKAGES["hanatool"]="HANA Tool|1.5.0|A command-line tool for various SAP HANA administration tasks.|https://git.technopunk.space/tomi/Scripts/raw/branch/main/hanatool.sh"
# Example: Add a new script with its version.
# SCRIPT_PACKAGES["My Other Script"]="1.0.0|https://path/to/my-other-script.sh"