|
#!/usr/bin/env bash |
|
set -uo pipefail |
|
IFS=$'\n\t' |
|
set +o noclobber |
|
|
|
# ----------------------------------------------------------------------------- |
|
# Script Name: Bash Alias Install (install_aliases.sh) |
|
# ----------------------------------------------------------------------------- |
|
# @brief This script installs and configures various system components including |
|
# system updates, required packages, and .bash_aliases file. |
|
# |
|
# @details The script performs the following tasks: |
|
# 1. Verifies that the system is Debian-based (Ubuntu/Debian/Raspbian). |
|
# 2. Confirms the user’s intention to proceed with the script. |
|
# 3. Initializes terminal color codes and formatting. |
|
# 4. Fixes broken apt packages and performs system updates/upgrades. |
|
# 5. Installs necessary packages like colordiff and deborphan. |
|
# 6. Downloads and installs the .bash_aliases file from a specified URL. |
|
# 7. Reloads the .bash_aliases file and applies aliases to the current session. |
|
# 8. Notifies the user if a kernel update requires a reboot. |
|
# |
|
# @notes |
|
# - The script uses sudo to perform system-level tasks, so the USE_SUDO variable |
|
# can be set to false if sudo is not required. |
|
# - The script is designed to be interactive but can also be run in non-interactive |
|
# environments without requiring user confirmation. |
|
# - The script assumes that the system has `apt-get` available and is Debian-based. |
|
# - The script’s progress and results are displayed with color-coded feedback for |
|
# easy understanding. |
|
# |
|
# @usage |
|
# 1. Run the script using: |
|
# ./install_aliases.sh |
|
# 2. Set the USE_SUDO variable to false if sudo is not required for the execution. |
|
# 3. Configure the BASH_ALIASES_URL variable if a custom URL for .bash_aliases |
|
# is needed. |
|
# 4. The script will automatically proceed without confirmation if no terminal is |
|
# detected or if the script is run in non-interactive mode. |
|
# |
|
# ----------------------------------------------------------------------------- |
|
|
|
# ----------------------------------------------------------------------------- |
|
# Global Variables for Script Configuration |
|
# ----------------------------------------------------------------------------- |
|
|
|
# @var DRY_RUN |
|
# @brief Enables simulated execution of certain commands. |
|
# @details When set to `true`, commands are not actually executed but are |
|
# simulated to allow testing or validation without side effects. |
|
# If set to `false`, commands execute normally. |
|
declare DRY_RUN="${DRY_RUN:-false}" |
|
|
|
# @var THIS_NAME |
|
# @brief Name of the script displayed in messages and logs. |
|
# @details This is used as the script's identifier for informational output. |
|
# It is helpful when referencing the script in messages or documentation. |
|
declare THIS_NAME="${THIS_NAME:-Bash Alias Install}" |
|
|
|
# @var USE_SUDO |
|
# @brief Flag to determine if the script should use sudo for elevated privileges. |
|
# @details Set to true if sudo is required to execute commands needing root access. |
|
# If the user has root privileges or sudo is not needed, set this to false. |
|
declare USE_SUDO="${USE_SUDO:-true}" |
|
|
|
# @var BASH_ALIASES_URL |
|
# @brief URL for downloading the .bash_aliases file. |
|
# @details This points to the location where the script fetches a preconfigured |
|
# `.bash_aliases` file. The URL can be customized via environment variable |
|
# `BASH_ALIASES_URL` if desired. |
|
declare BASH_ALIASES_URL="${BASH_ALIASES_URL:="https://gist.githubusercontent.com/lbussy/23c05d8dc8c24d8d8edddf1d381f1c8b/raw/my_aliases.sh"}" |
|
|
|
# @var COMMANDS |
|
# @brief List of essential commands required by the script. |
|
# @details These commands are validated for availability before the script executes. |
|
# The script will exit if any of these commands are missing. |
|
declare COMMANDS=( |
|
"curl" # For downloading files from URLs |
|
"apt-get" # For managing package installations on Debian-based systems |
|
"bash" # Required for running the script itself |
|
"grep" # Used for pattern matching and text processing |
|
) |
|
|
|
# @var APTPACKAGES |
|
# @brief List of packages to be installed during the script execution. |
|
# @details These are the system packages the script installs if they are not |
|
# already present on the system. They include tools for debugging, |
|
# system monitoring, and version control. |
|
declare APTPACKAGES=( |
|
"colordiff" # Enhanced colorized diff output |
|
"deborphan" # Identifies orphaned libraries and unused packages |
|
"htop" # Interactive process viewer for system monitoring |
|
"git" # Version control system for managing code repositories |
|
"gh" # GitHub CLI for interacting with GitHub repositories |
|
"needrestart" # Notifies if a reboot is required after updates |
|
) |
|
|
|
# ----------------------------------------------------------------------------- |
|
# Terminal Formatting and Color Codes |
|
# ----------------------------------------------------------------------------- |
|
|
|
# @var RESET |
|
# @brief ANSI escape code to reset all text formatting. |
|
# @details Used to clear any applied text attributes (e.g., bold, colors) and return |
|
# to the terminal's default style. |
|
|
|
# @var FGRED |
|
# @brief ANSI escape code for red-colored text. |
|
# @details Used to display error or critical messages to differentiate them visually. |
|
|
|
# @var FGGRN |
|
# @brief ANSI escape code for green-colored text. |
|
# @details Used for success messages or positive feedback to the user. |
|
|
|
# @var FGGLD |
|
# @brief ANSI escape code for gold/yellow-colored text. |
|
# @details Used for warnings or informational messages requiring attention. Fallbacks |
|
# to yellow if the terminal does not support gold. |
|
|
|
# @var BOLD |
|
# @brief ANSI escape code to make text bold. |
|
# @details Used to emphasize specific parts of the output, such as headers or important |
|
# information. |
|
declare RESET FGRED FGGRN FGGLD BOLD |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Prints a stack trace with optional formatting and a message. |
|
# @details This function generates and displays a formatted stack trace for |
|
# debugging purposes. It includes a log level and optional details, |
|
# with color-coded formatting and proper alignment. |
|
# |
|
# @param $1 [optional] Log level (DEBUG, INFO, WARN, ERROR, CRITICAL). |
|
# Defaults to INFO. |
|
# @param $2 [optional] Primary message for the stack trace. |
|
# @param $@ [optional] Additional context or details for the stack trace. |
|
# |
|
# @global FUNCNAME Array of function names in the call stack. |
|
# @global BASH_LINENO Array of line numbers corresponding to the call stack. |
|
# @global THIS_SCRIPT The name of the current script, used for logging. |
|
# @global COLUMNS Console width, used for formatting output. |
|
# |
|
# @throws None. |
|
# |
|
# @return None. Outputs the stack trace and message to standard output. |
|
# |
|
# @example |
|
# stack_trace WARN "Unexpected condition detected." |
|
# ----------------------------------------------------------------------------- |
|
stack_trace() { |
|
# Determine log level and message |
|
local level="${1:-INFO}" |
|
local message="" |
|
# Block width and character for header/footer |
|
local char="-" |
|
|
|
# Recalculate terminal columns |
|
COLUMNS=$( (command -v tput >/dev/null && tput cols) || printf "80") |
|
COLUMNS=$((COLUMNS > 0 ? COLUMNS : 80)) # Ensure COLUMNS is a positive number |
|
local width |
|
width=${COLUMNS:-80} # Max console width |
|
|
|
# Check if $1 is a valid level, otherwise treat it as the message |
|
case "$level" in |
|
DEBUG|INFO|WARN|WARNING|ERROR|CRIT|CRITICAL) |
|
shift |
|
;; |
|
*) |
|
message="$level" |
|
level="INFO" |
|
shift |
|
;; |
|
esac |
|
|
|
# Concatenate all remaining arguments into $message |
|
for arg in "$@"; do |
|
message+="$arg " |
|
done |
|
# Trim leading/trailing whitespace |
|
message=$(printf "%s" "$message" | xargs) |
|
|
|
# Define functions to skip |
|
local skip_functions=("die" "warn" "stack_trace") |
|
local encountered_main=0 # Track the first occurrence of main() |
|
|
|
# Generate title case function name for the banner |
|
local raw_function_name="${FUNCNAME[0]}" |
|
local header_name header_level |
|
header_name=$(printf "%s" "$raw_function_name" | sed -E 's/_/ /g; s/\b(.)/\U\1/g; s/(\b[A-Za-z])([A-Za-z]*)/\1\L\2/g') |
|
header_level=$(printf "%s" "$level" | sed -E 's/\b(.)/\U\1/g; s/(\b[A-Za-z])([A-Za-z]*)/\1\L\2/g') |
|
header_name="$header_level $header_name" |
|
|
|
# Helper: Skip irrelevant functions |
|
should_skip() { |
|
local func="$1" |
|
for skip in "${skip_functions[@]}"; do |
|
if [[ "$func" == "$skip" ]]; then |
|
return 0 |
|
fi |
|
done |
|
if [[ "$func" == "main" && $encountered_main -gt 0 ]]; then |
|
return 0 |
|
fi |
|
[[ "$func" == "main" ]] && ((encountered_main++)) |
|
return 1 |
|
} |
|
|
|
# Build the stack trace |
|
local displayed_stack=() |
|
local longest_length=0 |
|
for ((i = 1; i < ${#FUNCNAME[@]}; i++)); do |
|
local func="${FUNCNAME[i]}" |
|
local line="${BASH_LINENO[i - 1]}" |
|
local current_length=${#func} |
|
|
|
if should_skip "$func"; then |
|
continue |
|
elif (( current_length > longest_length )); then |
|
longest_length=$current_length |
|
fi |
|
|
|
displayed_stack=("$(printf "%s|%s" "$func()" "$line")" "${displayed_stack[@]}") |
|
done |
|
|
|
# General text attributes |
|
local reset="\033[0m" # Reset text formatting |
|
local bold="\033[1m" # Bold text |
|
|
|
# Foreground colors |
|
local fgred="\033[31m" # Red text |
|
local fggrn="\033[32m" # Green text |
|
local fgblu="\033[34m" # Blue text |
|
local fgmag="\033[35m" # Magenta text |
|
local fgcyn="\033[36m" # Cyan text |
|
local fggld="\033[38;5;220m" # Gold text (ANSI 256 color) |
|
|
|
# Determine color and label based on level |
|
local color label |
|
case "$level" in |
|
DEBUG) color=$fgcyn; label="[DEBUG]";; |
|
INFO) color=$fggrn; label="[INFO ]";; |
|
WARN|WARNING) color=$fggld; label="[WARN ]";; |
|
ERROR) color=$fgmag; label="[ERROR]";; |
|
CRIT|CRITICAL) color=$fgred; label="[CRIT ]";; |
|
esac |
|
|
|
# Create header and footer |
|
local dash_count=$(( (width - ${#header_name} - 2) / 2 )) |
|
local header_l header_r |
|
# Generate a string of spaces |
|
spaces="$(printf '%*s' "$dash_count" "")" |
|
# Replace spaces with the desired character |
|
header_l="$(tr ' ' "$char" <<< "$spaces")" |
|
header_r="$header_l" |
|
[[ $(( (width - ${#header_name}) % 2 )) -eq 1 ]] && header_r="${header_r}${char}" |
|
local header; header=$(printf "%b%s%b %b%b%s%b %b%s%b" \ |
|
"$color" "$header_l" "$reset" "$color" "$bold" "$header_name" "$reset" "$color" "$header_r" "$reset") |
|
local footer repeated_string |
|
# Generate a repeated string safely without triggering ShellCheck warnings |
|
read -r repeated_string < <(printf '%*s' "$width" "" | tr ' ' "$char") |
|
# Generate the footer |
|
footer="$(printf '%b%s%b' "$color" "$repeated_string" "$reset")" |
|
|
|
# Print header |
|
printf "%s\n" "$header" |
|
|
|
# Print the message, if provided |
|
if [[ -n "$message" ]]; then |
|
# Fallback mechanism for wrap_messages |
|
local result primary overflow secondary |
|
if command -v wrap_messages >/dev/null 2>&1; then |
|
result=$(wrap_messages "$width" "$message" || true) |
|
primary="${result%%"${delimiter}"*}" |
|
result="${result#*"${delimiter}"}" |
|
overflow="${result%%"${delimiter}"*}" |
|
else |
|
primary="$message" |
|
fi |
|
# Print the formatted message |
|
printf "%b%s%b\n" "${color}" "${primary}" "${reset}" |
|
printf "%b%s%b\n" "${color}" "${overflow}" "${reset}" |
|
fi |
|
|
|
# Print stack trace |
|
local indent=$(( (width / 2) - ((longest_length + 28) / 2) )) |
|
indent=$(( indent < 0 ? 0 : indent )) |
|
if [[ -z "${displayed_stack[*]}" ]]; then |
|
printf "%b[WARN ]%b Stack trace is empty.\n" "$fggld" "$reset" >&2 |
|
else |
|
for ((i = ${#displayed_stack[@]} - 1, idx = 0; i >= 0; i--, idx++)); do |
|
IFS='|' read -r func line <<< "${displayed_stack[i]}" |
|
printf "%b%*s [%d] Function: %-*s Line: %4s%b\n" \ |
|
"$color" "$indent" ">" "$idx" "$((longest_length + 2))" "$func" "$line" "$reset" |
|
done |
|
fi |
|
|
|
# Print footer |
|
printf "%s\n\n" "$footer" |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Wraps a message into lines with ellipses for overflow or continuation. |
|
# @details This function splits the message into lines, appending an ellipsis |
|
# for overflowed lines and prepending it for continuation lines. The |
|
# primary and secondary messages are processed separately and combined |
|
# with a delimiter. |
|
# |
|
# @param $1 [required] The message string to wrap. |
|
# @param $2 [required] Maximum width of each line (numeric). |
|
# @param $3 [optional] The secondary message string to include (defaults to |
|
# an empty string). |
|
# |
|
# @global None. |
|
# |
|
# @throws None. |
|
# |
|
# @return A single string with wrapped lines and ellipses added as necessary. |
|
# The primary and secondary messages are separated by a delimiter. |
|
# |
|
# @example |
|
# wrapped=$(wrap_messages "This is a long message" 50) |
|
# echo "$wrapped" |
|
# ----------------------------------------------------------------------------- |
|
wrap_messages() { |
|
local line_width=$1 |
|
local primary=$2 |
|
local secondary=${3:-} |
|
local delimiter="␞" |
|
|
|
# Validate input |
|
if [[ -z "$line_width" || ! "$line_width" =~ ^[0-9]+$ || "$line_width" -le 1 ]]; then |
|
printf "Error: Invalid line width '%s' in %s(). Must be a positive integer.\n" \ |
|
"$line_width" "${FUNCNAME[0]}" >&2 |
|
return 1 |
|
fi |
|
|
|
# Inner function to wrap a single message |
|
wrap_message() { |
|
local message=$1 |
|
local width=$2 |
|
local result=() |
|
# Address faulty width with a min of 10 |
|
local adjusted_width=$((width > 10 ? width - 1 : 10)) |
|
|
|
while IFS= read -r line; do |
|
result+=("$line") |
|
done <<< "$(printf "%s\n" "$message" | fold -s -w "$adjusted_width")" |
|
|
|
for ((i = 0; i < ${#result[@]}; i++)); do |
|
result[i]=$(printf "%s" "${result[i]}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') |
|
if ((i == 0)); then |
|
result[i]="${result[i]}…" |
|
elif ((i == ${#result[@]} - 1)); then |
|
result[i]="…${result[i]}" |
|
else |
|
result[i]="…${result[i]}…" |
|
fi |
|
done |
|
|
|
printf "%s\n" "${result[@]}" |
|
} |
|
|
|
# Process primary message |
|
local overflow="" |
|
if [[ ${#primary} -gt $line_width ]]; then |
|
local wrapped_primary |
|
wrapped_primary=$(wrap_message "$primary" "$line_width") |
|
overflow=$(printf "%s\n" "$wrapped_primary" | tail -n +2) |
|
primary=$(printf "%s\n" "$wrapped_primary" | head -n 1) |
|
fi |
|
|
|
# Process secondary message |
|
if [[ -n ${#secondary} && ${#secondary} -gt $line_width ]]; then |
|
secondary=$(wrap_message "$secondary" "$line_width") |
|
fi |
|
|
|
# Combine results |
|
printf "%s%b%s%b%s" \ |
|
"$primary" \ |
|
"$delimiter" \ |
|
"$overflow" \ |
|
"$delimiter" \ |
|
"$secondary" |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Logs a warning message with optional details and stack trace. |
|
# @details This function logs a warning message with color-coded formatting |
|
# and optional details. It adjusts the output to fit within the |
|
# terminal's width and supports message wrapping if the |
|
# `wrap_messages` function is available. If `WARN_STACK_TRACE` is set |
|
# to `true`, a stack trace is also logged. |
|
# |
|
# @param $1 [Optional] The primary warning message. Defaults to |
|
# "A warning was raised on this line" if not provided. |
|
# @param $@ [Optional] Additional details to include in the warning message. |
|
# |
|
# @global FALLBACK_SCRIPT_NAME The name of the script to use if the script |
|
# name cannot be determined. |
|
# @global FUNCNAME Bash array containing the function call stack. |
|
# @global BASH_LINENO Bash array containing the line numbers of |
|
# function calls in the stack. |
|
# @global WRAP_DELIMITER The delimiter used for separating wrapped |
|
# message parts. |
|
# @global WARN_STACK_TRACE If set to `true`, a stack trace will be logged. |
|
# @global COLUMNS The terminal's column width, used to format |
|
# the output. |
|
# |
|
# @return None. |
|
# |
|
# @example |
|
# warn "Configuration file missing." "Please check /etc/config." |
|
# warn "Invalid syntax in the configuration file." |
|
# |
|
# @note This function requires `tput` for terminal width detection and ANSI |
|
# formatting, with fallbacks for minimal environments. |
|
# ----------------------------------------------------------------------------- |
|
warn() { |
|
# Initialize variables |
|
local script="${FALLBACK_SCRIPT_NAME:-unknown}" # This script's name |
|
local func_name="${FUNCNAME[1]:-main}" # Calling function |
|
local caller_line=${BASH_LINENO[0]:-0} # Calling line |
|
|
|
# Get valid error code |
|
local error_code |
|
if [[ -n "${1:-}" && "$1" =~ ^[0-9]+$ ]]; then |
|
error_code=$((10#$1)) # Convert to numeric |
|
shift |
|
else |
|
error_code=1 # Default to 1 if not numeric |
|
fi |
|
|
|
# Configurable delimiter |
|
local delimiter="${WRAP_DELIMITER:-␞}" |
|
|
|
# Get the primary message |
|
local message |
|
message=$(sed -E 's/^[[:space:]]*//;s/[[:space:]]*$//' <<< "${1:-A warning was raised on this line}") |
|
[[ $# -gt 0 ]] && shift |
|
|
|
# Process details |
|
local details |
|
details=$(sed -E 's/^[[:space:]]*//;s/[[:space:]]*$//' <<< "$*") |
|
|
|
# Recalculate terminal columns |
|
COLUMNS=$( (command -v tput >/dev/null && tput cols) || printf "80") |
|
COLUMNS=$((COLUMNS > 0 ? COLUMNS : 80)) # Ensure COLUMNS is a positive number |
|
local width |
|
width=${COLUMNS:-80} # Max console width |
|
|
|
# Escape sequences for colors and attributes |
|
local reset="\033[0m" # Reset text |
|
local bold="\033[1m" # Bold text |
|
local fggld="\033[38;5;220m" # Gold text |
|
local fgcyn="\033[36m" # Cyan text |
|
local fgblu="\033[34m" # Blue text |
|
|
|
# Format prefix |
|
format_prefix() { |
|
local color=${1:-"\033[0m"} |
|
local label="${2:-'[WARN ] [unknown:main:0]'}" |
|
# Create prefix |
|
printf "%b%b%s%b %b[%s:%s:%s]%b " \ |
|
"${bold}" \ |
|
"${color}" \ |
|
"${label}" \ |
|
"${reset}" \ |
|
"${bold}" \ |
|
"${script}" \ |
|
"${func_name}" \ |
|
"${caller_line}" \ |
|
"${reset}" |
|
} |
|
|
|
# Generate prefixes |
|
local warn_prefix extd_prefix dets_prefix |
|
warn_prefix=$(format_prefix "$fggld" "[WARN ]") |
|
extd_prefix=$(format_prefix "$fgcyn" "[EXTND]") |
|
dets_prefix=$(format_prefix "$fgblu" "[DETLS]") |
|
|
|
# Strip ANSI escape sequences for length calculation |
|
local plain_warn_prefix adjusted_width |
|
plain_warn_prefix=$(printf "%s" "$warn_prefix" | sed -E 's/(\x1b\[[0-9;]*[a-zA-Z]|\x1b\([a-zA-Z])//g; s/^[[:space:]]*//; s/[[:space:]]*$//') |
|
adjusted_width=$((width - ${#plain_warn_prefix} - 1)) |
|
|
|
# Fallback mechanism for `wrap_messages` |
|
local result primary overflow secondary |
|
if command -v wrap_messages >/dev/null 2>&1; then |
|
result=$(wrap_messages "$adjusted_width" "$message" "$details" || true) |
|
primary="${result%%"${delimiter}"*}" |
|
result="${result#*"${delimiter}"}" |
|
overflow="${result%%"${delimiter}"*}" |
|
secondary="${result#*"${delimiter}"}" |
|
else |
|
primary="$message" |
|
overflow="" |
|
secondary="$details" |
|
fi |
|
|
|
# Print the primary message |
|
printf "%s%s\n" "$warn_prefix" "$primary" >&2 |
|
|
|
# Print overflow lines |
|
if [[ -n "$overflow" ]]; then |
|
while IFS= read -r line; do |
|
printf "%s%s\n" "$extd_prefix" "$line" >&2 |
|
done <<< "$overflow" |
|
fi |
|
|
|
# Print secondary details |
|
if [[ -n "$secondary" ]]; then |
|
while IFS= read -r line; do |
|
printf "%s%s\n" "$dets_prefix" "$line" >&2 |
|
done <<< "$secondary" |
|
fi |
|
|
|
# Execute stack trace if WARN_STACK_TRACE is enabled |
|
if [[ "${WARN_STACK_TRACE:-false}" == "true" ]]; then |
|
stack_trace "WARNING" "${message}" "${secondary}" |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Terminates the script with a critical error message. |
|
# @details This function is used to log a critical error message with optional |
|
# details and exit the script with the specified error code. It |
|
# supports formatting the output with ANSI color codes, dynamic |
|
# column widths, and optional multi-line message wrapping. |
|
# |
|
# If the optional `wrap_messages` function is available, it will be |
|
# used to wrap and combine messages. Otherwise, the function falls |
|
# back to printing the primary message and details as-is. |
|
# |
|
# @param $1 [optional] Numeric error code. Defaults to 1. |
|
# @param $2 [optional] Primary error message. Defaults to "Critical error". |
|
# @param $@ [optional] Additional details to include in the error message. |
|
# |
|
# @global FALLBACK_SCRIPT_NAME The script name to use as a fallback. |
|
# @global FUNCNAME Bash array containing the call stack. |
|
# @global BASH_LINENO Bash array containing line numbers of the stack. |
|
# @global WRAP_DELIMITER Delimiter used when combining wrapped messages. |
|
# @global COLUMNS The terminal's column width, used to adjust |
|
# message formatting. |
|
# |
|
# @return None. This function does not return. |
|
# @exit Exits the script with the specified error code. |
|
# |
|
# @example |
|
# die 127 "File not found" "Please check the file path and try again." |
|
# die "Critical configuration error" |
|
# ----------------------------------------------------------------------------- |
|
die() { |
|
# Initialize variables |
|
local script="${FALLBACK_SCRIPT_NAME:-unknown}" # This script's name |
|
local func_name="${FUNCNAME[1]:-main}" # Calling function |
|
local caller_line=${BASH_LINENO[0]:-0} # Calling line |
|
|
|
# Get valid error code |
|
if [[ -n "${1:-}" && "$1" =~ ^[0-9]+$ ]]; then |
|
error_code=$((10#$1)) # Convert to numeric |
|
shift |
|
else |
|
error_code=1 # Default to 1 if not numeric |
|
fi |
|
|
|
# Configurable delimiter |
|
local delimiter="${WRAP_DELIMITER:-␞}" |
|
|
|
# Process the primary message |
|
local message |
|
message=$(sed -E 's/^[[:space:]]*//;s/[[:space:]]*$//' <<< "${1:-Critical error}") |
|
|
|
# Only shift if there are remaining arguments |
|
[[ $# -gt 0 ]] && shift |
|
|
|
# Process details |
|
local details |
|
details=$(sed -E 's/^[[:space:]]*//;s/[[:space:]]*$//' <<< "$*") |
|
|
|
# Recalculate terminal columns |
|
COLUMNS=$( (command -v tput >/dev/null && tput cols) || printf "80") |
|
COLUMNS=$((COLUMNS > 0 ? COLUMNS : 80)) # Ensure COLUMNS is a positive number |
|
local width |
|
width=${COLUMNS:-80} # Max console width |
|
|
|
# Escape sequences as safe(r) alternatives to global tput values |
|
# General attributes |
|
local reset="\033[0m" |
|
local bold="\033[1m" |
|
# Foreground colors |
|
local fgred="\033[31m" # Red text |
|
local fgcyn="\033[36m" # Cyan text |
|
local fgblu="\033[34m" # Blue text |
|
|
|
# Format prefix |
|
format_prefix() { |
|
local color=${1:-"\033[0m"} |
|
local label="${2:-'[CRIT ] [unknown:main:0]'}" |
|
# Create prefix |
|
printf "%b%b%s%b %b[%s:%s:%s]%b " \ |
|
"${bold}" \ |
|
"${color}" \ |
|
"${label}" \ |
|
"${reset}" \ |
|
"${bold}" \ |
|
"${script}" \ |
|
"${func_name}" \ |
|
"${caller_line}" \ |
|
"${reset}" |
|
} |
|
|
|
# Generate prefixes |
|
local crit_prefix extd_prefix dets_prefix |
|
crit_prefix=$(format_prefix "$fgred" "[CRIT ]") |
|
extd_prefix=$(format_prefix "$fgcyn" "[EXTND]") |
|
dets_prefix=$(format_prefix "$fgblu" "[DETLS]") |
|
|
|
# Strip ANSI escape sequences for length calculation |
|
local plain_crit_prefix adjusted_width |
|
plain_crit_prefix=$(printf "%s" "$crit_prefix" | sed -E 's/(\x1b\[[0-9;]*[a-zA-Z]|\x1b\([a-zA-Z])//g; s/^[[:space:]]*//; s/[[:space:]]*$//') |
|
adjusted_width=$((width - ${#plain_crit_prefix} - 1)) |
|
|
|
# Fallback mechanism for `wrap_messages` since it is external |
|
local result primary overflow secondary |
|
if command -v wrap_messages >/dev/null 2>&1; then |
|
result=$(wrap_messages "$adjusted_width" "$message" "$details" || true) |
|
primary="${result%%"${delimiter}"*}" |
|
result="${result#*"${delimiter}"}" |
|
overflow="${result%%"${delimiter}"*}" |
|
secondary="${result#*"${delimiter}"}" |
|
else |
|
primary="$message" |
|
overflow="" |
|
secondary="$details" |
|
fi |
|
|
|
# Print the primary message |
|
printf "%s%s\n" "$crit_prefix" "$primary" >&2 |
|
|
|
# Print overflow lines |
|
if [[ -n "$overflow" ]]; then |
|
while IFS= read -r line; do |
|
printf "%s%s\n" "$extd_prefix" "$line" >&2 |
|
done <<< "$overflow" |
|
fi |
|
|
|
# Print secondary details |
|
if [[ -n "$secondary" ]]; then |
|
while IFS= read -r line; do |
|
printf "%s%s\n" "$dets_prefix" "$line" >&2 |
|
done <<< "$secondary" |
|
fi |
|
|
|
# Execute stack trace |
|
stack_trace "CRITICAL" "${message}" "${secondary}" |
|
|
|
# Exit with the specified error code |
|
exit "$error_code" |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Exit the script gracefully with a success message. |
|
# @details This function prints a success message to stderr and exits with a |
|
# status code of 0, indicating a successful execution. The message |
|
# can be customized through the function's argument. |
|
# |
|
# @param $1 [optional] The success message to print. Defaults to "Unspecified exit" |
|
# if no message is provided. |
|
# |
|
# @global FGGRN Used to set green-colored text for the success message. |
|
# @global BOLD Used to apply bold formatting to the success message. |
|
# @global RESET Used to reset text formatting to the terminal's default. |
|
# |
|
# @return This function exits the script with a status code of 0, indicating success. |
|
# |
|
# @example |
|
# ok_exit "Operation completed successfully." |
|
# # Output: [SUCCESS] Operation completed successfully. |
|
# ----------------------------------------------------------------------------- |
|
ok_exit() { |
|
# Local variable to store the success message |
|
local message="${1:-Unspecified exit}" # Defaults to "Unspecified exit" if no message is provided |
|
|
|
# Print the success message to stderr with formatting |
|
printf "%b%b[SUCCESS]%b %s\n" "${FGGRN}" "${BOLD}" "${RESET}" "$message" >&2 |
|
|
|
# Exit with a success status code |
|
exit 0 |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Verify the availability of required commands. |
|
# @details Iterates through the list of required commands defined in the global |
|
# `COMMANDS` array. If any command is missing, the script logs an error |
|
# and exits with status code 1 using the `die` function. |
|
# |
|
# @global COMMANDS Array containing the list of commands to validate. |
|
# |
|
# @throws Exits the script with an error message if a required command is missing. |
|
# |
|
# @example |
|
# COMMANDS=("curl" "bash") |
|
# check_commands |
|
# # If "curl" or "bash" is not installed, the script will exit with an error. |
|
# ----------------------------------------------------------------------------- |
|
check_commands() { |
|
# Iterate through each command in the COMMANDS array |
|
for cmd in "${COMMANDS[@]}"; do |
|
# Check if the command is available on the system |
|
if ! command -v "$cmd" >/dev/null 2>&1; then |
|
# Exit the script with an error if the command is not found |
|
die 1 "Required command '$cmd' is not available on this system." |
|
fi |
|
done |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Verify if the system is Debian-based and supports apt-get. |
|
# @details This function checks `/etc/os-release` for identifiers such as |
|
# "debian", "ubuntu", or "raspbian" to confirm the system is Debian-based. |
|
# It also ensures that the `apt-get` package manager is available. |
|
# If either condition fails, the script exits with an error message. |
|
# |
|
# @throws Exits the script with an error message if the system is not Debian-based |
|
# or does not support `apt-get`. |
|
# |
|
# @example |
|
# check_debian_based |
|
# # If the system is Debian-based and supports apt-get, the function returns 0. |
|
# # Otherwise, the script exits with an error. |
|
# ----------------------------------------------------------------------------- |
|
check_debian_based() { |
|
# Check if the system is Debian-based by looking for known identifiers in /etc/os-release |
|
if grep -qiE "debian|ubuntu|raspbian" /etc/os-release 2>/dev/null; then |
|
# Verify that apt-get is available on the system |
|
if command -v apt-get >/dev/null 2>&1; then |
|
# System is Debian-based and supports apt-get |
|
return 0 |
|
else |
|
# System is Debian-based but apt-get is not available |
|
die 1 "This system is Debian-based but does not support apt-get." |
|
fi |
|
else |
|
# System is not Debian-based |
|
die 1 "Error: This system is not Debian-based." |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Verify the availability of sudo and the user's privileges. |
|
# @details This function checks if the `sudo` command is available and whether |
|
# the user has sufficient privileges to execute sudo commands. If sudo |
|
# is required but unavailable or the user lacks privileges, the script |
|
# exits with an error. |
|
# |
|
# @global USE_SUDO A boolean flag indicating whether sudo is required for the script. |
|
# |
|
# @throws Exits the script with an error message if `sudo` is required but not |
|
# installed or if the user lacks sudo privileges. |
|
# |
|
# @example |
|
# USE_SUDO=true |
|
# check_sudo |
|
# # If sudo is unavailable or privileges are insufficient, the script exits with an error. |
|
# ----------------------------------------------------------------------------- |
|
check_sudo() { |
|
# Check if sudo usage is required |
|
if [ "${USE_SUDO}" = true ]; then |
|
# Verify that the 'sudo' command is available |
|
if ! command -v sudo >/dev/null 2>&1; then |
|
die 1 "This script requires 'sudo', but it is not installed." |
|
fi |
|
|
|
# Verify that the user has sudo privileges |
|
if ! sudo -v >/dev/null 2>&1; then |
|
die 1 "This script requires 'sudo' privileges, but you do not have them." |
|
fi |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Prompt the user for confirmation before proceeding. |
|
# @details This function checks if the script is running in an interactive terminal. |
|
# If it is, it prompts the user to confirm whether to proceed. If no terminal |
|
# is detected, the script automatically proceeds without user confirmation. |
|
# |
|
# @throws Exits the script with a success message if the user cancels the operation. |
|
# |
|
# @example |
|
# confirm_action |
|
# # If the user responds "y" or "yes", the function returns 0 to continue. |
|
# # If the user responds anything else, the script exits with a message. |
|
# ----------------------------------------------------------------------------- |
|
confirm_action() { |
|
# Local variable to store the user's response |
|
local response |
|
|
|
# Check if the script is running in an interactive terminal |
|
if [ -t 0 ]; then |
|
# Prompt the user for confirmation |
|
read -r -p "This script will modify your system. Do you want to continue? [y/N]: " response |
|
|
|
# Convert the response to lowercase for easier comparison |
|
response="${response,,}" |
|
|
|
# Default to "no" if the user does not provide input |
|
if [[ "$response" != "y" && "$response" != "yes" ]]; then |
|
ok_exit "Operation aborted." |
|
fi |
|
else |
|
# Non-interactive environment: automatically proceed |
|
printf "No terminal detected, proceeding without confirmation.\n" |
|
response="y" |
|
fi |
|
|
|
# Evaluate the user's response |
|
case "$response" in |
|
[yY][eE][sS]|[yY]) |
|
printf "\n" |
|
return 0 # User confirmed, proceed with the script |
|
;; |
|
*) |
|
ok_exit "Operation cancelled by user." |
|
esac |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Retrieve terminal control codes using tput if supported. |
|
# @details This function uses `tput` to retrieve terminal control codes for |
|
# text formatting (e.g., colors and attributes). If the terminal does |
|
# not support the requested command or color, it returns an empty string. |
|
# |
|
# @param $1 [required] The tput command to execute (e.g., "setaf", "bold"). |
|
# @param $2 [optional] The color or attribute value for the tput command (e.g., "1" for red). |
|
# Defaults to an empty string if not provided. |
|
# |
|
# @return The terminal control code if supported, or an empty string otherwise. |
|
# |
|
# @example |
|
# RESET=$(terminal_code sgr0) # Reset text formatting |
|
# BOLD=$(terminal_code bold) # Enable bold text |
|
# FGRED=$(terminal_code setaf 1) # Set text color to red |
|
# ----------------------------------------------------------------------------- |
|
terminal_code() { |
|
# Local variables |
|
local command=$1 # Required tput command |
|
local color="${2:-}" # Optional color or attribute value |
|
local tput_colors_available # Number of colors supported by the terminal |
|
|
|
# Check the number of colors supported by the terminal |
|
tput_colors_available=$(tput colors 2>/dev/null || echo "0") |
|
|
|
# If the terminal supports at least 8 colors |
|
if [ "$tput_colors_available" -ge 8 ]; then |
|
# Special case: Substitute "setaf 220" with "setaf 3" (yellow) if limited to 256 colors |
|
if [ "$tput_colors_available" -le 256 ] && [ "$command" = "setaf" ] && [ "$color" = "220" ]; then |
|
tput "$command" 3 2>/dev/null || echo "" # Fallback to "setaf 3" for yellow |
|
else |
|
# Execute the tput command with the provided arguments |
|
tput "$command" "$color" 2>/dev/null || echo "" |
|
fi |
|
else |
|
# Fallback to an empty string if the terminal does not support sufficient colors |
|
printf "" |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Initialize terminal color and formatting codes. |
|
# @details This function sets up variables for terminal text attributes and |
|
# foreground colors using `tput`. If the terminal does not support |
|
# these features, the variables are assigned empty strings to ensure |
|
# graceful fallback behavior. |
|
# |
|
# @global RESET Used to reset all text attributes to the terminal's default. |
|
# @global MOVE_UP Moves the cursor up one line. |
|
# @global CLEAR_LINE Clears the current line. |
|
# @global BOLD Enables bold text formatting. |
|
# @global FGRED Sets the foreground text color to red. |
|
# @global FGGRN Sets the foreground text color to green. |
|
# @global FGGLD Sets the foreground text color to gold (yellow for limited colors). |
|
# |
|
# @return None. |
|
# |
|
# @example |
|
# init_colors |
|
# printf "%bThis text is bold and red.%b\n" "${BOLD}${FGRED}" "${RESET}" |
|
# ----------------------------------------------------------------------------- |
|
init_colors() { |
|
# General text attributes |
|
RESET=$(terminal_code sgr0) # Reset all attributes to default |
|
MOVE_UP=$(terminal_code cuu1) # Move cursor up one line |
|
CLEAR_LINE=$(terminal_code el) # Clear the current line |
|
BOLD=$(terminal_code bold) # Enable bold text formatting |
|
|
|
# Foreground colors |
|
FGRED=$(terminal_code setaf 1) # Set text color to red |
|
FGGRN=$(terminal_code setaf 2) # Set text color to green |
|
FGGLD=$(terminal_code setaf 220) # Set text color to gold (fallback to yellow if limited) |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Executes a command in a separate Bash process. |
|
# @details This function manages the execution of a shell command, handling the |
|
# display of status messages. It supports dry-run mode, where the |
|
# command is simulated without execution. The function prints success |
|
# or failure messages and handles the removal of the "Running" line |
|
# once the command finishes. |
|
# |
|
# @param exec_name The name of the command or task being executed. |
|
# @param exec_process The command string to be executed. |
|
# @param debug Optional flag to enable debug messages. Set to "debug" to |
|
# enable. |
|
# |
|
# @return Returns 0 if the command was successful, non-zero otherwise. |
|
# |
|
# @note The function supports dry-run mode, controlled by the DRY_RUN variable. |
|
# When DRY_RUN is true, the command is only simulated without actual |
|
# execution. |
|
# |
|
# @example |
|
# exec_command "Test Command" "echo Hello World" "debug" |
|
# ----------------------------------------------------------------------------- |
|
exec_command() { |
|
local exec_name="$1" |
|
local exec_process="$2" |
|
|
|
# Basic status prefixes |
|
local running_pre="Running" |
|
local complete_pre="Complete" |
|
local failed_pre="Failed" |
|
|
|
# If DRY_RUN is enabled, show that in the prefix |
|
if [[ "$DRY_RUN" == "true" ]]; then |
|
running_pre+=" (dry)" |
|
complete_pre+=" (dry)" |
|
failed_pre+=" (dry)" |
|
fi |
|
running_pre+=":" |
|
complete_pre+=":" |
|
failed_pre+=":" |
|
|
|
# 1) Print ephemeral “Running” line |
|
printf "%b[-]%b %s %s.\n" "${FGGLD}" "${RESET}" "$running_pre" "$exec_name" |
|
# Optionally ensure it shows up (especially if the command is super fast): |
|
sleep 0.02 |
|
|
|
# 2) If DRY_RUN == "true", skip real exec |
|
if [[ "$DRY_RUN" == "true" ]]; then |
|
# Move up & clear ephemeral line |
|
printf "%b%b" "$MOVE_UP" "$CLEAR_LINE" |
|
printf "%b[✔]%b %s %s.\n" "${FGGRN}" "${RESET}" "$complete_pre" "$exec_name" |
|
return 0 |
|
fi |
|
|
|
# 3) Actually run the command (stdout/stderr handling is up to you): |
|
bash -c "$exec_process" &>/dev/null |
|
local status=$? |
|
|
|
# 4) Move up & clear ephemeral “Running” line |
|
printf "%b%b" "$MOVE_UP" "$CLEAR_LINE" |
|
|
|
# 5) Print final success/fail |
|
if [[ $status -eq 0 ]]; then |
|
printf "%b[✔]%b %s %s.\n" "${FGGRN}" "${RESET}" "$complete_pre" "$exec_name" |
|
else |
|
printf "%b[✘]%b %s %s.\n" "${FGRED}" "${RESET}" "$failed_pre" "$exec_name" |
|
# If specifically “command not found” exit code: |
|
if [[ $status -eq 127 ]]; then |
|
warn "Command not found: $exec_process" |
|
else |
|
warn "Command failed with status $status: $exec_process" |
|
fi |
|
fi |
|
|
|
return $status |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Attempt to fix broken apt packages and dependencies. |
|
# @details This function runs the `apt-get --fix-broken install` command to |
|
# resolve issues with broken package installations or unmet dependencies. |
|
# If the command fails, the script exits with an error message. |
|
# |
|
# @throws Exits the script with an error message if the fix-broken operation fails. |
|
# |
|
# @example |
|
# fix_broken |
|
# # If broken packages are detected, this will attempt to fix them. |
|
# # If the command fails, the script exits with an error. |
|
# ----------------------------------------------------------------------------- |
|
fix_broken() { |
|
# Execute the apt-get fix-broken install command |
|
if ! exec_command "Fix broken installs" "sudo apt-get --fix-broken install -y"; then |
|
# Exit with an error if the command fails |
|
die 1 "Failed to fix broken installs." |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Install GitHub CLI via APT on Debian/Ubuntu. |
|
# @details |
|
# - Ensures wget is available (installs it if missing) |
|
# - Creates /etc/apt/keyrings with proper permissions |
|
# - Downloads and installs GitHub CLI GPG key |
|
# - Adds the official GitHub CLI APT repository |
|
# |
|
# @param $1 Optional 'debug' flag to enable debug output. |
|
# @global None |
|
# @return 0 on success, non-zero on error. |
|
# ----------------------------------------------------------------------------- |
|
install_github_cli_keys() { |
|
# Prepare the keyrings directory |
|
exec_command "Creating /etc/apt/keyrings" "sudo mkdir -p -m 755 /etc/apt/keyrings" |
|
|
|
local url="https://cli.github.com/packages/githubcli-archive-keyring.gpg" |
|
local dest="/etc/apt/keyrings/githubcli-archive-keyring.gpg" |
|
|
|
# Download the script |
|
if ! exec_command "Downloading GitHub GPG keys" "sudo curl -fsSL $url -o $dest"; then |
|
warn "Failed to download GitHub GPG keys" |
|
fi |
|
|
|
# Set ownership and permissions |
|
if ! exec_command "Setting permissions and ownership" "sudo chmod go+r $dest"; then |
|
warn "Failed to set permissions and ownership" |
|
fi |
|
|
|
# Build the repo line |
|
local arch repo_line cmd |
|
arch=$(dpkg --print-architecture) |
|
repo_line="deb [arch=${arch} signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" |
|
|
|
# Pass just the pipeline to exec_command |
|
cmd="echo \"$repo_line\" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null" |
|
if ! exec_command "Adding GitHub CLI APT repository" "$cmd"; then |
|
warn "Failed to add GitHub CLI APT repository" |
|
fi |
|
|
|
return 0 |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Refresh the apt-get package cache. |
|
# @details This function runs `apt-get update` to update the local package index, |
|
# ensuring the system has the latest information about available packages. |
|
# If the update operation fails, the script exits with an error message. |
|
# |
|
# @throws Exits the script with an error message if the `apt-get update` command fails. |
|
# |
|
# @example |
|
# refresh_apt |
|
# # Updates the package list. If successful, the function returns 0. |
|
# # Otherwise, the script exits with an error. |
|
# ----------------------------------------------------------------------------- |
|
refresh_apt() { |
|
# Execute the apt-get update command to refresh the package cache |
|
if ! exec_command "Update local package index" "sudo apt-get update -y"; then |
|
# Exit with an error if the command fails |
|
die 1 "Failed to update package list." |
|
fi |
|
|
|
# Install apt-transport-https to get past https errors before upgrades |
|
if ! exec_command "Update https transport" "sudo apt-get install -y apt-transport-https"; then |
|
# Exit with an error if the command fails |
|
die 1 "Failed to update apt https transport." |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Perform package upgrades for all upgradable apt packages. |
|
# @details This function checks for packages marked as upgradable using |
|
# `apt list --upgradable`, and then upgrades them one at a time. |
|
# If no packages are upgradable, the function exits without error. |
|
# |
|
# @throws Exits the script if a package upgrade command fails. |
|
# |
|
# @example |
|
# upgrade_exist |
|
# # Upgrades all packages listed as upgradable. If no packages are upgradable, |
|
# # the function returns 0 without making any changes. |
|
# ----------------------------------------------------------------------------- |
|
upgrade_exist() { |
|
# Local variables to store the list of upgradable packages and current package name |
|
local pkg_list="" # List of upgradable packages |
|
local pkg_name="" # Current package being processed |
|
|
|
# Fetch the list of upgradable packages |
|
pkg_list=$(apt list --upgradable 2>/dev/null | awk -F '/' 'NR > 1 {print $1}') |
|
|
|
# Check if there are any packages to upgrade |
|
if [ -z "$pkg_list" ]; then |
|
# No packages are upgradable; return successfully |
|
return 0 |
|
fi |
|
|
|
# Iterate through each package in the list and upgrade it |
|
for pkg_name in $pkg_list; do |
|
# Upgrade the current package |
|
exec_command "Updating $pkg_name" "sudo apt-get install $pkg_name -y" |
|
done |
|
|
|
# Return successfully after processing all packages |
|
return 0 |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Perform system updates, upgrades, and maintenance. |
|
# @details This function executes a full system upgrade using `apt-get`. |
|
# |
|
# @throws Exits the script if the full system upgrade fails. |
|
# |
|
# @example |
|
# run_upgrade |
|
# # Performs a full system upgrade |
|
# ----------------------------------------------------------------------------- |
|
run_upgrade() { |
|
# Perform a full system upgrade |
|
if ! exec_command "Perform system upgrade" "sudo apt-get full-upgrade -y"; then |
|
die 1 "System upgrade failed." |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Install required packages. |
|
# @details This function ensures that all packages listed in the `APTPACKAGES` |
|
# array are installed. If a package is not installed, it is installed |
|
# using `apt-get install`. The function skips already installed packages. |
|
# |
|
# @global APTPACKAGES An array of package names to be installed. |
|
# |
|
# @throws Exits the script if a package installation fails. |
|
# |
|
# @example |
|
# install_apt |
|
# # Installs all required packages from the APTPACKAGES array that are not |
|
# # already installed. |
|
# ----------------------------------------------------------------------------- |
|
install_apt() { |
|
# Declare local variables |
|
local package # Current package being processed |
|
|
|
# Iterate through the list of packages |
|
for package in "${APTPACKAGES[@]}"; do |
|
# Check if the package is already installed |
|
if ! dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -q "install ok installed"; then |
|
# Attempt to install the package if not installed |
|
if ! exec_command "Install $package" "sudo apt-get install -y $package"; then |
|
die 1 "Failed to install package: $package" |
|
fi |
|
fi |
|
done |
|
|
|
# Return successfully after processing all packages |
|
return 0 |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Perform apt cache cleanup and remove unused packages. |
|
# @details This function executes `apt-get autoremove` to remove unnecessary |
|
# packages and `apt-get clean` to clear the package cache. These steps |
|
# free up disk space and ensure a clean package environment. |
|
# |
|
# @throws Exits the script if either the autoremove or clean operation fails. |
|
# |
|
# @example |
|
# run_cleanup |
|
# # Removes unused packages and cleans up the apt package cache. |
|
# ----------------------------------------------------------------------------- |
|
run_cleanup() { |
|
# Remove unused packages and their configuration files |
|
if ! exec_command "Remove unused packages" "sudo apt-get autoremove --purge -y"; then |
|
die 1 "Failed to remove unused packages." |
|
fi |
|
|
|
# Clean up the apt package cache |
|
if ! exec_command "Clean package cache" "sudo apt-get clean"; then |
|
die 1 "Failed to clean package cache." |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Download the .bash_aliases file. |
|
# @details This function downloads the `.bash_aliases` file from the URL |
|
# specified in the `BASH_ALIASES_URL` variable and saves it to the |
|
# user's home directory. It verifies that the file is downloaded, |
|
# moves it to the correct location, and sets appropriate ownership |
|
# and permissions. |
|
# |
|
# @global BASH_ALIASES_URL The URL from which the `.bash_aliases` file is downloaded. |
|
# |
|
# @throws Exits the script if: |
|
# - `BASH_ALIASES_URL` is not set. |
|
# - The file fails to download or move. |
|
# - Permissions or ownership adjustments fail. |
|
# |
|
# @example |
|
# download_aliases |
|
# # Downloads the `.bash_aliases` file from the URL and saves it in the user's |
|
# # home directory with correct ownership and permissions. |
|
# ----------------------------------------------------------------------------- |
|
download_aliases() { |
|
# Ensure the URL is set |
|
if [ -z "$BASH_ALIASES_URL" ]; then |
|
die 1 "Bash alias URL is not set." |
|
fi |
|
|
|
# Download the file to a temporary location in the user's home directory |
|
if ! exec_command "Download aliases" "curl -fsSL \"$BASH_ALIASES_URL\" -o \"$HOME/temp.aliases\""; then |
|
die 1 "Unable to download aliases." |
|
fi |
|
|
|
# Verify that the downloaded file exists |
|
if [ ! -f "$HOME/temp.aliases" ]; then |
|
die 1 "Downloaded file does not exist at $HOME/temp.aliases." |
|
fi |
|
|
|
# Move the downloaded file to `.bash_aliases` in the home directory |
|
if ! exec_command "Move aliases to .bash_aliases" "mv \"$HOME/temp.aliases\" \"$HOME/.bash_aliases\""; then |
|
die 1 "Failed to move .bash_aliases to the home directory." |
|
fi |
|
|
|
# Verify the file was moved successfully |
|
if [ ! -f "$HOME/.bash_aliases" ]; then |
|
die 1 "Download completed but .bash_aliases file is missing." |
|
fi |
|
|
|
# Adjust ownership of the `.bash_aliases` file |
|
if ! exec_command "Change owner" "chown $USER:$USER \"$HOME/.bash_aliases\""; then |
|
die 1 "Unable to change ownership." |
|
fi |
|
|
|
# Set appropriate permissions for the `.bash_aliases` file |
|
if ! exec_command "Change execution mode" "chmod 600 \"$HOME/.bash_aliases\""; then |
|
die 1 "Unable to change execution mode." |
|
fi |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Installs a command script to /usr/bin with the specified options. |
|
# @details This function creates a script at /usr/bin, sets executable |
|
# permissions, and assigns root ownership. The script allows for |
|
# custom `ls` options and defaults to the current directory if no |
|
# arguments are provided. |
|
# |
|
# @param $1 Name of the command (e.g., "la" or "ll"). |
|
# @param $2 `ls` options to use for the command (e.g., "-hal" or "-hl"). |
|
# |
|
# @throws Writes error messages to stderr if permissions or ownership cannot |
|
# be set, or if the script fails to create. |
|
# |
|
# @example |
|
# install_command "la" "-hal" |
|
# install_command "ll" "-hl" |
|
# ----------------------------------------------------------------------------- |
|
install_command() { |
|
local command_name="$1" |
|
local bin_path="/usr/local/bin" |
|
local command_path="$bin_path/${command_name}" |
|
local ls_options="$2" |
|
|
|
# Create a temporary script file |
|
local tmp_file="/tmp/${command_name}.tmp" |
|
|
|
cat << EOF > "$tmp_file" |
|
#!/usr/bin/env bash |
|
|
|
lsclean() { |
|
local target="\${1:-.}" |
|
|
|
# Ensure the target variable is always set |
|
if [[ -z "\$target" ]]; then |
|
target="." |
|
fi |
|
|
|
ls ${ls_options} --color=always "\$target" 2>/dev/null | sed -E 's/ -> .*//' |
|
} |
|
|
|
lsclean "\$@" |
|
EOF |
|
|
|
# Verify the file is written |
|
if [[ ! -s "$tmp_file" ]]; then |
|
die 1 "Failed to write temporary script file: $tmp_file" |
|
fi |
|
|
|
# Move it to /usr/local/bin with sudo |
|
sudo mv "$tmp_file" "$command_path" |
|
sudo chmod +x "$command_path" |
|
sudo chown root:root "$command_path" |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Installs the `la` command. |
|
# @details The `la` command lists all files, including hidden ones, in a |
|
# long format with human-readable sizes and color highlighting. |
|
# ----------------------------------------------------------------------------- |
|
install_la_command() { |
|
install_command "la" "-hal" |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Installs the `ll` command. |
|
# @details The `ll` command lists files (excluding hidden ones) in a |
|
# long format with human-readable sizes and color highlighting. |
|
# ----------------------------------------------------------------------------- |
|
install_ll_command() { |
|
install_command "ll" "-hl" |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Installs the `whitespace_clean` command. |
|
# @details The `whitespace_clean` command lists files (excluding hidden ones) in a |
|
# long format with human-readable sizes and color highlighting. |
|
# ----------------------------------------------------------------------------- |
|
install_whitespace_clean() { |
|
local url="https://gist.githubusercontent.com/lbussy/a0c00aa8b5f68d0aeb2e2c6dcd1676e8/raw/whitespace_clean" |
|
local dest="/usr/local/bin/whitespace_clean" |
|
|
|
# Download the script |
|
if ! exec_command "Downloading whitespace_clean" "sudo curl -fsSL $url -o $dest"; then |
|
warn "Failed to download whitespace_clean" |
|
fi |
|
|
|
# Set ownership and permissions |
|
if ! exec_command "Setting permissions and ownership." "sudo chown root:root $dest && sudo chmod 755 $dest" ; then |
|
warn "Failed to set permissions and ownership" |
|
fi |
|
|
|
return 0 |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Updates the `needrestart` configuration with the correct kernel filter. |
|
# |
|
# @details |
|
# - Extracts the kernel suffix from `uname -r` (everything after the last `-`). |
|
# - Writes the appropriate kernel filter regex to `/etc/needrestart/conf.d/kernel.conf`. |
|
# - Ensures `sudo` permissions are used to write to the configuration file. |
|
# - Uses `printf` for safer multi-line output handling. |
|
# |
|
# @throws |
|
# - Exits if `uname -r` fails. |
|
# - Requires `sudo` permissions; will fail if `sudo` is not available. |
|
# |
|
# @example |
|
# need_restart_regex |
|
# # Updates `/etc/needrestart/conf.d/kernel.conf` with the current kernel type. |
|
# ----------------------------------------------------------------------------- |
|
need_restart_regex() { |
|
set -e # Ensure function exits on failure |
|
|
|
# Detect the kernel version suffix (last part after the last '-') |
|
local kernel_ver |
|
kernel_ver=$(uname -r | awk -F'-' '{print $NF}') || { |
|
echo "❌ Failed to retrieve kernel version." |
|
return 1 |
|
} |
|
|
|
# Update the needrestart configuration with the correct kernel filter |
|
sudo tee /etc/needrestart/conf.d/kernel.conf > /dev/null <<EOF |
|
# Filter kernel image filenames by regex. This is required on Raspbian having |
|
# multiple kernel image variants installed in parallel. |
|
\$nrconf{kernelfilter} = qr(vmlinuz-.*-${kernel_ver}$); |
|
EOF |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Notify the user about personal aliases. |
|
# @details This function creates a `.personal_aliases` file in the user's home |
|
# directory (if it does not already exist). It sets the appropriate |
|
# ownership and permissions for the file and provides instructions |
|
# for adding personal aliases that are automatically loaded when |
|
# `.bash_aliases` is sourced. |
|
# |
|
# @throws Issues warnings if the file cannot be created, or ownership and |
|
# permissions cannot be updated. |
|
# |
|
# @example |
|
# notify_personal_aliases |
|
# # Ensures the `.personal_aliases` file exists, sets proper ownership |
|
# # and permissions, and provides user instructions. |
|
# ----------------------------------------------------------------------------- |
|
notify_personal_aliases() { |
|
# Local variable for the .personal_aliases file path |
|
local personal_aliases="$HOME/.personal_aliases" |
|
|
|
# Create the .personal_aliases file if it does not exist |
|
if ! exec_command "Create .personal_aliases" "touch $personal_aliases"; then |
|
warn "Unable to create .personal_aliases file." |
|
fi |
|
|
|
# Update ownership of the .personal_aliases file |
|
if ! exec_command "Change ownership" "chown $USER:$USER $personal_aliases"; then |
|
warn "Unable to change .personal_aliases ownership." |
|
fi |
|
|
|
# Update permissions for the .personal_aliases file |
|
if ! exec_command "Change execution mode" "chmod 600 $personal_aliases"; then |
|
warn "Unable to change .personal_aliases execution mode." |
|
fi |
|
|
|
# Display instructions for adding personal aliases |
|
cat <<EOF |
|
|
|
If you want to save your own aliases, create a file named ${BOLD}.personal_aliases${RESET} |
|
in your home directory. |
|
|
|
Any aliases in that file will be automatically loaded when ${BOLD}.bash_aliases${RESET} is |
|
sourced (e.g., during login or when starting a new shell session). |
|
|
|
Example: |
|
${BOLD}echo "alias ll='ls -lh'" >> ~/.personal_aliases${RESET} |
|
|
|
Then, reload with: |
|
${BOLD}reload_aliases${RESET} |
|
|
|
EOF |
|
|
|
# Indicate successful completion |
|
return 0 |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Remind the user to reload their bash aliases. |
|
# @details |
|
# Prints a message instructing the user to source their |
|
# .bash_aliases file immediately or reboot/restart their shell session. |
|
# |
|
# @param None |
|
# @return None |
|
# ----------------------------------------------------------------------------- |
|
reload_aliases_reminder() { |
|
cat << 'EOF' |
|
|
|
Your bash aliases have been updated. |
|
|
|
To apply them right now, run: |
|
source ./.bash_aliases |
|
|
|
Or restart your shell session (e.g. log out and back in, or reboot). |
|
|
|
EOF |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Main script execution flow. |
|
# @details This function orchestrates the script's execution by calling all the |
|
# required functions in a defined sequence. It ensures the system meets |
|
# necessary prerequisites, performs updates, installs packages, sets up |
|
# aliases, and checks if a reboot is required. The flow is designed to |
|
# handle errors gracefully and provide clear feedback to the user. |
|
# |
|
# @throws Exits the script if any critical errors are encountered in the checks |
|
# or setup phases. |
|
# |
|
# @example |
|
# _main "$@" |
|
# # Executes the main script logic. |
|
# ----------------------------------------------------------------------------- |
|
_main() { |
|
# Clear the screen for a clean start |
|
clear |
|
|
|
# Display a message indicating the script's start |
|
printf "%s beginning.\n\n" "$THIS_NAME" |
|
|
|
# Check for required commands |
|
check_commands |
|
|
|
# Verify the system is Debian-based |
|
check_debian_based |
|
|
|
# Check for sudo presence and privileges if required |
|
check_sudo |
|
|
|
# Request user confirmation in interactive mode |
|
confirm_action |
|
|
|
# Initialize terminal colors and formatting |
|
init_colors |
|
|
|
# Fix broken package installations |
|
fix_broken |
|
|
|
# Install GitHub keys and add the GitHub CLI APT repository |
|
install_github_cli_keys |
|
|
|
# Update the apt-get package cache |
|
refresh_apt |
|
|
|
# Upgrade existing packages |
|
upgrade_exist |
|
|
|
# Perform system upgrades, including checking for kernel updates |
|
run_upgrade |
|
|
|
# Install necessary packages |
|
install_apt |
|
|
|
# Perform apt cache cleanup |
|
run_cleanup |
|
|
|
# Download and set up the .bash_aliases file |
|
download_aliases |
|
|
|
# Install the `la` and `ll` commands |
|
install_la_command |
|
install_ll_command |
|
|
|
# Install the `whitespace_clean` command |
|
install_whitespace_clean |
|
|
|
# Fix regex for kernel detection and run |
|
need_restart_regex |
|
setsid sudo needrestart < /dev/tty |
|
|
|
# Notify the user about setting up personal aliases |
|
notify_personal_aliases |
|
|
|
# Display a final message indicating successful completion |
|
printf "\n%s completed successfully.\n" "$THIS_NAME" |
|
|
|
# Display a notice about loading aliases |
|
reload_aliases_reminder |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# @brief Main function entry point. |
|
# @details This function calls `_main` to initiate the script execution. By |
|
# calling `main`, we enable the correct reporting of the calling |
|
# function in Bash, ensuring that the stack trace and function call |
|
# are handled appropriately during the script execution. |
|
# |
|
# @param "$@" Arguments to be passed to `_main`. |
|
# @return Returns the status code from `_main`. |
|
# ----------------------------------------------------------------------------- |
|
main() { _main "$@"; return "$?"; } |
|
|
|
main "$@" |
|
exit $? |