#!/bin/bash # 设置严格模式 set -euo pipefail # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # 初始化日志 setup_logging() { local log_dir="${USER_HOME}/.logs" local log_file="${log_dir}/shell_setup_$(date +%Y%m%d_%H%M%S).log" mkdir -p "$log_dir" exec 1> >(tee -a "$log_file") exec 2> >(tee -a "$log_file" >&2) log_info "开始记录日志到: $log_file" } # 日志函数 log_info() { echo -e "${GREEN}[INFO]${NC} $1" | tee -a "${LOG_FILE:-/dev/null}" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" | tee -a "${LOG_FILE:-/dev/null}" } log_error() { echo -e "${RED}[ERROR]${NC} $1" | tee -a "${LOG_FILE:-/dev/null}" } log_success() { echo -e "${BLUE}[SUCCESS]${NC} $1" | tee -a "${LOG_FILE:-/dev/null}" } # 进度显示函数 show_progress() { local msg="$1" echo -ne "\r${GREEN}[INFO]${NC} $msg... " } update_progress() { local msg="$1" local status="$2" echo -e "\r${GREEN}[INFO]${NC} $msg... ${status}" } # 清理函数 cleanup() { local exit_code=$? if [ $exit_code -ne 0 ]; then log_error "脚本执行失败,退出码: $exit_code" if [ -n "${LAST_BACKUP:-}" ] && [ -f "$LAST_BACKUP" ]; then log_info "正在恢复配置文件..." manage_config "restore" "$LAST_BACKUP" fi fi exit $exit_code } # 网络检查函数 check_network() { local test_urls=("github.com" "raw.githubusercontent.com") for url in "${test_urls[@]}"; do if ! ping -c 1 $url >/dev/null 2>&1; then log_warn "无法访问 $url" return 1 fi done return 0 } # 获取镜像源 get_mirror_url() { local original_url="$1" local url_type="$2" if check_network; then echo "$original_url" return 0 fi case "$url_type" in "github") echo "https://ghproxy.com/$original_url" ;; "omz") echo "https://gitee.com/mirrors/oh-my-zsh/raw/master/tools/install.sh" ;; *) echo "$original_url" ;; esac } # 配置文件管理 manage_config() { local action="$1" local config_file="$2" local backup_dir="${USER_HOME}/.config_backups" local timestamp=$(date +%Y%m%d_%H%M%S) mkdir -p "$backup_dir" case "$action" in "backup") if [ -f "$config_file" ]; then local backup_file="${backup_dir}/$(basename ${config_file}).${timestamp}.bak" cp "$config_file" "$backup_file" LAST_BACKUP="$backup_file" log_info "已备份 $config_file 到 $backup_file" fi ;; "restore") local source_file="$config_file" if [ -f "$source_file" ]; then local dest_file=$(echo "$source_file" | sed 's/\.[0-9]*_[0-9]*\.bak$//') cp "$source_file" "$dest_file" log_info "已恢复 $dest_file" fi ;; esac } # 插件管理 manage_plugin() { local plugin_name="$1" local plugin_dir="$2" local plugin_url="$3" show_progress "处理插件 $plugin_name" if [ -d "$plugin_dir" ]; then if [ -d "$plugin_dir/.git" ]; then (cd "$plugin_dir" && git pull origin master >/dev/null 2>&1) if [ $? -eq 0 ]; then update_progress "处理插件 $plugin_name" "更新成功" return 0 fi fi log_warn "更新 $plugin_name 失败,尝试重新安装" rm -rf "$plugin_dir" fi if git clone --depth=1 "$plugin_url" "$plugin_dir" >/dev/null 2>&1; then update_progress "处理插件 $plugin_name" "安装成功" return 0 else update_progress "处理插件 $plugin_name" "安装失败" return 1 fi } # 依赖检查 check_dependencies() { local deps=("$@") local missing_deps=() show_progress "检查依赖" for dep in "${deps[@]}"; do if ! command -v "$dep" >/dev/null 2>&1; then missing_deps+=("$dep") fi done if [ ${#missing_deps[@]} -ne 0 ]; then update_progress "检查依赖" "发现缺失" log_warn "缺少以下依赖: ${missing_deps[*]}" log_info "正在安装缺失的依赖..." apt-get update >/dev/null 2>&1 apt-get install -y "${missing_deps[@]}" >/dev/null 2>&1 update_progress "安装依赖" "完成" else update_progress "检查依赖" "已满足" fi } # 用户交互 confirm() { local msg="$1" local default="${2:-Y}" while true; do read -p "$msg [Y/n] " response response=${response:-$default} case $response in [Yy]) return 0 ;; [Nn]) return 1 ;; *) echo "请输入 y 或 n" ;; esac done } # 检查命令是否存在 check_command() { if ! command -v "$1" &> /dev/null; then log_error "命令 '$1' 未找到" return 1 fi return 0 } # 检查是否为root用户 check_root() { if [ "$EUID" -ne 0 ]; then log_error "请使用root权限运行此脚本" exit 1 fi } # 获取当前用户信息 get_user_info() { CURRENT_USER=$SUDO_USER if [ -z "$CURRENT_USER" ]; then CURRENT_USER=$(who | awk '{print $1}' | head -n1) fi USER_HOME="/home/$CURRENT_USER" # 验证用户信息 if ! id "$CURRENT_USER" >/dev/null 2>&1; then log_error "无法获取用户信息: $CURRENT_USER" exit 1 fi if [ ! -d "$USER_HOME" ]; then log_error "用户主目录不存在: $USER_HOME" exit 1 fi } # 检查Node.js版本 check_nodejs() { if command -v node &> /dev/null; then local node_version=$(node -v | cut -d 'v' -f2) if [ "$(echo -e "${node_version}\n16.0.0" | sort -V | head -n1)" = "16.0.0" ]; then log_info "检测到 Node.js ${node_version},版本满足要求" return 0 else log_warn "当前 Node.js 版本 ${node_version} 过低,需要更新" return 1 fi else log_warn "未检测到 Node.js" return 1 fi } # 安装Node.js install_nodejs() { if ! check_nodejs; then log_info "安装 Node.js..." # 尝试使用 Debian 仓库安装 if apt-get install -y nodejs npm && check_nodejs; then log_info "成功从 Debian 仓库安装 Node.js" else log_warn "从 Debian 仓库安装 Node.js 失败,尝试使用 NodeSource..." # 删除可能存在的旧版本 apt-get remove -y nodejs npm # 添加 NodeSource 仓库 mkdir -p /etc/apt/keyrings curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list apt-get update apt-get install -y nodejs fi # 安装neovim包 su - $CURRENT_USER -c "npm install -g neovim" fi } # 设置Python虚拟环境 setup_python_venv() { local nvim_venv="${USER_HOME}/.local/share/nvim/venv" # 创建虚拟环境 su - $CURRENT_USER -c "python3 -m venv ${nvim_venv}" # 安装pynvim su - $CURRENT_USER -c "${nvim_venv}/bin/pip install pynvim" echo "${nvim_venv}" } # 下载文件的通用函数 download_file() { local url="$1" local output="$2" local max_retries=3 local retry=0 local success=false show_progress "下载 $(basename "$output")" # 获取可能的镜像URL local mirror_url=$(get_mirror_url "$url" "github") while [ $retry -lt $max_retries ] && [ "$success" = false ]; do if wget --no-check-certificate -q -O "$output" "$mirror_url" 2>/dev/null; then success=true elif curl -k -L -s -o "$output" "$mirror_url" 2>/dev/null; then success=true else retry=$((retry + 1)) log_warn "下载失败,尝试重试 ($retry/$max_retries)..." sleep 2 fi done if [ "$success" = false ]; then update_progress "下载 $(basename "$output")" "失败" return 1 fi update_progress "下载 $(basename "$output")" "成功" return 0 } # 检查并安装基础依赖 install_base_dependencies() { log_info "安装基础依赖..." local base_deps=( "git" "curl" "wget" "unzip" "python3-pip" "python3-venv" "build-essential" "ca-certificates" "fontconfig" ) check_dependencies "${base_deps[@]}" # 更新 CA 证书 update-ca-certificates >/dev/null 2>&1 } # 安装字体 install_font() { local font_url="$1" local font_name="$2" local font_dir="${USER_HOME}/.local/share/fonts" show_progress "安装字体 $font_name" # 创建字体目录 su - $CURRENT_USER -c "mkdir -p ${font_dir}" # 下载并安装字体 if download_file "$font_url" "${font_dir}/${font_name}"; then # 确保fontconfig已安装 ensure_package "fontconfig" # 更新字体缓存 fc-cache -f -v >/dev/null 2>&1 update_progress "安装字体 $font_name" "完成" return 0 else update_progress "安装字体 $font_name" "失败" return 1 fi } # 添加环境变量 add_env_var() { local var_name="$1" local var_value="$2" local config_file="$3" if ! grep -q "export ${var_name}=" "$config_file" 2>/dev/null; then echo "export ${var_name}=\"${var_value}\"" >> "$config_file" log_info "添加环境变量 ${var_name}=${var_value}" fi } # 添加PATH add_path() { local new_path="$1" local config_file="$2" if ! grep -q "export PATH=.*${new_path}" "$config_file" 2>/dev/null; then echo "export PATH=\"${new_path}:\$PATH\"" >> "$config_file" log_info "添加PATH ${new_path}" fi } # 添加别名 add_alias() { local alias_name="$1" local alias_value="$2" local config_file="$3" if ! grep -q "alias ${alias_name}=" "$config_file" 2>/dev/null; then echo "alias ${alias_name}='${alias_value}'" >> "$config_file" log_info "添加别名 ${alias_name}=${alias_value}" fi } # 检查并创建目录 ensure_dir() { local dir="$1" local owner="${2:-}" if [ ! -d "$dir" ]; then mkdir -p "$dir" if [ -n "$owner" ]; then chown -R "$owner" "$dir" fi log_info "创建目录 $dir" fi } # 检查并安装依赖包 ensure_package() { local package="$1" if ! dpkg -l | grep -q "^ii $package " 2>/dev/null; then log_info "安装 $package..." apt-get update >/dev/null 2>&1 apt-get install -y "$package" >/dev/null 2>&1 log_success "$package 安装完成" fi } # 检查进程是否在运行 check_process() { local process="$1" if pgrep -x "$process" >/dev/null; then return 0 fi return 1 } # 等待进程结束 wait_process() { local process="$1" local timeout="$2" local count=0 while check_process "$process" && [ $count -lt $timeout ]; do sleep 1 count=$((count + 1)) done if check_process "$process"; then return 1 fi return 0 } # 设置trap trap cleanup EXIT