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:
234
install.sh
234
install.sh
@@ -2,38 +2,26 @@
|
||||
|
||||
# --- Main Script ---
|
||||
|
||||
# This script presents a menu of software packages defined in a remote
|
||||
# configuration file. The user can select one or more packages, and the
|
||||
# script will download the corresponding files. It includes a feature to show
|
||||
# a diff and ask for confirmation before overwriting existing config files.
|
||||
# It can also check for updates to already-installed scripts.
|
||||
# This script presents a menu of software packages, or installs them
|
||||
# non-interactively via command-line arguments. It downloads files from a
|
||||
# remote configuration, shows a diff for config updates, and checks versions.
|
||||
|
||||
# --- Functions ---
|
||||
|
||||
# A simple function to log messages with a consistent format.
|
||||
log() {
|
||||
echo "[$1] $2"
|
||||
}
|
||||
|
||||
# Get the version from a local script file.
|
||||
# It reads the first 5 lines and extracts the version number.
|
||||
get_local_version() {
|
||||
local file_path="$1"
|
||||
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}'
|
||||
else
|
||||
echo "0.0.0" # Return a base version if file doesn't exist.
|
||||
fi
|
||||
}
|
||||
|
||||
# Compare two version strings (e.g., "1.2.0" vs "1.10.0").
|
||||
# Returns 0 if v1 is newer, 1 if they are the same or v2 is newer.
|
||||
# Compare two version strings. Returns 0 if v1 is newer.
|
||||
is_version_greater() {
|
||||
local v1=$1
|
||||
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
|
||||
return 0 # v1 is greater
|
||||
else
|
||||
@@ -41,181 +29,199 @@ is_version_greater() {
|
||||
fi
|
||||
}
|
||||
|
||||
# New function to process a single selected package.
|
||||
# Process a single selected package.
|
||||
process_package() {
|
||||
local choice="$1"
|
||||
# Check if the choice is a valid package name.
|
||||
if [[ -z "${SCRIPT_PACKAGES[$choice]}" ]]; then
|
||||
log "❌" "Invalid package name provided: '${choice}'"
|
||||
local choice_key="$1"
|
||||
local force_overwrite="$2" # Expects "true" or "false"
|
||||
|
||||
if [[ -z "${SCRIPT_PACKAGES[$choice_key]}" ]]; then
|
||||
echo "[❌] Invalid package name provided: '${choice_key}'"
|
||||
return
|
||||
fi
|
||||
|
||||
echo
|
||||
log "⬇️" "Processing package: '${choice}'..."
|
||||
echo "[⬇️] Processing package: '${choice_key}'..."
|
||||
|
||||
# Get the config value and split it into version and URLs
|
||||
config_value="${SCRIPT_PACKAGES[$choice]}"
|
||||
remote_version=$(echo "${config_value}" | cut -d'|' -f1)
|
||||
urls_to_download=$(echo "${config_value}" | cut -d'|' -f2-)
|
||||
# Parse the new config format
|
||||
config_value="${SCRIPT_PACKAGES[$choice_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_to_download=$(echo "${config_value}" | cut -d'|' -f4-)
|
||||
|
||||
read -r -a urls_to_download_array <<< "$urls_to_download"
|
||||
|
||||
for url in "${urls_to_download_array[@]}"; do
|
||||
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
|
||||
log "->" "Found existing config file: '${filename}'."
|
||||
tmp_file=$(mktemp)
|
||||
if [[ "$force_overwrite" == "true" ]]; then
|
||||
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
|
||||
log "🔎" "Comparing versions..."
|
||||
echo "[🔎] Comparing versions..."
|
||||
echo "-------------------- DIFF START --------------------"
|
||||
if command -v colordiff &> /dev/null; then
|
||||
colordiff -u "${filename}" "${tmp_file}"
|
||||
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}"
|
||||
fi
|
||||
echo "--------------------- DIFF END ---------------------"
|
||||
|
||||
read -p "Do you want to overwrite '${filename}'? (y/N) " -n 1 -r REPLY
|
||||
echo
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
mv "${tmp_file}" "${filename}"
|
||||
log "✅" "Updated '${filename}'."
|
||||
echo "[✅] Updated '${filename}'."
|
||||
else
|
||||
rm "${tmp_file}"
|
||||
log "🤷" "Kept existing version of '${filename}'."
|
||||
echo "[🤷] Kept existing version of '${filename}'."
|
||||
fi
|
||||
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}"
|
||||
fi
|
||||
else
|
||||
# Original download logic for all other files.
|
||||
log "->" "Downloading '${filename}'..."
|
||||
echo "[->] Downloading '${filename}'..."
|
||||
if curl -fsSL -o "${filename}" "${url}"; then
|
||||
log "✅" "Successfully downloaded '${filename}'."
|
||||
echo "[✅] Successfully downloaded '${filename}'."
|
||||
if [[ "${filename}" == *.sh || "${filename}" == *.bash ]]; then
|
||||
chmod +x "${filename}"
|
||||
log "🤖" "Made '${filename}' executable."
|
||||
echo "[🤖] Made '${filename}' executable."
|
||||
fi
|
||||
else
|
||||
log "❌" "Error: Failed to download '${filename}'."
|
||||
echo "[❌] Error: Failed to download '${filename}'."
|
||||
fi
|
||||
fi
|
||||
done
|
||||
log "📦" "Package processing complete for '${choice}'."
|
||||
echo "[📦] Package processing complete for '${choice_key}'."
|
||||
}
|
||||
|
||||
|
||||
# --- Main Logic ---
|
||||
|
||||
# Generate a unique temporary filename with a timestamp.
|
||||
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
|
||||
|
||||
# Download the configuration file.
|
||||
log "🔄" "Downloading configuration file..."
|
||||
echo "[🔄] Downloading configuration file..."
|
||||
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
|
||||
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}"
|
||||
|
||||
# --- 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.
|
||||
# We will modify this array to show installation and update status.
|
||||
declare -a options
|
||||
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
|
||||
if [ ${#packages_to_install[@]} -eq 0 ]; then
|
||||
echo "[❌] Flag provided with no package names. Exiting."
|
||||
exit 1
|
||||
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 " Script Downloader "
|
||||
echo "-------------------------------------"
|
||||
for i in "${!options[@]}"; do
|
||||
printf "%d) %s\n" "$((i+1))" "${options[$i]}"
|
||||
done
|
||||
echo "[🔎] Checking for updates..."
|
||||
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
|
||||
|
||||
# If no choices are made, exit gracefully.
|
||||
if [ ${#user_choices[@]} -eq 0 ]; then
|
||||
log "👋" "No selection made. Exiting."
|
||||
echo "[👋] No selection made. Exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Loop through the user's selections and process each one.
|
||||
for choice_num in "${user_choices[@]}"; do
|
||||
# Validate that the input is a number.
|
||||
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
|
||||
fi
|
||||
|
||||
# Convert selection number to array index (0-based).
|
||||
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."
|
||||
if [ "$choice_num" -eq "$quit_num" ]; then
|
||||
echo "[👋] Quit selected. Exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Process the selected package.
|
||||
process_package "${choice}"
|
||||
index=$((choice_num - 1))
|
||||
if [[ -z "${ordered_keys[$index]}" ]]; then
|
||||
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
|
||||
|
||||
echo
|
||||
log "🏁" "All selected packages have been processed."
|
||||
echo "[🏁] All selected packages have been processed."
|
||||
|
||||
|
||||
Reference in New Issue
Block a user