Files
System-Sync-Restore/lib/common.sh
T
Oliver Surke 69541209dc Fix ssr::confirm using bash 4+ lowercase operator on bash 3.2
macOS ships /bin/bash 3.2.57 which has no `${var,,}` parameter expansion.
Replace with a portable case statement matching y/Y. Behaviorally
identical (read -n 1 already constrains input to a single char, and the
original code only accepted 'y' after lowercasing, so 'y' and 'Y' are
the only accepting inputs).

Repo-wide audit for other bash 4+ features (case modification, mapfile,
nameref locals, ${var@op} transforms, coproc) returned clean.

Repro before fix:
  $ ./install.sh
  Enable daily scheduled sync (launchd)? [y/N]: y
  lib/common.sh: line 52: ${reply,,}: bad substitution

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 15:42:20 +02:00

84 lines
2.9 KiB
Bash

#!/usr/bin/env bash
# lib/common.sh — shared helpers (logging, errors, validation).
# Sourced by all ssr commands. Never executed directly.
set -euo pipefail
# Resolve repo root from any sourced location.
SSR_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SSR_ROOT="$(cd "${SSR_LIB_DIR}/.." && pwd)"
export SSR_ROOT SSR_LIB_DIR
# Colors only when stdout is a TTY (no ANSI in launchd logs).
if [[ -t 1 ]]; then
SSR_C_RED=$'\033[31m'; SSR_C_GRN=$'\033[32m'
SSR_C_YLW=$'\033[33m'; SSR_C_BLU=$'\033[34m'
SSR_C_DIM=$'\033[2m'; SSR_C_RST=$'\033[0m'
else
SSR_C_RED=""; SSR_C_GRN=""; SSR_C_YLW=""; SSR_C_BLU=""; SSR_C_DIM=""; SSR_C_RST=""
fi
ssr::ts() { date '+%Y-%m-%dT%H:%M:%S%z'; }
ssr::log() { printf '%s %s[INFO]%s %s\n' "$(ssr::ts)" "$SSR_C_BLU" "$SSR_C_RST" "$*"; }
ssr::ok() { printf '%s %s[OK]%s %s\n' "$(ssr::ts)" "$SSR_C_GRN" "$SSR_C_RST" "$*"; }
ssr::warn() { printf '%s %s[WARN]%s %s\n' "$(ssr::ts)" "$SSR_C_YLW" "$SSR_C_RST" "$*" >&2; }
ssr::err() { printf '%s %s[ERR]%s %s\n' "$(ssr::ts)" "$SSR_C_RED" "$SSR_C_RST" "$*" >&2; }
ssr::die() { ssr::err "$@"; exit 1; }
# Mirror to syslog if available so launchd runs are findable via `log show`.
ssr::syslog() {
[[ -x /usr/bin/logger ]] || return 0
/usr/bin/logger -t "ssr" -- "$*" || true
}
ssr::require_macos() {
[[ "$(uname -s)" == "Darwin" ]] || ssr::die "ssr requires macOS (Darwin). Detected: $(uname -s)"
}
ssr::require_cmd() {
command -v "$1" >/dev/null 2>&1 || ssr::die "Required command missing: $1"
}
# Confirm prompt with optional timeout (returns 0 = yes).
ssr::confirm() {
local prompt="${1:-Continue?}" timeout="${2:-0}" reply
if [[ "$timeout" -gt 0 ]]; then
read -r -t "$timeout" -n 1 -p "${prompt} [y/N]: " reply || reply=""
else
read -r -n 1 -p "${prompt} [y/N]: " reply || reply=""
fi
echo
case "$reply" in y|Y) return 0 ;; *) return 1 ;; esac
}
# Validate a path is safe (no newlines, no `..` traversal). Used before any FS op.
# Note: bash strings cannot hold NUL bytes, so no separate NUL check needed.
ssr::validate_path() {
local p="$1"
[[ -n "$p" ]] || ssr::die "Empty path"
case "$p" in
*$'\n'*) ssr::die "Path contains newline: $p" ;;
*..*) ssr::die "Path traversal rejected: $p" ;;
esac
}
ssr::ensure_dir() {
local d="$1"
ssr::validate_path "$d"
[[ -d "$d" ]] || mkdir -p "$d"
}
# Source config file if it exists. Validates that only known KEY=VALUE lines
# are present to avoid arbitrary code execution from a tampered config.
ssr::load_config() {
local cfg="${1:-${SSR_CONFIG:-$HOME/.config/ssr/ssr.conf}}"
export SSR_CONFIG="$cfg"
[[ -f "$cfg" ]] || return 0
if grep -Eq -v '^[[:space:]]*(#|$|[A-Z_][A-Z0-9_]*=)' "$cfg"; then
ssr::die "Config $cfg contains invalid lines (only KEY=VALUE allowed)"
fi
# shellcheck disable=SC1090
source "$cfg"
}