mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 23:07:03 +01:00
Some checks failed
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
billsplit CI / quality (push) Has been cancelled
reverse-survivors CI / check (push) Has been cancelled
The shellcheck hook ran with --severity=warning, so info- and style-level
findings never failed a commit. 222 had accumulated across 94 of 244 shell
files. They were not all style noise -- six were bugs:
- Five scripts in single_use/misc/testsAndMisc-bash/ sourced
$SCRIPT_DIR/../../lib/common.sh, one directory too shallow. Nothing exists
at that path, so all five failed at runtime.
- setup_personal_website.sh and setup_kcd2_dice_solver.sh called usage()
(which ends in `exit 0`) and then `exit 1` for an unknown command. The
exit 1 was unreachable, so a typo'd subcommand reported SUCCESS.
- Six i3blocks scripts had a `# shellcheck source=` directive missing the
scripts/periodic_background/ path segment. The stale path had silently
disabled source-following for all of them.
- diagnose_arch_performance.sh word-split a newline-joined file list; when
the find came back empty, awk got no file operands, fell through to
reading stdin, and the diagnostic hung.
- lib/common.sh's handle_arg_help_or_unknown ended in an unreachable
`return 0` while documenting "Returns: 0 if handled" -- a contract no code
path could satisfy.
- deploy.sh's SCRIPT_DIR is reassigned by config.sh, which it sources at top
level, and config.sh can repoint it at the on-device path
/data/local/tmp/focus_mode. Renamed to DEPLOY_DIR.
The SC1091 bulk (81 findings) was resolved rather than silenced: .shellcheckrc
sets external-sources + source-path=SCRIPTDIR so sourced files are actually
followed and analysed. That is what surfaced the i3blocks and deploy.sh bugs.
17 `# shellcheck disable=` directives remain, each with a written reason, and
only where the finding is provably wrong: injection-test payloads that must not
expand, literal source text being grepped for, POSIX sh scripts on the phone
where arrays do not exist, and source paths that only exist at runtime.
Everything else was fixed in code.
Verified in a CLEAN worktree, not just my own tree. config.sh sources the
gitignored config_secrets.sh, which exists locally but not in a fresh checkout;
a working-tree-only check passed while ci_mirror.sh -- which lints HEAD in a
clean worktree, exactly as CI does -- failed on it.
The gate is verified to bite: a file whose only finding is SC2016 (info) fails
the hook, while the old --severity=warning gate exits 0 on it.
third_party/ is excluded -- vendored upstream, matching the prettier hook.
Not fixed, reported: check_polling_antipatterns.sh carries a pre-existing
SC2034 disable that hides a real bug -- its `patterns` array is never used, so
the checker detects nothing. Confirmed unchanged by this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
621 lines
16 KiB
Bash
Executable File
621 lines
16 KiB
Bash
Executable File
#!/bin/bash
|
|
# Common library functions for linux-configuration scripts
|
|
# Source this file at the beginning of scripts that need shared functionality
|
|
#
|
|
# Usage: source "$(dirname "$(readlink -f "$0")")/../lib/common.sh"
|
|
# Or: source "/path/to/scripts/lib/common.sh"
|
|
|
|
# Prevent multiple sourcing
|
|
[[ -n ${_LIB_COMMON_LOADED:-} ]] && return 0
|
|
_LIB_COMMON_LOADED=1
|
|
|
|
# =============================================================================
|
|
# LOGGING FUNCTIONS
|
|
# =============================================================================
|
|
|
|
# Log message with timestamp to stderr and optionally to a file
|
|
# Usage: log_message "message" [log_file]
|
|
log_message() {
|
|
local msg="$1"
|
|
local log_file="${2:-}"
|
|
local formatted
|
|
printf -v formatted '%(%Y-%m-%d %H:%M:%S)T - %s' -1 "$msg"
|
|
echo "$formatted" >&2
|
|
if [[ -n $log_file ]]; then
|
|
echo "$formatted" >>"$log_file" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
# Simple log with timestamp (no file output)
|
|
# Usage: log "message"
|
|
log() {
|
|
local _ts
|
|
printf -v _ts '%(%Y-%m-%d %H:%M:%S)T' -1
|
|
printf '[%s] %s\n' "$_ts" "$*"
|
|
}
|
|
|
|
# =============================================================================
|
|
# SUDO / ROOT HANDLING
|
|
# =============================================================================
|
|
|
|
# Check if running as root, if not re-exec with sudo
|
|
# Usage: require_root "$@"
|
|
require_root() {
|
|
if [[ $EUID -ne 0 ]]; then
|
|
echo "This script requires root privileges."
|
|
echo "Requesting sudo access..."
|
|
exec sudo "$0" "$@"
|
|
fi
|
|
}
|
|
|
|
# Get the actual user even when running with sudo
|
|
# Usage: ACTUAL_USER=$(get_actual_user)
|
|
get_actual_user() {
|
|
echo "${SUDO_USER:-$USER}"
|
|
}
|
|
|
|
# Get the actual user's home directory
|
|
# Usage: USER_HOME=$(get_actual_user_home)
|
|
get_actual_user_home() {
|
|
local user
|
|
user=$(get_actual_user)
|
|
if [[ $user == "root" ]]; then
|
|
echo "/root"
|
|
else
|
|
echo "/home/$user"
|
|
fi
|
|
}
|
|
|
|
# Set both ACTUAL_USER and USER_HOME variables (common pattern)
|
|
# Usage: set_actual_user_vars
|
|
# echo "$ACTUAL_USER" # => the actual user
|
|
# echo "$USER_HOME" # => /home/username
|
|
set_actual_user_vars() {
|
|
ACTUAL_USER=$(get_actual_user)
|
|
USER_HOME=$(get_actual_user_home)
|
|
export ACTUAL_USER USER_HOME
|
|
}
|
|
|
|
# =============================================================================
|
|
# ARGUMENT PARSING HELPERS
|
|
# =============================================================================
|
|
|
|
# Parse common --interactive/-i and --help/-h flags
|
|
# Sets INTERACTIVE_MODE variable (exported for use by calling scripts)
|
|
# Usage: parse_common_args "$@"
|
|
# shift "$COMMON_ARGS_SHIFT"
|
|
export INTERACTIVE_MODE=false
|
|
export COMMON_ARGS_SHIFT=0
|
|
|
|
parse_interactive_args() {
|
|
INTERACTIVE_MODE=false
|
|
COMMON_ARGS_SHIFT=0
|
|
local script_name="${0##*/}"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-i | --interactive)
|
|
INTERACTIVE_MODE=true
|
|
((COMMON_ARGS_SHIFT++))
|
|
shift
|
|
;;
|
|
-h | --help)
|
|
echo "Usage: $script_name [OPTIONS]"
|
|
echo "Options:"
|
|
echo " -i, --interactive Enable interactive prompts (default: auto-yes)"
|
|
echo " -h, --help Show this help message"
|
|
exit 0
|
|
;;
|
|
*)
|
|
# Stop parsing at first unknown argument
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# Handle common argument patterns for scripts with custom usage functions
|
|
# Usage: handle_arg_help_or_unknown "$1" usage_function err_function
|
|
# Returns: 1 if the argument is not ours, so the caller handles it.
|
|
# Exits: on -h/--help (exit 0) or unknown arg starting with - (exit 2).
|
|
# There is deliberately no success return: every other branch exits. The old
|
|
# trailing `return 0` was unreachable, and the doc promised a 0 that no code
|
|
# path could ever produce.
|
|
handle_arg_help_or_unknown() {
|
|
local arg="$1"
|
|
local usage_fn="${2:-usage}"
|
|
local err_fn="${3:-err}"
|
|
|
|
case "$arg" in
|
|
-h | --help)
|
|
"$usage_fn"
|
|
exit 0
|
|
;;
|
|
-*)
|
|
"$err_fn" "Unknown argument: $arg"
|
|
"$usage_fn"
|
|
exit 2
|
|
;;
|
|
*)
|
|
return 1 # Not a flag, let caller handle it
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Initialize a setup script with common boilerplate
|
|
# Usage: init_setup_script "Script Title" "$@"
|
|
# This combines: parse_interactive_args, shift, require_root, print_setup_header
|
|
init_setup_script() {
|
|
local title="$1"
|
|
shift
|
|
parse_interactive_args "$@"
|
|
shift "$COMMON_ARGS_SHIFT"
|
|
require_root "$@"
|
|
print_setup_header "$title"
|
|
}
|
|
|
|
# =============================================================================
|
|
# FOCUS APP DETECTION (for digital wellbeing scripts)
|
|
# =============================================================================
|
|
|
|
# Default focus apps - can be overridden before calling is_focus_app_running
|
|
FOCUS_APPS_WINDOWS=(
|
|
"Visual Studio Code"
|
|
"VSCodium"
|
|
"Cursor"
|
|
"IntelliJ IDEA"
|
|
"PyCharm"
|
|
"WebStorm"
|
|
"CLion"
|
|
"Rider"
|
|
"Sublime Text"
|
|
"Blender"
|
|
"Godot"
|
|
"Unity"
|
|
"Unreal Editor"
|
|
)
|
|
|
|
FOCUS_APPS_PROCESSES=(
|
|
"steam_app_"
|
|
"gamescope"
|
|
)
|
|
|
|
# Check if any focus app is running (window-based detection)
|
|
# Returns 0 if focus app found, 1 otherwise
|
|
# Echoes the name of the found app
|
|
is_focus_app_running() {
|
|
# One xdotool call with a combined regex instead of N separate calls
|
|
if command -v xdotool &>/dev/null && [[ ${#FOCUS_APPS_WINDOWS[@]} -gt 0 ]]; then
|
|
local regex wid
|
|
printf -v regex '%s|' "${FOCUS_APPS_WINDOWS[@]}"
|
|
regex="${regex%|}" # strip trailing |
|
|
while IFS= read -r wid; do
|
|
[[ -n $wid ]] || continue
|
|
echo "focus app"
|
|
return 0
|
|
done < <(xdotool search --name "$regex" 2>/dev/null)
|
|
fi
|
|
|
|
# Check specific processes via /proc (no fork)
|
|
local app comm
|
|
for app in "${FOCUS_APPS_PROCESSES[@]}"; do
|
|
for comm in /proc/[0-9]*/comm; do
|
|
[[ -r $comm ]] || continue
|
|
read -r _proc_comm <"$comm" 2>/dev/null || continue
|
|
if [[ $_proc_comm == *"$app"* ]]; then
|
|
echo "$_proc_comm"
|
|
return 0
|
|
fi
|
|
done
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
# =============================================================================
|
|
# COMMAND AVAILABILITY
|
|
# =============================================================================
|
|
|
|
# Check if a command exists
|
|
# Usage: if require_command ffmpeg; then ...
|
|
require_command() {
|
|
local cmd="$1"
|
|
local pkg="${2:-$1}"
|
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
echo "Error: '$cmd' is not installed or not in PATH." >&2
|
|
echo "Install with: sudo pacman -S $pkg" >&2
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Check for ImageMagick and display helpful installation message
|
|
# Usage: require_imagemagick [optional: "magick" or "convert"]
|
|
# Returns: Sets MAGICK_CMD variable to available command
|
|
require_imagemagick() {
|
|
local preferred="${1:-}"
|
|
|
|
if [[ $preferred == "magick" ]] || [[ -z $preferred ]]; then
|
|
if command -v magick &>/dev/null; then
|
|
MAGICK_CMD="magick"
|
|
export MAGICK_CMD
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
if [[ $preferred == "convert" ]] || [[ -z $preferred ]]; then
|
|
if command -v convert &>/dev/null; then
|
|
MAGICK_CMD="convert"
|
|
export MAGICK_CMD
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
echo "Error: ImageMagick is not installed." >&2
|
|
echo "Install it with:" >&2
|
|
echo " Arch Linux: sudo pacman -S imagemagick" >&2
|
|
echo " Ubuntu/Debian: sudo apt install imagemagick" >&2
|
|
return 1
|
|
}
|
|
|
|
# Install missing pacman packages
|
|
# Usage: install_missing_pacman_packages pkg1 pkg2 pkg3 ...
|
|
# Returns 0 if all packages installed successfully, 1 otherwise
|
|
install_missing_pacman_packages() {
|
|
local packages=("$@")
|
|
local missing=()
|
|
|
|
for pkg in "${packages[@]}"; do
|
|
if ! pacman -Qi "$pkg" >/dev/null 2>&1; then
|
|
missing+=("$pkg")
|
|
fi
|
|
done
|
|
|
|
if [[ ${#missing[@]} -eq 0 ]]; then
|
|
echo "[INFO] All required packages are already installed."
|
|
return 0
|
|
fi
|
|
|
|
echo "[INFO] Installing missing packages: ${missing[*]}"
|
|
if ! sudo pacman -S --needed --noconfirm "${missing[@]}"; then
|
|
echo "[ERROR] Failed to install packages" >&2
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# =============================================================================
|
|
# NOTIFICATION
|
|
# =============================================================================
|
|
|
|
# Send desktop notification (fails silently if notify-send not available)
|
|
# Usage: notify "Title" "Message" [urgency: low/normal/critical] [timeout_ms]
|
|
notify() {
|
|
local title="$1"
|
|
local message="$2"
|
|
local urgency="${3:-normal}"
|
|
local timeout="${4:-5000}"
|
|
|
|
if command -v notify-send &>/dev/null; then
|
|
notify-send -u "$urgency" -t "$timeout" "$title" "$message" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# FILE/PATH UTILITIES
|
|
# =============================================================================
|
|
|
|
# Get the directory containing the calling script
|
|
# Usage: SCRIPT_DIR=$(get_script_dir)
|
|
get_script_dir() {
|
|
dirname "$(readlink -f "${BASH_SOURCE[1]:-$0}")"
|
|
}
|
|
|
|
# Ensure a directory exists
|
|
# Usage: ensure_dir "/path/to/dir"
|
|
ensure_dir() {
|
|
local dir="$1"
|
|
if [[ ! -d $dir ]]; then
|
|
mkdir -p "$dir"
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# SYSTEMD HELPERS
|
|
# =============================================================================
|
|
|
|
# Internal helper for running systemctl with optional --user flag
|
|
_systemctl_cmd() {
|
|
local user_flag="$1"
|
|
shift
|
|
if [[ $user_flag == "--user" ]]; then
|
|
systemctl --user "$@"
|
|
else
|
|
systemctl "$@"
|
|
fi
|
|
}
|
|
|
|
# Enable and start a systemd service (user or system)
|
|
# Usage: enable_service "service-name" [--user]
|
|
enable_service() {
|
|
local service="$1"
|
|
local user_flag="${2:-}"
|
|
_systemctl_cmd "$user_flag" daemon-reload
|
|
_systemctl_cmd "$user_flag" enable --now "$service"
|
|
}
|
|
|
|
# Check if a systemd service is active
|
|
# Usage: if is_service_active "service-name" [--user]; then ...
|
|
is_service_active() {
|
|
_systemctl_cmd "${2:-}" is-active --quiet "$1"
|
|
}
|
|
|
|
# Check if a systemd service is enabled
|
|
# Usage: if is_service_enabled "service-name" [--user]; then ...
|
|
is_service_enabled() {
|
|
_systemctl_cmd "${2:-}" is-enabled --quiet "$1" 2>/dev/null
|
|
}
|
|
|
|
# =============================================================================
|
|
# COLORED LOGGING (for scripts that need colored output)
|
|
# =============================================================================
|
|
|
|
# ANSI color codes
|
|
declare -g COLOR_RED='\033[1;31m'
|
|
declare -g COLOR_GREEN='\033[1;32m'
|
|
declare -g COLOR_YELLOW='\033[1;33m'
|
|
declare -g COLOR_BLUE='\033[1;34m'
|
|
declare -g COLOR_NC='\033[0m'
|
|
|
|
log_info() {
|
|
printf "${COLOR_BLUE}[INFO]${COLOR_NC} %s\n" "$*"
|
|
}
|
|
|
|
log_ok() {
|
|
printf "${COLOR_GREEN}[ OK ]${COLOR_NC} %s\n" "$*"
|
|
}
|
|
|
|
log_warn() {
|
|
printf "${COLOR_YELLOW}[WARN]${COLOR_NC} %s\n" "$*" >&2
|
|
}
|
|
|
|
log_error() {
|
|
printf "${COLOR_RED}[ERROR]${COLOR_NC} %s\n" "$*" >&2
|
|
}
|
|
|
|
# Alias for compatibility
|
|
warn() { log_warn "$@"; }
|
|
err() { log_error "$@"; }
|
|
|
|
# =============================================================================
|
|
# EFFICIENT TIME FUNCTIONS (zero-fork bash builtins)
|
|
# =============================================================================
|
|
# These functions use printf '%(...)'T' bash builtin (NO external commands)
|
|
# to avoid fork-storm anti-patterns in polling scripts.
|
|
# See: .github/skills/efficient-polling-scripts/SKILL.md
|
|
|
|
# Get current Unix timestamp (seconds since epoch)
|
|
# Usage: ts=$(get_timestamp)
|
|
# FORK-FREE: uses bash builtin printf %s (sec_since_epoch)
|
|
get_timestamp() {
|
|
printf '%(%s)T' -1
|
|
}
|
|
|
|
# Get current date in YYYY-MM-DD format
|
|
# Usage: date=$(get_date)
|
|
get_date() {
|
|
printf '%(%Y-%m-%d)T' -1
|
|
}
|
|
|
|
# Get current time in HH:MM:SS format
|
|
# Usage: time=$(get_time)
|
|
get_time() {
|
|
printf '%(%H:%M:%S)T' -1
|
|
}
|
|
|
|
# Get current date-time in YYYY-MM-DD HH:MM:SS format
|
|
# Usage: dt=$(get_datetime)
|
|
get_datetime() {
|
|
printf '%(%Y-%m-%d %H:%M:%S)T' -1
|
|
}
|
|
|
|
# Get day of week (1=Monday, 7=Sunday)
|
|
# Usage: dow=$(get_day_of_week)
|
|
get_day_of_week() {
|
|
printf '%(%u)T' -1
|
|
}
|
|
|
|
# Get day name (Monday, Tuesday, ...)
|
|
# Usage: day=$(get_day_name)
|
|
get_day_name() {
|
|
printf '%(%A)T' -1
|
|
}
|
|
|
|
# Get current hour (00-23)
|
|
# Usage: hour=$(get_hour)
|
|
get_hour() {
|
|
printf '%(%H)T' -1
|
|
}
|
|
|
|
# Get current minute (00-59)
|
|
# Usage: minute=$(get_minute)
|
|
get_minute() {
|
|
printf '%(%M)T' -1
|
|
}
|
|
|
|
# Get current second (00-59)
|
|
# Usage: second=$(get_second)
|
|
get_second() {
|
|
printf '%(%S)T' -1
|
|
}
|
|
|
|
# Get Unix timestamp from boot (uptime in seconds)
|
|
# Usage: boot_seconds=$(get_uptime_seconds)
|
|
get_uptime_seconds() {
|
|
read -r uptime_with_fraction _ </proc/uptime
|
|
printf '%.*f\n' 0 "$uptime_with_fraction"
|
|
}
|
|
|
|
# Get boot time in YYYY-MM-DD HH:MM:SS format
|
|
# Usage: boot_time=$(get_boot_datetime)
|
|
# Calculates: current_time - uptime_seconds
|
|
get_boot_datetime() {
|
|
local uptime_seconds
|
|
uptime_seconds=$(get_uptime_seconds)
|
|
local boot_ts=$(($(get_timestamp) - uptime_seconds))
|
|
printf '%(%Y-%m-%d %H:%M:%S)T' "$boot_ts"
|
|
}
|
|
|
|
# Get boot time date only (YYYY-MM-DD)
|
|
# Usage: boot_date=$(get_boot_date)
|
|
get_boot_date() {
|
|
local uptime_seconds
|
|
uptime_seconds=$(get_uptime_seconds)
|
|
local boot_ts=$(($(get_timestamp) - uptime_seconds))
|
|
printf '%(%Y-%m-%d)T' "$boot_ts"
|
|
}
|
|
|
|
# Get boot time hour only (00-23)
|
|
# Usage: boot_hour=$(get_boot_hour)
|
|
get_boot_hour() {
|
|
local uptime_seconds
|
|
uptime_seconds=$(get_uptime_seconds)
|
|
local boot_ts=$(($(get_timestamp) - uptime_seconds))
|
|
printf '%(%H)T' "$boot_ts"
|
|
}
|
|
|
|
# Check if current time is within a given hour range
|
|
# Usage: if is_hour_in_range 5 8; then ... # 5AM-8AM
|
|
is_hour_in_range() {
|
|
local start_hour=$1
|
|
local end_hour=$2
|
|
local current_hour
|
|
current_hour=$(get_hour)
|
|
local current_hour_num=$((10#$current_hour))
|
|
[[ $current_hour_num -ge $start_hour ]] && [[ $current_hour_num -lt $end_hour ]]
|
|
}
|
|
|
|
# Check if current day is a specific day of week
|
|
# Usage: if is_day_of_week 1 5 6 7; then ... # Monday, Friday, Saturday, Sunday
|
|
is_day_of_week() {
|
|
local target_day
|
|
target_day=$(get_day_of_week)
|
|
for day in "$@"; do
|
|
[[ $target_day -eq $day ]] && return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# =============================================================================
|
|
# INTERACTIVE PROMPTS
|
|
# =============================================================================
|
|
|
|
# Ask yes/no question, returns 0 for yes, 1 for no
|
|
# Usage: if ask_yes_no "Continue?"; then ...
|
|
ask_yes_no() {
|
|
local prompt="$1"
|
|
local ans
|
|
read -r -p "$prompt [y/N]: " ans || true
|
|
case "${ans:-}" in
|
|
y | Y | yes | YES) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# Check if a command is available
|
|
# Usage: if has_cmd git; then ...
|
|
has_cmd() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
# =============================================================================
|
|
# STANDARD SETUP HEADER
|
|
# =============================================================================
|
|
|
|
# Print a standard setup header for scripts
|
|
# Usage: print_setup_header "Script Name"
|
|
print_setup_header() {
|
|
local title="$1"
|
|
local current_datetime
|
|
current_datetime=$(get_datetime)
|
|
echo "$title"
|
|
printf '=%.0s' $(seq 1 ${#title})
|
|
echo ""
|
|
echo "Current Date: $current_datetime"
|
|
echo "User: $USER"
|
|
echo "Original user: $(get_actual_user)"
|
|
if [[ $INTERACTIVE_MODE == "true" ]]; then
|
|
echo "Mode: Interactive (prompts enabled)"
|
|
else
|
|
echo "Mode: Automatic (auto-yes, use --interactive for prompts)"
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# MOUNT/UNMOUNT HELPERS (for hosts guard and similar)
|
|
# =============================================================================
|
|
|
|
# Count mount layers for a path
|
|
# Usage: count=$(mount_layers_count "/etc/hosts")
|
|
mount_layers_count() {
|
|
local target="$1"
|
|
awk -v t="$target" '$5==t{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0
|
|
}
|
|
|
|
# Collapse all bind mount layers for a path
|
|
# Usage: collapse_mounts "/etc/hosts" [max_iterations]
|
|
collapse_mounts() {
|
|
local target="$1"
|
|
local max_iter="${2:-20}"
|
|
local i=0
|
|
|
|
if has_cmd mountpoint; then
|
|
while mountpoint -q "$target"; do
|
|
umount -l "$target" >/dev/null 2>&1 || break
|
|
i=$((i + 1))
|
|
((i >= max_iter)) && break
|
|
done
|
|
else
|
|
local cnt
|
|
cnt=$(mount_layers_count "$target")
|
|
while ((cnt > 1)); do
|
|
umount -l "$target" >/dev/null 2>&1 || break
|
|
i=$((i + 1))
|
|
((i >= max_iter)) && break
|
|
cnt=$(mount_layers_count "$target")
|
|
done
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# RESOLUTION/FORMAT VALIDATION
|
|
# =============================================================================
|
|
|
|
# Validate resolution format (WIDTHxHEIGHT)
|
|
# Usage: if validate_resolution "1920x1080"; then ...
|
|
validate_resolution() {
|
|
local res="$1"
|
|
[[ $res =~ ^[0-9]+x[0-9]+$ ]]
|
|
}
|
|
|
|
# Generate output filename with suffix
|
|
# Usage: output=$(generate_output_filename "input.jpg" "_resized")
|
|
generate_output_filename() {
|
|
local input="$1"
|
|
local suffix="$2"
|
|
local ext="${3:-}"
|
|
|
|
local basename dirname filename extension
|
|
basename=$(basename "$input")
|
|
dirname=$(dirname "$input")
|
|
filename="${basename%.*}"
|
|
extension="${basename##*.}"
|
|
|
|
# Handle files without extension
|
|
if [[ $filename == "$extension" ]]; then
|
|
extension="${ext:-jpg}"
|
|
fi
|
|
|
|
echo "${dirname}/${filename}${suffix}.${extension}"
|
|
}
|