# This is NOT a script for execution, but for loading functions, so NOT need execution permission or shebang. # NOTE that you NOT need to `cd ..' because the `$0' is NOT this file, but the script file which will source this file. # shellcheck shell=bash function try { "$@" || sleep 0; } function v(){ echo -e "####################################################" echo -e "${STY_BLUE}[$0]: Next command:${STY_RST}" echo -e "${STY_GREEN}$*${STY_RST}" local execute=true if $ask;then while true;do echo -e "${STY_BLUE}Execute? ${STY_RST}" echo " y = Yes" echo " e = Exit now" echo " s = Skip this command (NOT recommended - your setup might not work correctly)" echo " yesforall = Yes and don't ask again; NOT recommended unless you really sure" local p; read -p "====> " p case $p in [yY]) echo -e "${STY_BLUE}OK, executing...${STY_RST}" ;break ;; [eE]) echo -e "${STY_BLUE}Exiting...${STY_RST}" ;exit ;break ;; [sS]) echo -e "${STY_BLUE}Alright, skipping this one...${STY_RST}" ;execute=false ;break ;; "yesforall") echo -e "${STY_BLUE}Alright, won't ask again. Executing...${STY_RST}"; ask=false ;break ;; *) echo -e "${STY_RED}Please enter [y/e/s/yesforall].${STY_RST}";; esac done fi if $execute;then x "$@";else echo -e "${STY_YELLOW}[$0]: Skipped \"$*\"${STY_RST}" fi } # When use v() for a defined function, use x() INSIDE its definition to catch errors. function x(){ if "$@";then local cmdstatus=0;else local cmdstatus=1;fi # 0=normal; 1=failed; 2=failed but ignored while [ $cmdstatus == 1 ] ;do echo -e "${STY_RED}[$0]: Command \"${STY_GREEN}$*${STY_RED}\" has failed." echo -e "You may need to resolve the problem manually BEFORE repeating this command." echo -e "[Tip] If a certain package is failing to install, try installing it separately in another terminal.${STY_RST}" echo " r = Repeat this command (DEFAULT)" echo " e = Exit now" echo " i = Ignore this error and continue (your setup might not work correctly)" local p; read -p " [R/e/i]: " p case $p in [iI]) echo -e "${STY_BLUE}Alright, ignore and continue...${STY_RST}";cmdstatus=2;; [eE]) echo -e "${STY_BLUE}Alright, will exit.${STY_RST}";break;; *) echo -e "${STY_BLUE}OK, repeating...${STY_RST}" if "$@";then cmdstatus=0;else cmdstatus=1;fi ;; esac done case $cmdstatus in 0) echo -e "${STY_BLUE}[$0]: Command \"${STY_GREEN}$*${STY_BLUE}\" finished.${STY_RST}";; 1) echo -e "${STY_RED}[$0]: Command \"${STY_GREEN}$*${STY_RED}\" has failed. Exiting...${STY_RST}";exit 1;; 2) echo -e "${STY_RED}[$0]: Command \"${STY_GREEN}$*${STY_RED}\" has failed but ignored by user.${STY_RST}";; esac } function showfun(){ echo -e "${STY_BLUE}[$0]: The definition of function \"$1\" is as follows:${STY_RST}" printf "${STY_GREEN}" type -a "$1" 2>/dev/null || return 1 printf "${STY_RST}" } function pause(){ if [ ! "$ask" == "false" ];then printf "${STY_FAINT}${STY_SLANT}" local p; read -p "(Ctrl-C to abort, Enter to proceed)" p printf "${STY_RST}" fi } function remove_bashcomments_emptylines(){ echo "pwd=$(pwd)" echo "input=$1" echo "output=$2" mkdir -p "$(dirname "$2")" cat "$1" | sed -e 's/#.*//' -e '/^[[:space:]]*$/d' > "$2" } function prevent_sudo_or_root(){ case $(whoami) in root) echo -e "${STY_RED}[$0]: This script is NOT to be executed with sudo or as root. Aborting...${STY_RST}";exit 1;; esac } # Initialize sudo session and keep it alive in background # Store PID in a global variable that can be accessed by trap declare -g SUDO_KEEPALIVE_PID="" function sudo_init_keepalive(){ # Check if sudo is available if ! command -v sudo >/dev/null 2>&1; then return 0 fi # Skip if already initialized if [[ -n "$SUDO_KEEPALIVE_PID" ]] && kill -0 "$SUDO_KEEPALIVE_PID" 2>/dev/null; then return 0 fi # Prompt for sudo password once at the beginning echo -e "${STY_CYAN}[$0]: Requesting sudo privileges for installation...${STY_RST}" if ! sudo -v; then echo -e "${STY_RED}[$0]: Failed to obtain sudo privileges. Aborting...${STY_RST}" exit 1 fi # Start background process to keep sudo session alive # This updates the sudo timestamp every 60 seconds ( while true; do sleep 60 sudo -v 2>/dev/null || exit 0 done ) & SUDO_KEEPALIVE_PID=$! echo -e "${STY_GREEN}[$0]: Sudo session initialized and will be kept alive (PID: $SUDO_KEEPALIVE_PID)${STY_RST}" } # Stop the sudo keepalive background process function sudo_stop_keepalive(){ if [[ -n "$SUDO_KEEPALIVE_PID" ]] && kill -0 "$SUDO_KEEPALIVE_PID" 2>/dev/null; then kill "$SUDO_KEEPALIVE_PID" 2>/dev/null || true wait "$SUDO_KEEPALIVE_PID" 2>/dev/null || true SUDO_KEEPALIVE_PID="" fi } function git_auto_unshallow(){ # We need this function for latest_commit_hash to work properly if [[ -f "$(git rev-parse --git-dir)/shallow" ]]; then echo "Shallow clone detected. Unshallowing..." git fetch --unshallow fi } function latest_commit_timestamp(){ local target_path="$1" local result=$(git log -1 --format="%ct" -- "$target_path" 2>/dev/null) if [[ -z "$result" ]]; then echo "[latest_commit_timestamp] The timestamp of \"$target_path\" is empty. Aborting..." >&2 return 1 fi echo "$result" } function log_info() { echo -e "${STY_BLUE}[INFO]${STY_RST} $1" } function log_success() { echo -e "${STY_GREEN}[SUCCESS]${STY_RST} $1" } function log_warning() { echo -e "${STY_YELLOW}[WARNING]${STY_RST} $1" } function log_error() { echo -e "${STY_RED}[ERROR]${STY_RST} $1" >&2 } function log_header() { echo -e "\n${STY_PURPLE}=== $1 ===${STY_RST}" } function log_die() { log_error "$1" exit 1 } # Enhanced: Check if command exists function command_exists() { command -v "$1" >/dev/null 2>&1 } # Enhanced: Require a command or die function require_command() { if ! command_exists "$1"; then log_die "Required command '$1' not found. Please install it first." fi } # Enhanced: Sanitize file paths to prevent directory traversal function sanitize_path() { local path="$1" # Remove null bytes, newlines, and control characters path=$(echo "$path" | tr -d '\000-\037') # Prevent directory traversal beyond current context case "$path" in ..|../*|*/../*|*/..|\.\./*) log_die "Invalid path detected (directory traversal attempt): $path" ;; esac echo "$path" } # Enhanced: Safe file comparison that checks existence first function files_differ() { local file1="$1" local file2="$2" # Check if both files exist if [[ ! -f "$file1" ]] || [[ ! -f "$file2" ]]; then return 0 # Consider them different if either doesn't exist fi # Quick size check first (faster than byte comparison) local size1 size2 if command -v stat &>/dev/null; then # Try both BSD and GNU stat formats size1=$(stat -f%z "$file1" 2>/dev/null || stat -c%s "$file1" 2>/dev/null) size2=$(stat -f%z "$file2" 2>/dev/null || stat -c%s "$file2" 2>/dev/null) if [[ "$size1" != "$size2" ]]; then return 0 # Different sizes = different files fi fi # Then byte-by-byte comparison cmp -s "$file1" "$file2" && return 1 || return 0 } # Enhanced: Create backup of a file with timestamp function backup_file_simple() { local file="$1" local backup_suffix="${2:-.bak}" if [[ ! -f "$file" ]]; then log_warning "Cannot backup non-existent file: $file" return 1 fi local timestamp timestamp=$(date +%Y%m%d-%H%M%S) local backup_name="${file}${backup_suffix}.${timestamp}" if cp -p "$file" "$backup_name" 2>/dev/null; then log_info "Backed up: $file → $backup_name" return 0 else log_error "Failed to backup: $file" return 1 fi } # Enhanced: Validate that a file path is within allowed directory function validate_path_in_directory() { local file_path="$1" local allowed_dir="$2" # Resolve to absolute paths local abs_file local abs_dir abs_file=$(cd "$(dirname "$file_path")" 2>/dev/null && pwd -P)/$(basename "$file_path") || return 1 abs_dir=$(cd "$allowed_dir" 2>/dev/null && pwd -P) || return 1 # Check if file path starts with allowed directory case "$abs_file" in "$abs_dir"/*) return 0 ;; *) log_error "Path validation failed: $file_path is not within $allowed_dir" return 1 ;; esac } # Enhanced: Check if script is running in a CI/CD environment function is_ci_environment() { [[ -n "${CI:-}" ]] || \ [[ -n "${GITHUB_ACTIONS:-}" ]] || \ [[ -n "${GITLAB_CI:-}" ]] || \ [[ -n "${TRAVIS:-}" ]] || \ [[ -n "${CIRCLECI:-}" ]] } # Enhanced: Progress bar (optional, for long operations) function show_progress() { local current="$1" local total="$2" local message="${3:-Processing}" if ! command_exists tput; then return fi local percent=$((current * 100 / total)) local bar_length=40 local filled=$((bar_length * current / total)) local empty=$((bar_length - filled)) printf "\r${message}: [" >&2 printf "%${filled}s" | tr ' ' '=' >&2 printf "%${empty}s" | tr ' ' ' ' >&2 printf "] %d%%" "$percent" >&2 if [[ $current -eq $total ]]; then echo >&2 fi } # Enhanced: Cleanup temporary files on exit #declare -a TEMP_FILES_TO_CLEANUP=() function register_temp_file() { local temp_file="$1" TEMP_FILES_TO_CLEANUP+=("$temp_file") } function cleanup_temp_files() { for temp_file in "${TEMP_FILES_TO_CLEANUP[@]}"; do if [[ -f "$temp_file" ]]; then rm -f "$temp_file" 2>/dev/null || true fi done TEMP_FILES_TO_CLEANUP=() } # Enhanced: Check disk space before operations function check_disk_space() { local path="${1:-.}" local required_mb="${2:-100}" # Default 100MB if ! command_exists df; then log_warning "df command not available, skipping disk space check" return 0 fi local available_kb available_kb=$(df -k "$path" | awk 'NR==2 {print $4}') local available_mb=$((available_kb / 1024)) if [[ $available_mb -lt $required_mb ]]; then log_warning "Low disk space: ${available_mb}MB available, ${required_mb}MB recommended" return 1 fi return 0 } function auto_update_git_submodule(){ if git submodule status --recursive | grep -E '^[+-U]';then # Note: `git pull --recurse-submodules` cannot substitute `git submodule update --init --recursive` cuz it does not init a submodule when needed. x git submodule update --init --recursive fi } function backup_clashing_targets(){ # For non-recursive dirs/files under target_dir, only backup those which clashes with the ones under source_dir # However, ignore the ones listed in ignored_list # Deal with arguments local source_dir="$1" local target_dir="$2" local backup_dir="$3" local -a ignored_list=("${@:4}") # Find clash dirs/files, save as clash_list local clash_list=() local source_list=($(ls -A "$source_dir")) local target_list=($(ls -A "$target_dir")) local -A target_map for i in "${target_list[@]}"; do target_map["$i"]=1 done for i in "${source_list[@]}"; do if [[ -n "${target_map[$i]}" ]]; then clash_list+=("$i") fi done local -A delk for del in "${ignored_list[@]}" ; do delk[$del]=1 ; done for k in "${!clash_list[@]}" ; do [ "${delk[${clash_list[$k]}]-}" ] && unset 'clash_list[k]' done clash_list=("${clash_list[@]}") # Construct args_includes for rsync local args_includes=() for i in "${clash_list[@]}"; do if [[ -d "$target_dir/$i" ]]; then args_includes+=(--include="/$i/") args_includes+=(--include="/$i/**") else args_includes+=(--include="/$i") fi done args_includes+=(--exclude='*') x mkdir -p $backup_dir x rsync -av --progress "${args_includes[@]}" "$target_dir/" "$backup_dir/" } function install_cmds(){ case $OS_GROUP_ID in "arch") local pkgs=() for cmd in "$@";do # For package name which is not cmd name, use "case" syntax to replace case $cmd in ip) pkgs+=(iproute2);; *) pkgs+=($cmd) ;; esac done v sudo pacman -Syu v sudo pacman -S --noconfirm --needed "${pkgs[@]}" ;; "debian") local pkgs=() for cmd in "$@";do # For package name which is not cmd name, use "case" syntax to replace case $cmd in ip) pkgs+=(iproute2);; *) pkgs+=($cmd) ;; esac done v sudo apt update -y v sudo apt install -y "${pkgs[@]}" ;; "fedora") local pkgs=() for cmd in "$@";do # For package name which is not cmd name, use "case" syntax to replace case $cmd in ip) pkgs+=(iproute);; *) pkgs+=($cmd) ;; esac done v sudo dnf install -y "${pkgs[@]}" ;; "suse") local pkgs=() for cmd in "$@";do # For package name which is not cmd name, use "case" syntax to replace case $cmd in ip) pkgs+=(iproute2);; *) pkgs+=($cmd) ;; esac done v sudo zypper refresh v sudo zypper -n install "${pkgs[@]}" ;; *) printf "WARNING\n" printf "No method found to install package providing the commands:\n" printf " $@\n" printf "Please install by yourself.\n" ;; esac } function ensure_cmds(){ local not_found_cmds=() for cmd in "$@"; do if ! command -v $cmd >/dev/null 2>&1;then not_found_cmds+=($cmd) fi done if [[ ${#not_found_cmds[@]} -gt 0 ]]; then echo -e "${STY_YELLOW}[$0]: Not found: ${not_found_cmds[*]}.${STY_RST}" install_cmds "${not_found_cmds[@]}" fi } function dedup_and_sort_listfile(){ if ! test -f "$1"; then echo "File not found: $1" >&2; return 2 else temp="$(mktemp)" sort -u -- "$1" > "$temp" mv -f -- "$temp" "$2" fi }