a1d66a700d
* 引入终端同步更新转义序列以减少屏幕闪烁 * 重构缓冲区机制,区分普通数据与传输数据缓存 * 新增前缀匹配算法函数用于处理跨分段的标记位识别 * 完善同步状态机逻辑,实现对同步更新区域的精确追踪 * 优化输入流读取与分发流程,提升终端输出稳定性
1255 lines
42 KiB
Bash
1255 lines
42 KiB
Bash
#!/usr/bin/env bash
|
||
# Homebrew 智能升级脚本(失败隔离、选择性重试与性能优化版)
|
||
|
||
set -euo pipefail
|
||
|
||
prepend_path_once() {
|
||
local dir="$1"
|
||
[[ -d "$dir" ]] || return 0
|
||
case ":$PATH:" in
|
||
*":$dir:"*) ;;
|
||
*) PATH="$dir:$PATH" ;;
|
||
esac
|
||
}
|
||
|
||
prepend_path_once "/usr/local/sbin"
|
||
prepend_path_once "/opt/homebrew/sbin"
|
||
export PATH
|
||
|
||
# 禁止 Homebrew 在每个安装步骤后自动清理;统一在最后一步彻底清理。
|
||
export HOMEBREW_NO_INSTALL_CLEANUP=1
|
||
# brewup 本身就是明确的批量升级操作;关闭 Homebrew 的 Ask 模式,
|
||
# 等价于对“Do you want to proceed with the upgrade?”自动回答 y。
|
||
export HOMEBREW_NO_ASK=1
|
||
|
||
hex_channel_to_16bit() {
|
||
local hex="$1" value maximum bits
|
||
|
||
[[ "$hex" =~ ^[[:xdigit:]]{1,4}$ ]] || return 1
|
||
value=$((16#$hex))
|
||
bits=$((${#hex} * 4))
|
||
maximum=$(((1 << bits) - 1))
|
||
printf '%s' "$((value * 65535 / maximum))"
|
||
}
|
||
|
||
query_terminal_background_rgb() {
|
||
local response="" red_hex green_hex blue_hex red green blue
|
||
|
||
[[ -r /dev/tty && -w /dev/tty ]] || return 1
|
||
command -v stty >/dev/null 2>&1 || return 1
|
||
command -v dd >/dev/null 2>&1 || return 1
|
||
|
||
# OSC 11 是跨终端的背景色查询协议。使用子 shell 确保任何退出路径都会
|
||
# 恢复 TTY 设置;短暂无输入超时也避免不支持该协议的终端阻塞流程。
|
||
response="$({
|
||
saved_tty="$(stty -g < /dev/tty 2>/dev/null)" || exit 1
|
||
trap 'stty "$saved_tty" < /dev/tty 2>/dev/null || true' EXIT HUP INT TERM
|
||
stty -echo -icanon min 0 time 3 < /dev/tty 2>/dev/null || exit 1
|
||
printf '\033]11;?\007' > /dev/tty
|
||
LC_ALL=C dd bs=1 count=128 < /dev/tty 2>/dev/null || true
|
||
} 2>/dev/null)"
|
||
|
||
if [[ "$response" =~ rgb:([[:xdigit:]]{1,4})/([[:xdigit:]]{1,4})/([[:xdigit:]]{1,4}) ]]; then
|
||
red_hex="${BASH_REMATCH[1]}"
|
||
green_hex="${BASH_REMATCH[2]}"
|
||
blue_hex="${BASH_REMATCH[3]}"
|
||
red="$(hex_channel_to_16bit "$red_hex")" || return 1
|
||
green="$(hex_channel_to_16bit "$green_hex")" || return 1
|
||
blue="$(hex_channel_to_16bit "$blue_hex")" || return 1
|
||
printf '%s %s %s' "$red" "$green" "$blue"
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
lowercase() {
|
||
printf '%s' "$1" | tr '[:upper:]' '[:lower:]'
|
||
}
|
||
|
||
detect_apple_appearance_theme() {
|
||
local appearance="" product_name=""
|
||
|
||
[[ "$(uname -s 2>/dev/null || true)" == "Darwin" ]] || return 1
|
||
command -v defaults >/dev/null 2>&1 || return 1
|
||
|
||
product_name="$(sw_vers -productName 2>/dev/null || true)"
|
||
appearance="$(defaults read -g AppleInterfaceStyle 2>/dev/null || true)"
|
||
if [[ -z "$appearance" && \
|
||
("$product_name" == *"iPhone"* || "$product_name" == *"iOS"* || "$product_name" == *"iPad"*) ]]; then
|
||
appearance="$(defaults read /var/mobile/Library/Preferences/.GlobalPreferences AppleInterfaceStyle 2>/dev/null || true)"
|
||
fi
|
||
case "$appearance" in
|
||
Dark|dark) printf 'dark' ;;
|
||
Light|light) printf 'light' ;;
|
||
'')
|
||
# macOS 中该键不存在代表浅色;iOS/iPadOS 无权读取时交给终端检测。
|
||
if [[ "$product_name" == *"iPhone"* || "$product_name" == *"iOS"* || "$product_name" == *"iPad"* ]]; then
|
||
return 1
|
||
fi
|
||
printf 'light'
|
||
;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
detect_windows_appearance_theme() {
|
||
local powershell="" appearance=""
|
||
|
||
if command -v powershell.exe >/dev/null 2>&1; then
|
||
powershell="powershell.exe"
|
||
elif command -v powershell >/dev/null 2>&1; then
|
||
powershell="powershell"
|
||
fi
|
||
[[ -n "$powershell" ]] || return 1
|
||
|
||
# shellcheck disable=SC2016 # The single-quoted variables belong to PowerShell.
|
||
appearance="$($powershell -NoLogo -NoProfile -NonInteractive -Command \
|
||
'$theme = Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" -ErrorAction SilentlyContinue; if ($null -ne $theme.SystemUsesLightTheme) { [Console]::Write($theme.SystemUsesLightTheme) } elseif ($null -ne $theme.AppsUseLightTheme) { [Console]::Write($theme.AppsUseLightTheme) }' \
|
||
2>/dev/null || true)"
|
||
appearance="${appearance//$'\r'/}"
|
||
appearance="${appearance//[[:space:]]/}"
|
||
case "$appearance" in
|
||
0) printf 'dark' ;;
|
||
1) printf 'light' ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
theme_from_rgb_255() {
|
||
local value="$1" red green blue brightness
|
||
|
||
value="${value//,/ }"
|
||
read -r red green blue <<< "$value"
|
||
if [[ "$red" =~ ^[0-9]+$ && "$green" =~ ^[0-9]+$ && "$blue" =~ ^[0-9]+$ ]] && \
|
||
((red <= 255 && green <= 255 && blue <= 255)); then
|
||
brightness=$(((red * 299 + green * 587 + blue * 114) / 1000))
|
||
if ((brightness >= 128)); then
|
||
printf 'light'
|
||
else
|
||
printf 'dark'
|
||
fi
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
detect_linux_appearance_theme() {
|
||
local setting="" setting_lower="" theme_name="" theme_name_lower=""
|
||
local kde_reader="" background=""
|
||
|
||
# 无图形会话时,gsettings 可能只返回默认值而非真实主题;此时应让
|
||
# SSH/容器继续使用终端背景检测。
|
||
if [[ -z "${XDG_CURRENT_DESKTOP:-}" && -z "${DESKTOP_SESSION:-}" && \
|
||
-z "${DISPLAY:-}" && -z "${WAYLAND_DISPLAY:-}" && \
|
||
-z "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
# freedesktop Appearance Portal:GNOME、KDE 和多数 Wayland 桌面的统一接口。
|
||
if command -v gdbus >/dev/null 2>&1; then
|
||
setting="$(gdbus call --session \
|
||
--dest org.freedesktop.portal.Desktop \
|
||
--object-path /org/freedesktop/portal/desktop \
|
||
--method org.freedesktop.portal.Settings.Read \
|
||
org.freedesktop.appearance color-scheme 2>/dev/null || true)"
|
||
case "$setting" in
|
||
*'uint32 1'*) printf 'dark'; return ;;
|
||
*'uint32 2'*) printf 'light'; return ;;
|
||
esac
|
||
fi
|
||
|
||
# GNOME 42+ / Cinnamon 以及兼容 gsettings 的 GTK 桌面。
|
||
if command -v gsettings >/dev/null 2>&1; then
|
||
setting="$(gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null || true)"
|
||
setting_lower="$(lowercase "$setting")"
|
||
case "$setting_lower" in
|
||
*prefer-dark*) printf 'dark'; return ;;
|
||
*prefer-light*) printf 'light'; return ;;
|
||
esac
|
||
theme_name="$(gsettings get org.gnome.desktop.interface gtk-theme 2>/dev/null || true)"
|
||
if [[ -z "$theme_name" ]]; then
|
||
theme_name="$(gsettings get org.cinnamon.desktop.interface gtk-theme 2>/dev/null || true)"
|
||
fi
|
||
if [[ -n "$theme_name" ]]; then
|
||
theme_name_lower="$(lowercase "$theme_name")"
|
||
if [[ "$theme_name_lower" == *dark* ]]; then
|
||
printf 'dark'
|
||
else
|
||
printf 'light'
|
||
fi
|
||
return
|
||
fi
|
||
fi
|
||
|
||
# KDE Plasma 6/5:先看配色方案名,再读取窗口背景色作判断。
|
||
if command -v kreadconfig6 >/dev/null 2>&1; then
|
||
kde_reader="kreadconfig6"
|
||
elif command -v kreadconfig5 >/dev/null 2>&1; then
|
||
kde_reader="kreadconfig5"
|
||
fi
|
||
if [[ -n "$kde_reader" ]]; then
|
||
theme_name="$($kde_reader --file kdeglobals --group General --key ColorScheme 2>/dev/null || true)"
|
||
theme_name_lower="$(lowercase "$theme_name")"
|
||
case "$theme_name_lower" in
|
||
*dark*) printf 'dark'; return ;;
|
||
*light*) printf 'light'; return ;;
|
||
esac
|
||
background="$($kde_reader --file kdeglobals --group 'Colors:Window' --key BackgroundNormal 2>/dev/null || true)"
|
||
if [[ -n "$background" ]]; then
|
||
theme_from_rgb_255 "$background" && return
|
||
fi
|
||
fi
|
||
|
||
# XFCE 的主题名通常直接包含 Dark;存在非 Dark 主题时按浅色处理。
|
||
if command -v xfconf-query >/dev/null 2>&1; then
|
||
theme_name="$(xfconf-query -c xsettings -p /Net/ThemeName 2>/dev/null || true)"
|
||
if [[ -n "$theme_name" ]]; then
|
||
theme_name_lower="$(lowercase "$theme_name")"
|
||
if [[ "$theme_name_lower" == *dark* ]]; then
|
||
printf 'dark'
|
||
else
|
||
printf 'light'
|
||
fi
|
||
return
|
||
fi
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
detect_android_appearance_theme() {
|
||
local night_mode="" night_mode_lower=""
|
||
|
||
command -v cmd >/dev/null 2>&1 || return 1
|
||
night_mode="$(cmd uimode night 2>/dev/null || true)"
|
||
night_mode_lower="$(lowercase "$night_mode")"
|
||
case "$night_mode_lower" in
|
||
*'night mode: yes'*) printf 'dark' ;;
|
||
*'night mode: no'*) printf 'light' ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
detect_system_appearance_theme() {
|
||
local kernel
|
||
|
||
kernel="$(uname -s 2>/dev/null || true)"
|
||
case "$kernel" in
|
||
Darwin)
|
||
detect_apple_appearance_theme
|
||
;;
|
||
Linux)
|
||
if [[ -n "${WSL_DISTRO_NAME:-}" || "$(uname -r 2>/dev/null || true)" == *[Mm]icrosoft* ]]; then
|
||
detect_windows_appearance_theme && return
|
||
fi
|
||
if [[ -n "${ANDROID_ROOT:-}" || -n "${TERMUX_VERSION:-}" ]]; then
|
||
detect_android_appearance_theme && return
|
||
fi
|
||
detect_linux_appearance_theme
|
||
;;
|
||
MINGW*|MSYS*|CYGWIN*)
|
||
detect_windows_appearance_theme
|
||
;;
|
||
FreeBSD|OpenBSD|NetBSD|DragonFly|SunOS)
|
||
# BSD/Unix 桌面也可能提供 freedesktop、GTK、KDE 或 XFCE 接口。
|
||
detect_linux_appearance_theme
|
||
;;
|
||
*)
|
||
return 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
detect_color_theme() {
|
||
local requested="${HB_COLOR_THEME:-auto}" background_index="" rgb=""
|
||
local red green blue brightness system_theme=""
|
||
|
||
case "$requested" in
|
||
dark|light)
|
||
printf '%s' "$requested"
|
||
return
|
||
;;
|
||
auto|'') ;;
|
||
*) requested=auto ;;
|
||
esac
|
||
|
||
# 本地运行优先跟随操作系统外观。SSH 中远端系统状态不代表客户端
|
||
# (例如 iOS/Android 上的 Termius),因此直接交给后面的终端背景检测。
|
||
if [[ -z "${SSH_TTY:-}" && -z "${SSH_CONNECTION:-}" && -z "${SSH_CLIENT:-}" ]]; then
|
||
system_theme="$(detect_system_appearance_theme || true)"
|
||
if [[ "$system_theme" == "dark" || "$system_theme" == "light" ]]; then
|
||
printf '%s' "$system_theme"
|
||
return
|
||
fi
|
||
fi
|
||
|
||
# 系统状态读取失败时,再查询终端自身提供的背景信息。
|
||
# COLORFGBG 的最后一项是背景色索引,常见终端会主动提供。
|
||
if [[ -n "${COLORFGBG:-}" ]]; then
|
||
background_index="${COLORFGBG##*[;:]}"
|
||
if [[ "$background_index" =~ ^[0-9]+$ ]]; then
|
||
if ((background_index == 0 || background_index == 8 || background_index < 7)); then
|
||
printf 'dark'
|
||
else
|
||
printf 'light'
|
||
fi
|
||
return
|
||
fi
|
||
fi
|
||
|
||
# Apple Terminal 不设置 COLORFGBG,但能返回当前标签页的实际背景色。
|
||
if [[ "${TERM_PROGRAM:-}" == "Apple_Terminal" ]] && command -v osascript >/dev/null 2>&1; then
|
||
rgb="$(osascript -e 'tell application "Terminal" to get background color of current settings of selected tab of front window' 2>/dev/null || true)"
|
||
rgb="${rgb//, / }"
|
||
read -r red green blue <<< "$rgb"
|
||
if [[ "$red" =~ ^[0-9]+$ && "$green" =~ ^[0-9]+$ && "$blue" =~ ^[0-9]+$ ]]; then
|
||
brightness=$(((red * 299 + green * 587 + blue * 114) / 1000))
|
||
if ((brightness >= 32768)); then
|
||
printf 'light'
|
||
else
|
||
printf 'dark'
|
||
fi
|
||
return
|
||
fi
|
||
fi
|
||
|
||
# iTerm2、Termius、Kitty、WezTerm、Alacritty 等终端通常支持 OSC 11;
|
||
# 它也可经由 SSH 返回客户端终端的真实背景色。
|
||
rgb="$(query_terminal_background_rgb || true)"
|
||
read -r red green blue <<< "$rgb"
|
||
if [[ "$red" =~ ^[0-9]+$ && "$green" =~ ^[0-9]+$ && "$blue" =~ ^[0-9]+$ ]]; then
|
||
brightness=$(((red * 299 + green * 587 + blue * 114) / 1000))
|
||
if ((brightness >= 32768)); then
|
||
printf 'light'
|
||
else
|
||
printf 'dark'
|
||
fi
|
||
return
|
||
fi
|
||
|
||
# 所有自动检测均不可用时默认深色,也可用 HB_COLOR_THEME 手动覆盖。
|
||
printf 'dark'
|
||
}
|
||
|
||
COLOR_THEME="none"
|
||
if [[ -t 1 ]]; then
|
||
COLOR_COUNT="$(tput colors 2>/dev/null || true)"
|
||
[[ "$COLOR_COUNT" =~ ^[0-9]+$ ]] || COLOR_COUNT=8
|
||
COLOR_THEME="$(detect_color_theme)"
|
||
if ((COLOR_COUNT >= 256)) && [[ "$COLOR_THEME" == "light" ]]; then
|
||
# 浅色背景使用更深的色阶,尤其避免亮黄、亮青在白底上失去对比。
|
||
RED='\033[38;5;124m'
|
||
GREEN='\033[38;5;28m'
|
||
YELLOW='\033[38;5;130m'
|
||
BLUE='\033[38;5;25m'
|
||
MAGENTA='\033[38;5;90m'
|
||
CYAN='\033[38;5;30m'
|
||
elif ((COLOR_COUNT >= 256)); then
|
||
# 深色背景使用偏亮的色阶,保持正文和状态信息清晰。
|
||
RED='\033[38;5;203m'
|
||
GREEN='\033[38;5;114m'
|
||
YELLOW='\033[38;5;221m'
|
||
BLUE='\033[38;5;75m'
|
||
MAGENTA='\033[38;5;177m'
|
||
CYAN='\033[38;5;80m'
|
||
elif [[ "$COLOR_THEME" == "light" ]]; then
|
||
RED='\033[31m'
|
||
GREEN='\033[32m'
|
||
YELLOW='\033[33m'
|
||
BLUE='\033[34m'
|
||
MAGENTA='\033[35m'
|
||
CYAN='\033[36m'
|
||
else
|
||
RED='\033[91m'
|
||
GREEN='\033[92m'
|
||
YELLOW='\033[93m'
|
||
BLUE='\033[94m'
|
||
MAGENTA='\033[95m'
|
||
CYAN='\033[96m'
|
||
fi
|
||
BOLD='\033[1m'
|
||
NC='\033[0m'
|
||
export HOMEBREW_COLOR=1
|
||
else
|
||
COLOR_COUNT=0
|
||
RED=''
|
||
GREEN=''
|
||
YELLOW=''
|
||
BLUE=''
|
||
MAGENTA=''
|
||
CYAN=''
|
||
BOLD=''
|
||
NC=''
|
||
fi
|
||
|
||
TERMINAL_WIDTH=""
|
||
ACTIVE_LOG_FILE=""
|
||
LOCK_ACQUIRED=0
|
||
LOCK_DIR="${HB_LOCK_DIR:-${TMPDIR:-/tmp}/brewup-$(id -u).lock}"
|
||
FAILED_FORMULAE=()
|
||
FAILED_CASKS=()
|
||
CLEANUP_FAILED=0
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
Usage: brew-upgrade-manager.sh [--width N]
|
||
|
||
Options:
|
||
--width N, --width=N Set output width (40-500).
|
||
-h, --help Show this help.
|
||
EOF
|
||
}
|
||
|
||
parse_args() {
|
||
while (($# > 0)); do
|
||
case "$1" in
|
||
--width)
|
||
[[ $# -ge 2 ]] || { print_error "--width requires a value."; exit 2; }
|
||
TERMINAL_WIDTH="$2"
|
||
shift 2
|
||
;;
|
||
--width=*)
|
||
TERMINAL_WIDTH="${1#*=}"
|
||
shift
|
||
;;
|
||
-h|--help)
|
||
usage
|
||
exit 0
|
||
;;
|
||
*)
|
||
print_error "Unknown argument: $1"
|
||
usage >&2
|
||
exit 2
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
resolve_terminal_width() {
|
||
if [[ -z "$TERMINAL_WIDTH" ]]; then
|
||
TERMINAL_WIDTH="${HB_TERMINAL_WIDTH:-}"
|
||
fi
|
||
if [[ -z "$TERMINAL_WIDTH" ]] && [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
|
||
TERMINAL_WIDTH="$(tput cols 2>/dev/null || true)"
|
||
fi
|
||
[[ -n "$TERMINAL_WIDTH" ]] || TERMINAL_WIDTH=130
|
||
|
||
if [[ ! "$TERMINAL_WIDTH" =~ ^[0-9]+$ ]] || ((TERMINAL_WIDTH < 40 || TERMINAL_WIDTH > 500)); then
|
||
print_error "terminal width must be an integer between 40 and 500."
|
||
exit 2
|
||
fi
|
||
export COLUMNS="$TERMINAL_WIDTH"
|
||
}
|
||
|
||
separator() {
|
||
local i
|
||
printf '%b' "$BLUE"
|
||
for ((i = 0; i < TERMINAL_WIDTH; i++)); do
|
||
printf '='
|
||
done
|
||
printf '%b\n' "$NC"
|
||
}
|
||
|
||
print_header() {
|
||
printf '%b%s%b\n' "$BLUE" "$1" "$NC"
|
||
}
|
||
|
||
print_error() {
|
||
printf '%bError:%b %s\n' "$RED" "$NC" "$*" >&2
|
||
}
|
||
|
||
print_warning() {
|
||
printf '%bWarning:%b %s\n' "$YELLOW" "$NC" "$*"
|
||
}
|
||
|
||
print_notice() {
|
||
printf '%b%s%b\n' "$YELLOW" "$*" "$NC"
|
||
}
|
||
|
||
print_success() {
|
||
printf '%b%s%b\n' "$GREEN" "$*" "$NC"
|
||
}
|
||
|
||
print_info() {
|
||
printf '%b%s%b\n' "$CYAN" "$*" "$NC"
|
||
}
|
||
|
||
print_colored_list() {
|
||
local label="$1" color="$2" items="$3" item
|
||
|
||
printf '%b%s%b\n' "$color" "$label" "$NC"
|
||
while IFS= read -r item; do
|
||
[[ -n "$item" ]] || continue
|
||
printf ' %b%s%b\n' "$color" "$item" "$NC"
|
||
done <<< "$items"
|
||
}
|
||
|
||
print_version_transition() {
|
||
local line="$1" indent left arrow new_version suffix old_version package
|
||
|
||
# Homebrew 常见版本行:
|
||
# package 1.0.0 -> 1.1.0
|
||
# 1.0.0 -> 1.1.0
|
||
# 包名、旧版本、方向和新版本分别着色,让变化能被快速识别。
|
||
if [[ "$line" =~ ^([[:space:]]*)(.*[^[:space:]])[[:space:]]+(\-\>|=\>)[[:space:]]+([^[:space:]]+)(.*)$ ]]; then
|
||
indent="${BASH_REMATCH[1]}"
|
||
left="${BASH_REMATCH[2]}"
|
||
arrow="${BASH_REMATCH[3]}"
|
||
new_version="${BASH_REMATCH[4]}"
|
||
suffix="${BASH_REMATCH[5]}"
|
||
old_version="${left##*[[:space:]]}"
|
||
package="${left%"$old_version"}"
|
||
|
||
printf '%s' "$indent"
|
||
if [[ -n "$package" ]]; then
|
||
printf '%b%s%b' "$BOLD$CYAN" "$package" "$NC"
|
||
fi
|
||
printf '%b%s%b %b%s%b %b%s%b%s\n' \
|
||
"$YELLOW" "$old_version" "$NC" \
|
||
"$BLUE" "$arrow" "$NC" \
|
||
"$BOLD$GREEN" "$new_version" "$NC" "$suffix"
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
colorize_brew_heading() {
|
||
local text="${1#==> }" action subject
|
||
|
||
case "$text" in
|
||
Upgrading\ *|Installing\ *|Reinstalling\ *|Fetching\ *|Downloading\ *|Pouring\ *|Would\ upgrade\ *)
|
||
action="${text%% *}"
|
||
subject="${text#* }"
|
||
printf '%b==>%b %b%s%b %b%s%b\n' \
|
||
"$BLUE" "$NC" "$CYAN" "$action" "$NC" "$BOLD$GREEN" "$subject" "$NC"
|
||
;;
|
||
Upgraded\ *|Installed\ *|Successfully\ *)
|
||
printf '%b==>%b %b%s%b\n' "$BLUE" "$NC" "$BOLD$GREEN" "$text" "$NC"
|
||
;;
|
||
Outdated\ *|Disabled\ *|Warning*)
|
||
printf '%b==>%b %b%s%b\n' "$BLUE" "$NC" "$YELLOW" "$text" "$NC"
|
||
;;
|
||
Formulae|Casks|New\ Formulae|New\ Casks|Caveats|Summary)
|
||
printf '%b==>%b %b%s%b\n' "$BLUE" "$NC" "$BOLD$MAGENTA" "$text" "$NC"
|
||
;;
|
||
*)
|
||
printf '%b==>%b %b%s%b\n' "$BLUE" "$NC" "$CYAN" "$text" "$NC"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
colorize_brew_output() {
|
||
local line
|
||
|
||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||
if print_version_transition "$line"; then
|
||
continue
|
||
fi
|
||
case "$line" in
|
||
'==> '*)
|
||
colorize_brew_heading "$line"
|
||
;;
|
||
Warning:*|warning:*)
|
||
printf '%b%s%b\n' "$YELLOW" "$line" "$NC"
|
||
;;
|
||
Error:*|error:*|Failed:*|failed:*)
|
||
printf '%b%s%b\n' "$RED" "$line" "$NC"
|
||
;;
|
||
Removing:*)
|
||
printf '%b%s%b\n' "$YELLOW" "$line" "$NC"
|
||
;;
|
||
'✔︎ '*|'✔ '*|'🍺 '*|*'was successfully upgraded!'|*'successfully installed!'|All\ dependencies\ satisfied.)
|
||
printf '%b%s%b\n' "$GREEN" "$line" "$NC"
|
||
;;
|
||
Already\ up-to-date.*|Updated\ Homebrew\ *|Updated\ *tap*|Successfully\ rebased\ *|Pruned\ *|This\ operation\ has\ freed\ *)
|
||
printf '%b%s%b\n' "$GREEN" "$line" "$NC"
|
||
;;
|
||
' Downloaded '*|' Verified '*)
|
||
printf '%b%s%b\n' "$GREEN" "$line" "$NC"
|
||
;;
|
||
*)
|
||
printf '%s\n' "$line"
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
can_stream_colorize_tty_output() {
|
||
[[ -t 1 && "$(uname -s 2>/dev/null || true)" == "Darwin" && \
|
||
-x /usr/bin/script && -x /usr/bin/perl ]]
|
||
}
|
||
|
||
colorize_brew_tty_stream() {
|
||
# `script` 将 brew 的输出传递给本函数时,普通行以 CRLF 结尾,进度条则以
|
||
# CR 重绘。按行的 Bash read 无法同时处理两者,会等到下一个换行才刷新进度。
|
||
# 这个小型 Perl 流式过滤器会立即透传 CR 更新,仅对完整的普通行重复使用
|
||
# 现有的语义配色规则。script(1) 读取控制终端时会临时关闭 ONLCR,
|
||
# 因此完整文本行必须显式输出 CRLF,不能依赖终端将 LF 自动转换为回车换行。
|
||
HB_STREAM_RED="$RED" \
|
||
HB_STREAM_GREEN="$GREEN" \
|
||
HB_STREAM_YELLOW="$YELLOW" \
|
||
HB_STREAM_BLUE="$BLUE" \
|
||
HB_STREAM_MAGENTA="$MAGENTA" \
|
||
HB_STREAM_CYAN="$CYAN" \
|
||
HB_STREAM_BOLD="$BOLD" \
|
||
HB_STREAM_NC="$NC" \
|
||
/usr/bin/perl -e '
|
||
use strict;
|
||
use warnings;
|
||
use utf8;
|
||
|
||
$| = 1;
|
||
my $line_ending = "\r\n";
|
||
|
||
my %color = map { $_ => ($ENV{"HB_STREAM_" . uc($_)} // "") }
|
||
qw(red green yellow blue magenta cyan bold nc);
|
||
for my $value (values %color) {
|
||
$value =~ s/\\033/\e/g;
|
||
$value =~ s/\\e/\e/g;
|
||
}
|
||
|
||
sub print_version_transition {
|
||
my ($line) = @_;
|
||
return 0 unless $line =~ /^(\s*)(.*\S)\s+(-\>|=\>)\s+(\S+)(.*)$/;
|
||
|
||
my ($indent, $left, $arrow, $new_version, $suffix) = ($1, $2, $3, $4, $5);
|
||
my ($old_version) = $left =~ /(\S+)$/;
|
||
my $package = substr($left, 0, length($left) - length($old_version));
|
||
|
||
print $indent;
|
||
print $color{bold}, $color{cyan}, $package, $color{nc} if length $package;
|
||
print $color{yellow}, $old_version, $color{nc}, " ";
|
||
print $color{blue}, $arrow, $color{nc}, " ";
|
||
print $color{bold}, $color{green}, $new_version, $color{nc}, $suffix, $line_ending;
|
||
return 1;
|
||
}
|
||
|
||
sub print_heading {
|
||
my ($line) = @_;
|
||
my $text = substr($line, 4);
|
||
|
||
if ($text =~ /^(Upgrading|Installing|Reinstalling|Fetching|Downloading|Pouring|Would)\s+(.+)$/) {
|
||
print $color{blue}, "==>", $color{nc}, " ";
|
||
print $color{cyan}, $1, $color{nc}, " ";
|
||
print $color{bold}, $color{green}, $2, $color{nc}, $line_ending;
|
||
} elsif ($text =~ /^(?:Upgraded\s|Installed\s|Successfully\s)/) {
|
||
print $color{blue}, "==>", $color{nc}, " ", $color{bold}, $color{green}, $text, $color{nc}, $line_ending;
|
||
} elsif ($text =~ /^(?:Outdated\s|Disabled\s|Warning)/) {
|
||
print $color{blue}, "==>", $color{nc}, " ", $color{yellow}, $text, $color{nc}, $line_ending;
|
||
} elsif ($text =~ /^(?:Formulae|Casks|New Formulae|New Casks|Caveats|Summary)$/) {
|
||
print $color{blue}, "==>", $color{nc}, " ", $color{bold}, $color{magenta}, $text, $color{nc}, $line_ending;
|
||
} else {
|
||
print $color{blue}, "==>", $color{nc}, " ", $color{cyan}, $text, $color{nc}, $line_ending;
|
||
}
|
||
}
|
||
|
||
sub colorize_line {
|
||
my ($line) = @_;
|
||
# Homebrew 的进度队列会在普通输出前留下光标控制序列(例如显示光标)。
|
||
# 先原样输出这些序列,再匹配文本,避免它们成为包名的一部分而破坏版本着色。
|
||
my $control_prefix = "";
|
||
while ($line =~ s/\A(\e\[[0-?]*[ -\/]*[@-~])//) {
|
||
$control_prefix .= $1;
|
||
}
|
||
print $control_prefix;
|
||
|
||
return if print_version_transition($line);
|
||
|
||
if ($line =~ /^==> /) {
|
||
print_heading($line);
|
||
} elsif ($line =~ /^(?:Warning:|warning:)/) {
|
||
print $color{yellow}, $line, $color{nc}, $line_ending;
|
||
} elsif ($line =~ /^(?:Error:|error:|Failed:|failed:)/) {
|
||
print $color{red}, $line, $color{nc}, $line_ending;
|
||
} elsif ($line =~ /^Removing:/) {
|
||
print $color{yellow}, $line, $color{nc}, $line_ending;
|
||
} elsif ($line =~ /^(?:✔︎ |✔ |🍺 )/ ||
|
||
$line =~ /was successfully upgraded!|successfully installed!|All dependencies satisfied\./) {
|
||
print $color{green}, $line, $color{nc}, $line_ending;
|
||
} elsif ($line =~ /^(?:Already up-to-date|Updated Homebrew |Updated .*tap|Successfully rebased |Pruned |This operation has freed )/) {
|
||
print $color{green}, $line, $color{nc}, $line_ending;
|
||
} elsif ($line =~ /^ (?:Downloaded|Verified) /) {
|
||
print $color{green}, $line, $color{nc}, $line_ending;
|
||
} else {
|
||
print $line, $line_ending;
|
||
}
|
||
}
|
||
|
||
my $sync_begin = "\e[?2026h";
|
||
my $sync_end = "\e[?2026l";
|
||
my $transport_buffer = "";
|
||
my $normal_buffer = "";
|
||
my $inside_sync_update = 0;
|
||
|
||
sub suffix_prefix_length {
|
||
my ($text, $marker) = @_;
|
||
my $maximum = length($marker) - 1;
|
||
$maximum = length($text) if length($text) < $maximum;
|
||
|
||
for (my $length = $maximum; $length > 0; $length--) {
|
||
return $length if substr($text, -$length) eq substr($marker, 0, $length);
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
sub emit_leading_control_sequences {
|
||
while ($normal_buffer =~ s/\A(\e\[[0-?]*[ -\/]*[@-~])//) {
|
||
print $1;
|
||
}
|
||
}
|
||
|
||
sub drain_normal_output {
|
||
my ($final) = @_;
|
||
emit_leading_control_sequences();
|
||
|
||
while (length $normal_buffer) {
|
||
my $newline = index($normal_buffer, "\n");
|
||
my $carriage = index($normal_buffer, "\r");
|
||
my $position;
|
||
if ($newline < 0) {
|
||
$position = $carriage;
|
||
} elsif ($carriage < 0) {
|
||
$position = $newline;
|
||
} else {
|
||
$position = $newline < $carriage ? $newline : $carriage;
|
||
}
|
||
|
||
if ($position < 0) {
|
||
if ($final) {
|
||
colorize_line($normal_buffer);
|
||
$normal_buffer = "";
|
||
}
|
||
return;
|
||
}
|
||
|
||
my $separator = substr($normal_buffer, $position, 1);
|
||
# 分块时 CR 可能正好在 CRLF 的边界,等待下一块再决定它是换行还是进度重绘。
|
||
return if !$final && $separator eq "\r" && $position + 1 == length($normal_buffer);
|
||
|
||
my $record = substr($normal_buffer, 0, $position);
|
||
if ($separator eq "\r" && substr($normal_buffer, $position + 1, 1) eq "\n") {
|
||
substr($normal_buffer, 0, $position + 2, "");
|
||
colorize_line($record);
|
||
} elsif ($separator eq "\n") {
|
||
substr($normal_buffer, 0, $position + 1, "");
|
||
colorize_line($record);
|
||
} else {
|
||
substr($normal_buffer, 0, $position + 1, "");
|
||
print $record, "\r";
|
||
}
|
||
emit_leading_control_sequences();
|
||
}
|
||
}
|
||
|
||
sub drain_transport_output {
|
||
my ($final) = @_;
|
||
|
||
while (length $transport_buffer) {
|
||
if ($inside_sync_update) {
|
||
my $end_position = index($transport_buffer, $sync_end);
|
||
if ($end_position >= 0) {
|
||
my $length = $end_position + length($sync_end);
|
||
print substr($transport_buffer, 0, $length, "");
|
||
$inside_sync_update = 0;
|
||
next;
|
||
}
|
||
|
||
my $keep = $final ? 0 : suffix_prefix_length($transport_buffer, $sync_end);
|
||
my $available = length($transport_buffer) - $keep;
|
||
print substr($transport_buffer, 0, $available, "") if $available > 0;
|
||
return;
|
||
}
|
||
|
||
my $begin_position = index($transport_buffer, $sync_begin);
|
||
if ($begin_position >= 0) {
|
||
$normal_buffer .= substr($transport_buffer, 0, $begin_position, "");
|
||
drain_normal_output(0);
|
||
print substr($transport_buffer, 0, length($sync_begin), "");
|
||
$inside_sync_update = 1;
|
||
next;
|
||
}
|
||
|
||
my $keep = $final ? 0 : suffix_prefix_length($transport_buffer, $sync_begin);
|
||
my $available = length($transport_buffer) - $keep;
|
||
if ($available > 0) {
|
||
$normal_buffer .= substr($transport_buffer, 0, $available, "");
|
||
drain_normal_output(0);
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
while (sysread(STDIN, my $chunk, 8192)) {
|
||
$transport_buffer .= $chunk;
|
||
drain_transport_output(0);
|
||
}
|
||
|
||
drain_transport_output(1);
|
||
drain_normal_output(1);
|
||
'
|
||
}
|
||
|
||
run_brew_in_pseudoterminal() {
|
||
local log_file="$1"
|
||
shift
|
||
|
||
# brewup 通常通过 curl 管道启动,因而主脚本的 stdin 可能是已关闭的管道。
|
||
# script(1) 若继承它,会向伪终端传入 ^D,使 Homebrew 的 y/n 提示直接失败。
|
||
# 应从控制终端读取输入,继续支持升级确认与实时进度。
|
||
if [[ -n "$log_file" ]]; then
|
||
if [[ -r /dev/tty ]]; then
|
||
/usr/bin/script -qF "$log_file" env -u HOMEBREW_COLOR HOMEBREW_NO_COLOR=1 brew "$@" < /dev/tty
|
||
else
|
||
/usr/bin/script -qF "$log_file" env -u HOMEBREW_COLOR HOMEBREW_NO_COLOR=1 brew "$@"
|
||
fi
|
||
elif [[ -r /dev/tty ]]; then
|
||
/usr/bin/script -q /dev/null env -u HOMEBREW_COLOR HOMEBREW_NO_COLOR=1 brew "$@" < /dev/tty
|
||
else
|
||
/usr/bin/script -q /dev/null env -u HOMEBREW_COLOR HOMEBREW_NO_COLOR=1 brew "$@"
|
||
fi
|
||
}
|
||
|
||
run_brew_colored() {
|
||
local pipeline_status=()
|
||
|
||
# 伪终端保留 Homebrew 的实时进度,流式着色器只处理完整文本行,
|
||
# 不会缓冲或改写用 CR 刷新的进度输出。
|
||
if can_stream_colorize_tty_output; then
|
||
if run_brew_in_pseudoterminal "" "$@" | colorize_brew_tty_stream; then
|
||
return 0
|
||
fi
|
||
pipeline_status=("${PIPESTATUS[@]}")
|
||
if ((${pipeline_status[1]:-0} != 0)); then
|
||
print_error "failed to colorize Homebrew output."
|
||
return "${pipeline_status[1]}"
|
||
fi
|
||
return "${pipeline_status[0]}"
|
||
elif [[ -t 1 ]]; then
|
||
env -u HOMEBREW_COLOR brew "$@"
|
||
return $?
|
||
fi
|
||
|
||
if env -u HOMEBREW_COLOR brew "$@" 2>&1 | colorize_brew_output; then
|
||
return 0
|
||
fi
|
||
pipeline_status=("${PIPESTATUS[@]}")
|
||
if ((${pipeline_status[1]:-0} != 0)); then
|
||
print_error "failed to colorize Homebrew output."
|
||
return "${pipeline_status[1]}"
|
||
fi
|
||
return "${pipeline_status[0]}"
|
||
}
|
||
|
||
is_enabled() {
|
||
case "${1:-}" in
|
||
1|true|TRUE|yes|YES|on|ON) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
# shellcheck disable=SC2329 # Invoked by EXIT trap.
|
||
cleanup_runtime() {
|
||
if [[ -n "${ACTIVE_LOG_FILE:-}" && -f "$ACTIVE_LOG_FILE" ]]; then
|
||
rm -f "$ACTIVE_LOG_FILE"
|
||
fi
|
||
if ((LOCK_ACQUIRED)); then
|
||
rm -f "$LOCK_DIR/pid"
|
||
rmdir "$LOCK_DIR" 2>/dev/null || true
|
||
fi
|
||
}
|
||
|
||
# shellcheck disable=SC2329 # Invoked by INT trap.
|
||
on_interrupt() {
|
||
exit 130
|
||
}
|
||
|
||
# shellcheck disable=SC2329 # Invoked by TERM trap.
|
||
on_terminate() {
|
||
exit 143
|
||
}
|
||
|
||
trap cleanup_runtime EXIT
|
||
trap on_interrupt INT
|
||
trap on_terminate TERM
|
||
|
||
acquire_lock() {
|
||
local existing_pid=""
|
||
|
||
if mkdir "$LOCK_DIR" 2>/dev/null; then
|
||
LOCK_ACQUIRED=1
|
||
printf '%s\n' "$$" > "$LOCK_DIR/pid"
|
||
return 0
|
||
fi
|
||
|
||
if [[ -r "$LOCK_DIR/pid" ]]; then
|
||
IFS= read -r existing_pid < "$LOCK_DIR/pid" || true
|
||
fi
|
||
if [[ "$existing_pid" =~ ^[0-9]+$ ]] && kill -0 "$existing_pid" 2>/dev/null; then
|
||
print_error "another brewup process is already running (PID ${existing_pid})."
|
||
exit 75
|
||
fi
|
||
|
||
rm -f "$LOCK_DIR/pid" 2>/dev/null || true
|
||
rmdir "$LOCK_DIR" 2>/dev/null || {
|
||
print_error "unable to remove stale lock directory: $LOCK_DIR"
|
||
exit 75
|
||
}
|
||
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||
print_error "another brewup process acquired the lock."
|
||
exit 75
|
||
fi
|
||
LOCK_ACQUIRED=1
|
||
printf '%s\n' "$$" > "$LOCK_DIR/pid"
|
||
}
|
||
|
||
retry_attempts() {
|
||
local attempts="${HB_RETRY_ATTEMPTS:-3}"
|
||
if [[ ! "$attempts" =~ ^[1-9][0-9]*$ ]] || ((attempts > 10)); then
|
||
attempts=3
|
||
fi
|
||
printf '%s' "$attempts"
|
||
}
|
||
|
||
retry_delay_seconds() {
|
||
local delay="${HB_RETRY_DELAY_SECONDS:-5}"
|
||
if [[ ! "$delay" =~ ^[0-9]+$ ]] || ((delay > 300)); then
|
||
delay=5
|
||
fi
|
||
printf '%s' "$delay"
|
||
}
|
||
|
||
is_non_retryable_brew_error() {
|
||
local log_file="$1"
|
||
grep -Eiq \
|
||
'Running Homebrew as root|Permission denied|Operation not permitted|No space left on device|checksum mismatch|SHA-?256 mismatch|certificate verify failed|SSL certificate problem|curl: \((3|23|26|60)\)|unknown (option|argument)|invalid (option|argument)' \
|
||
"$log_file"
|
||
}
|
||
|
||
is_retryable_brew_error() {
|
||
local log_file="$1"
|
||
grep -Eiq \
|
||
'curl: \((5|6|7|18|28|35|52|55|56|92)\)|Transferred a partial file|Could not resolve host|timed out|timeout|connection (reset|refused|closed)|HTTP[^0-9]*(429|5[0-9]{2})|returned error: (429|5[0-9]{2})|temporarily unavailable|failed to download|download.*failed|RPC failed|early EOF|remote end hung up|unexpected disconnect' \
|
||
"$log_file"
|
||
}
|
||
|
||
run_brew_with_retries() {
|
||
local label="$1"
|
||
shift
|
||
|
||
local attempts delay attempt status next_delay
|
||
local pipeline_status=()
|
||
attempts="$(retry_attempts)"
|
||
delay="$(retry_delay_seconds)"
|
||
ACTIVE_LOG_FILE="$(mktemp "${TMPDIR:-/tmp}/brewup-retry.XXXXXX.log")"
|
||
|
||
for ((attempt = 1; attempt <= attempts; attempt++)); do
|
||
if ((attempt > 1)); then
|
||
print_info "Retrying ${label} (${attempt}/${attempts}) after ${delay}s..."
|
||
sleep "$delay"
|
||
next_delay=$((delay * 2))
|
||
((next_delay > 300)) && next_delay=300
|
||
delay="$next_delay"
|
||
fi
|
||
|
||
: > "$ACTIVE_LOG_FILE"
|
||
if can_stream_colorize_tty_output; then
|
||
# 为错误分类保留转录,同时通过伪终端和流式着色兼顾进度条与版本配色。
|
||
if run_brew_in_pseudoterminal "$ACTIVE_LOG_FILE" "$@" | colorize_brew_tty_stream; then
|
||
status=0
|
||
else
|
||
pipeline_status=("${PIPESTATUS[@]}")
|
||
status="${pipeline_status[0]}"
|
||
if ((${pipeline_status[1]:-0} != 0)); then
|
||
print_error "failed to colorize Homebrew output."
|
||
status="${pipeline_status[1]}"
|
||
fi
|
||
fi
|
||
elif env -u HOMEBREW_COLOR brew "$@" 2>&1 | tee "$ACTIVE_LOG_FILE" | colorize_brew_output; then
|
||
status=0
|
||
else
|
||
pipeline_status=("${PIPESTATUS[@]}")
|
||
status="${pipeline_status[0]}"
|
||
if ((${pipeline_status[1]:-0} != 0)); then
|
||
print_error "failed to write retry log."
|
||
status="${pipeline_status[1]}"
|
||
elif ((${pipeline_status[2]:-0} != 0)); then
|
||
print_error "failed to colorize Homebrew output."
|
||
status="${pipeline_status[2]}"
|
||
fi
|
||
fi
|
||
|
||
if ((status == 0)); then
|
||
rm -f "$ACTIVE_LOG_FILE"
|
||
ACTIVE_LOG_FILE=""
|
||
return 0
|
||
fi
|
||
|
||
if is_non_retryable_brew_error "$ACTIVE_LOG_FILE"; then
|
||
rm -f "$ACTIVE_LOG_FILE"
|
||
ACTIVE_LOG_FILE=""
|
||
return "$status"
|
||
fi
|
||
if ((attempt == attempts)) || ! is_retryable_brew_error "$ACTIVE_LOG_FILE"; then
|
||
rm -f "$ACTIVE_LOG_FILE"
|
||
ACTIVE_LOG_FILE=""
|
||
return "$status"
|
||
fi
|
||
print_notice "${label} failed with a retryable error."
|
||
done
|
||
}
|
||
|
||
get_outdated_formulae() {
|
||
env -u HOMEBREW_COLOR brew outdated --formula 2>/dev/null
|
||
}
|
||
|
||
get_outdated_casks() {
|
||
env -u HOMEBREW_COLOR brew outdated --cask --greedy 2>/dev/null
|
||
}
|
||
|
||
run_brew_doctor() {
|
||
local output status=0 line
|
||
|
||
if output="$(env -u HOMEBREW_COLOR brew doctor 2>&1)"; then
|
||
status=0
|
||
else
|
||
status=$?
|
||
fi
|
||
while IFS= read -r line; do
|
||
case "$line" in
|
||
Warning:*|*deprecated*|*disabled*|*pre-release*|*"find replacements"*|*"do not provide support"*|*"Tier 2 configuration"*)
|
||
printf '%b%s%b\n' "$YELLOW" "$line" "$NC"
|
||
;;
|
||
' '*|' https://'*)
|
||
printf '%b%s%b\n' "$CYAN" "$line" "$NC"
|
||
;;
|
||
*)
|
||
printf '%s\n' "$line"
|
||
;;
|
||
esac
|
||
done <<< "$output"
|
||
return "$status"
|
||
}
|
||
|
||
run_formula_upgrade() {
|
||
local bulk_status=0 initial_formulae="" initial_formulae_known=1
|
||
local outdated_formulae remaining_formulae formula
|
||
local failed_formulae=()
|
||
|
||
if ! initial_formulae="$(get_outdated_formulae)"; then
|
||
initial_formulae_known=0
|
||
print_warning "unable to list Formulae requiring updates before upgrade."
|
||
elif [[ -n "$initial_formulae" ]]; then
|
||
print_colored_list "Formulae requiring updates:" "$YELLOW" "$initial_formulae"
|
||
else
|
||
print_success "No Formulae require updates."
|
||
fi
|
||
|
||
run_brew_colored upgrade --formula || bulk_status=$?
|
||
if ((bulk_status == 0)); then
|
||
if ((initial_formulae_known)) && [[ -n "$initial_formulae" ]]; then
|
||
print_colored_list "Updated Formulae:" "$GREEN" "$initial_formulae"
|
||
else
|
||
print_success "Formula upgrade completed."
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
print_warning "Formula upgrade returned ${bulk_status}; checking remaining Formulae..."
|
||
if ! outdated_formulae="$(get_outdated_formulae)"; then
|
||
print_warning "unable to verify outdated Formulae."
|
||
return "$bulk_status"
|
||
fi
|
||
if [[ -z "$outdated_formulae" ]]; then
|
||
if [[ -n "$initial_formulae" ]]; then
|
||
print_colored_list "Updated Formulae:" "$GREEN" "$initial_formulae"
|
||
else
|
||
print_success "No outdated Formulae remain; the bulk error was recovered."
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
while IFS= read -r formula; do
|
||
[[ -n "$formula" ]] || continue
|
||
print_info "Retrying Formula: ${formula}"
|
||
if run_brew_with_retries "Formula ${formula}" upgrade --formula "$formula"; then
|
||
:
|
||
else
|
||
failed_formulae+=("$formula")
|
||
fi
|
||
done <<< "$outdated_formulae"
|
||
|
||
if ! remaining_formulae="$(get_outdated_formulae)"; then
|
||
print_warning "unable to verify Formulae after individual retries."
|
||
FAILED_FORMULAE=("${failed_formulae[@]}")
|
||
return 1
|
||
fi
|
||
if [[ -z "$remaining_formulae" ]]; then
|
||
if [[ -n "$initial_formulae" ]]; then
|
||
print_colored_list "Updated Formulae after isolated retries:" "$GREEN" "$initial_formulae"
|
||
else
|
||
print_success "All outdated Formulae were upgraded after isolated retries."
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
while IFS= read -r formula; do
|
||
[[ -n "$formula" ]] && FAILED_FORMULAE+=("$formula")
|
||
done <<< "$remaining_formulae"
|
||
print_colored_list "Outdated Formulae remaining after retries:" "$RED" "$remaining_formulae"
|
||
return 1
|
||
}
|
||
|
||
run_cask_upgrade() {
|
||
local bulk_status=0 initial_casks="" initial_casks_known=1
|
||
local outdated_casks remaining_casks cask
|
||
local bulk_args=(upgrade --cask --greedy)
|
||
local failed_casks=()
|
||
|
||
if is_enabled "${HB_CASK_FORCE:-0}"; then
|
||
bulk_args+=(--force)
|
||
print_warning "HB_CASK_FORCE is enabled; existing Cask files may be overwritten."
|
||
fi
|
||
|
||
if ! initial_casks="$(get_outdated_casks)"; then
|
||
initial_casks_known=0
|
||
print_warning "unable to list Casks requiring updates before upgrade."
|
||
elif [[ -n "$initial_casks" ]]; then
|
||
print_colored_list "Casks requiring updates:" "$YELLOW" "$initial_casks"
|
||
else
|
||
print_success "No Casks require updates."
|
||
fi
|
||
|
||
run_brew_colored "${bulk_args[@]}" || bulk_status=$?
|
||
if ((bulk_status == 0)); then
|
||
if ((initial_casks_known)) && [[ -n "$initial_casks" ]]; then
|
||
print_colored_list "Updated Casks:" "$GREEN" "$initial_casks"
|
||
else
|
||
print_success "Cask upgrade completed."
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
print_warning "Cask upgrade returned ${bulk_status}; checking remaining Casks..."
|
||
if ! outdated_casks="$(get_outdated_casks)"; then
|
||
print_warning "unable to verify outdated Casks."
|
||
return "$bulk_status"
|
||
fi
|
||
if [[ -z "$outdated_casks" ]]; then
|
||
if [[ -n "$initial_casks" ]]; then
|
||
print_colored_list "Updated Casks:" "$GREEN" "$initial_casks"
|
||
else
|
||
print_success "No outdated Casks remain; the bulk error was recovered."
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
while IFS= read -r cask; do
|
||
[[ -n "$cask" ]] || continue
|
||
print_info "Retrying Cask: ${cask} (without --force)"
|
||
if run_brew_with_retries "Cask ${cask}" upgrade --cask --greedy "$cask"; then
|
||
:
|
||
else
|
||
failed_casks+=("$cask")
|
||
fi
|
||
done <<< "$outdated_casks"
|
||
|
||
if ! remaining_casks="$(get_outdated_casks)"; then
|
||
print_warning "unable to verify Casks after individual retries."
|
||
FAILED_CASKS=("${failed_casks[@]}")
|
||
return 1
|
||
fi
|
||
if [[ -z "$remaining_casks" ]]; then
|
||
if [[ -n "$initial_casks" ]]; then
|
||
print_colored_list "Updated Casks after isolated retries:" "$GREEN" "$initial_casks"
|
||
else
|
||
print_success "All outdated Casks were upgraded after isolated retries."
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
while IFS= read -r cask; do
|
||
[[ -n "$cask" ]] && FAILED_CASKS+=("$cask")
|
||
done <<< "$remaining_casks"
|
||
print_colored_list "Outdated Casks remaining after retries:" "$RED" "$remaining_casks"
|
||
return 1
|
||
}
|
||
|
||
run_cleanup() {
|
||
run_brew_colored cleanup --prune=all
|
||
}
|
||
|
||
print_summary() {
|
||
local overall_status="$1"
|
||
|
||
separator
|
||
if ((overall_status == 0)); then
|
||
print_success "All operations completed successfully."
|
||
else
|
||
printf '%bHomebrew maintenance completed with unresolved failures.%b\n' "$RED" "$NC"
|
||
if ((${#FAILED_FORMULAE[@]} > 0)); then
|
||
printf '%bFailed Formulae:%b %s\n' "$RED" "$NC" "${FAILED_FORMULAE[*]}"
|
||
fi
|
||
if ((${#FAILED_CASKS[@]} > 0)); then
|
||
printf '%bFailed Casks:%b %s\n' "$RED" "$NC" "${FAILED_CASKS[*]}"
|
||
fi
|
||
if ((CLEANUP_FAILED)); then
|
||
printf '%bHomebrew cleanup failed.%b\n' "$RED" "$NC"
|
||
fi
|
||
fi
|
||
separator
|
||
}
|
||
|
||
parse_args "$@"
|
||
resolve_terminal_width
|
||
|
||
command -v brew >/dev/null 2>&1 || { print_error "Homebrew is not installed or not in PATH."; exit 127; }
|
||
command -v tee >/dev/null 2>&1 || { print_error "tee is required."; exit 127; }
|
||
acquire_lock
|
||
|
||
separator
|
||
print_header "Step 1: Updating Homebrew repositories (brew update -v)"
|
||
run_brew_with_retries "Homebrew repository update" update -v
|
||
separator
|
||
printf '\n'
|
||
|
||
separator
|
||
print_header "Step 2: Performing health check (brew doctor)"
|
||
if is_enabled "${HB_SKIP_DOCTOR:-0}"; then
|
||
print_notice "brew doctor skipped because HB_SKIP_DOCTOR is enabled."
|
||
elif ! run_brew_doctor; then
|
||
print_warning "'brew doctor' detected issues. Manual review is recommended."
|
||
else
|
||
print_success "Homebrew environment is in good health."
|
||
fi
|
||
separator
|
||
printf '\n'
|
||
|
||
separator
|
||
print_header "Step 3: Upgrading Formulae and Casks with failure isolation"
|
||
overall_status=0
|
||
|
||
printf '\n'
|
||
print_info ">>> [1/2] Upgrading CLI Formulae..."
|
||
if ! run_formula_upgrade; then
|
||
overall_status=1
|
||
fi
|
||
|
||
printf '\n'
|
||
print_info ">>> [2/2] Upgrading GUI Casks..."
|
||
if ! run_cask_upgrade; then
|
||
overall_status=1
|
||
fi
|
||
separator
|
||
printf '\n'
|
||
|
||
separator
|
||
print_header "Step 4: Cleaning all Homebrew cache and old versions (brew cleanup --prune=all)"
|
||
if ! run_cleanup; then
|
||
overall_status=1
|
||
CLEANUP_FAILED=1
|
||
fi
|
||
separator
|
||
printf '\n'
|
||
|
||
print_summary "$overall_status"
|
||
exit "$overall_status"
|