#!/usr/bin/env bash # Homebrew 智能升级脚本(强制 Cask 更新增强版 v5.3 - 移除 sudo 密码注入) # ================== 脚本环境设置 ================== # set -e:当命令返回非零退出状态(表示失败)时,脚本会立即退出。 set -e # set -o pipefail:在管道命令中,如果任何一个子命令失败,整个管道即为失败。 set -o pipefail # --- 颜色定义 (自动检测终端是否支持) --- if [ -t 1 ]; then GREEN='\033[1;32m' YELLOW='\033[1;33m' BLUE='\033[1;34m' CYAN='\033[1;36m' NC='\033[0m' else GREEN='' YELLOW='' BLUE='' CYAN='' NC='' fi # --- 终端宽度和打印函数 --- DEFAULT_FALLBACK_WIDTH="130" TERMINAL_WIDTH_OVERRIDE="" # 解析命令行参数 while [[ $# -gt 0 ]]; do case "$1" in --width) if [[ -n "$2" && "$2" =~ ^[0-9]+$ ]]; then TERMINAL_WIDTH_OVERRIDE="$2" shift 2 else echo -e "${YELLOW}Error: '--width' parameter requires a valid numeric value.${NC}" exit 1 fi ;; --width=*) TERMINAL_WIDTH_OVERRIDE="${1#*=}" if ! [[ "$TERMINAL_WIDTH_OVERRIDE" =~ ^[0-9]+$ ]]; then echo -e "${YELLOW}Error: '--width' parameter requires a valid numeric value.${NC}" exit 1 fi shift ;; *) shift ;; esac done FIXED_TERMINAL_WIDTH="" if [[ -n "$TERMINAL_WIDTH_OVERRIDE" ]]; then FIXED_TERMINAL_WIDTH="$TERMINAL_WIDTH_OVERRIDE" elif [[ -n "$HB_TERMINAL_WIDTH" && "$HB_TERMINAL_WIDTH" =~ ^[0-9]+$ ]]; then FIXED_TERMINAL_WIDTH="$HB_TERMINAL_WIDTH" fi terminal_width() { local width="" if [[ -n "$FIXED_TERMINAL_WIDTH" ]]; then printf "%s" "$FIXED_TERMINAL_WIDTH" return fi if command -v stty &>/dev/null && stty size &>/dev/null; then width=$(stty size 2>/dev/null | awk '{print $2}') fi if [[ -z "$width" || "$width" -le 0 ]]; then width=$(tput cols 2>/dev/null || echo "$DEFAULT_FALLBACK_WIDTH") fi if [[ -z "$width" || "$width" -le 0 ]]; then width="$DEFAULT_FALLBACK_WIDTH" fi printf "%s" "$width" } separator() { local width; width=$(terminal_width); printf '=%.0s' $(seq 1 "$width"); printf "\n"; } print_header() { echo -e "${BLUE}$1${NC}"; } # ================== 流程开始 ================== separator print_header "Step 1: Updating Homebrew repositories (brew update -v)" brew update -v separator printf "\n" separator print_header "Step 2: Performing health check (brew doctor)" if ! brew doctor; then echo -e "${YELLOW}Warning: 'brew doctor' detected issues. Manual review and resolution are recommended.${NC}" else echo "Homebrew environment is in good health." fi separator printf "\n" separator print_header "Step 3: Verifying brew-cu extension for GUI Apps" if ! brew tap | grep -q "buo/cask-upgrade"; then echo -e "${YELLOW}Extension 'brew-cu' not found. Installing now...${NC}" brew tap buo/cask-upgrade else echo -e "${GREEN}Extension 'brew-cu' is already active.${NC}" fi separator printf "\n" separator print_header "Step 4: Executing comprehensive upgrades (Formulae & Casks)" echo -e "${NC}" # 1. 升级命令行工具 (Formulae) echo -e "\n${CYAN}>>> [1/2] Upgrading CLI Formulae (brew upgrade --formula)...${NC}" brew upgrade --formula # 2. 升级图形界面软件 (Casks) echo -e "\n${CYAN}>>> [2/2] Upgrading GUI Casks (brew cu -yaq)...${NC}" # 强制注入环境变量,确保终端输出依然保留 ANSI 颜色格式 export HOMEBREW_COLOR=1 # --- 终端宽度处理 --- # 交互式终端里让子进程读取 PTY 的实时尺寸;非交互环境或固定宽度模式下提供 COLUMNS 兜底。 if [[ -n "$FIXED_TERMINAL_WIDTH" || ! -t 1 ]]; then export COLUMNS COLUMNS="$(terminal_width)" else unset COLUMNS fi # 使用 Python 原生 PTY 自建极简转发引擎。它只转发用户输入,不保存、不注入 sudo 密码。 python3 -c ' import pty, os, sys, select, fcntl, termios, struct, tty, signal cmd = sys.argv[1:] pid, fd = pty.fork() if pid == 0: os.execvp(cmd[0], cmd) else: def sync_winsize(*_): s = struct.pack("HHHH", 0, 0, 0, 0) try: winsize = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, s) rows, cols, xpix, ypix = struct.unpack("HHHH", winsize) if rows > 0 and cols > 0: fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize) return except Exception: pass try: fallback_cols = int(os.environ.get("COLUMNS", "130") or "130") except ValueError: fallback_cols = 130 try: fallback_rows = int(os.environ.get("LINES", "40") or "40") except ValueError: fallback_rows = 40 fallback = struct.pack("HHHH", fallback_rows, fallback_cols, 0, 0) fcntl.ioctl(fd, termios.TIOCSWINSZ, fallback) sync_winsize() signal.signal(signal.SIGWINCH, sync_winsize) stdin_fd = sys.stdin.fileno() stdout_fd = sys.stdout.fileno() forward_stdin = sys.stdin.isatty() old_stdin_attrs = None if forward_stdin: old_stdin_attrs = termios.tcgetattr(stdin_fd) tty.setraw(stdin_fd) exit_code = 0 try: while True: try: watch = [fd] if forward_stdin: watch.append(stdin_fd) try: rfds, _, _ = select.select(watch, [], [], 0.1) except InterruptedError: continue if fd in rfds: data = os.read(fd, 8192) if not data: break os.write(stdout_fd, data) if forward_stdin and stdin_fd in rfds: data = os.read(stdin_fd, 8192) if data: os.write(fd, data) except OSError: break try: wpid, wstatus = os.waitpid(pid, os.WNOHANG) if wpid == pid: if os.WIFEXITED(wstatus): exit_code = os.WEXITSTATUS(wstatus) else: exit_code = 1 break except ChildProcessError: break finally: if old_stdin_attrs is not None: termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_stdin_attrs) sys.exit(exit_code) ' brew cu -yaq separator printf "\n" separator print_header "Step 5: Cleaning up old files and caches (brew cleanup --prune=all)" brew cleanup --prune=all separator printf "\n" echo -e "${GREEN}All operations completed!${NC}" printf "\n"