#!/usr/bin/env bash set -Eeuo pipefail # =================配置参数================= DEFAULT_MAX_DEPTH=3 DEFAULT_IGNORE_DIR="compose-ignore" # ========================================= # 变量初始化 : "${MAX_DEPTH:=$DEFAULT_MAX_DEPTH}" : "${IGNORE_DIR:=$DEFAULT_IGNORE_DIR}" COMPOSE_CMD="" SCRIPT_START_TIME=$(date +%s) # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' MAGENTA='\033[0;35m' BOLD='\033[1m' NC='\033[0m' # 状态图标 ICON_SUCCESS="✅" ICON_FAIL="❌" ICON_WARN="⚠️" ICON_INFO="ℹ️" ICON_PKG="📦" ICON_TIME="⏱️" # 捕获中断信号 trap 'echo -e "\n${RED}[!] 用户取消操作,正在退出...${NC}"; exit 1' SIGINT SIGTERM # 辅助函数:打印带颜色的消息 print_info() { echo -e "${BLUE}${ICON_INFO} [INFO]${NC} $1"; } print_success() { echo -e "${GREEN}${ICON_SUCCESS} [SUCCESS]${NC} $1"; } print_warning() { echo -e "${YELLOW}${ICON_WARN} [WARNING]${NC} $1"; } print_error() { echo -e "${RED}${ICON_FAIL} [ERROR]${NC} $1"; } print_header() { echo -e "\n${MAGENTA}${BOLD}$1${NC}"; } print_line() { echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; print_line_end; } print_line_end() { echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; } # 显示帮助信息 show_help() { print_header "🐳 Docker Compose 自动管理工具" echo -e "${BOLD}用法:${NC}" echo " $0 [命令]" echo "" echo -e "${BOLD}命令:${NC}" printf " %-15s - %s\n" "update" "更新所有服务 (pull + down + up)" printf " %-15s - %s\n" "down" "停止所有服务" printf " %-15s - %s\n" "up" "启动所有服务 (up -d)" printf " %-15s - %s\n" "list" "列出所有compose文件" printf " %-15s - %s\n" "help" "显示此帮助信息" printf " %-15s - %s\n" "alias-setup" "配置别名 'cpman' 以简化调用" printf " %-15s - %s\n" "(无参数)" "进入交互式菜单" echo "" echo -e "${BOLD}环境变量:${NC}" echo " MAX_DEPTH=N - 设置搜索深度(默认:$DEFAULT_MAX_DEPTH)" echo " IGNORE_DIR=name - 设置忽略目录(默认:$DEFAULT_IGNORE_DIR)" echo "" echo -e "${BOLD}示例:${NC}" local script_url="https://www.mgrsc.top/http/linux/sh/cpman.sh" echo " # 交互模式" echo " bash <(curl -fsSL $script_url)" echo "" echo " # 直接更新" echo " bash <(curl -fsSL $script_url) update" echo "" echo " # 自定义深度和忽略目录" echo " MAX_DEPTH=5 bash <(curl -fsSL $script_url) update" echo "" echo " # 配置别名" echo " bash <(curl -fsSL $script_url) alias-setup" echo "" } # 计算耗时 format_time() { local duration=$1 if (( duration < 60 )); then echo "${duration}s" else echo "$((duration / 60))m $((duration % 60))s" fi } # 检测 Compose 命令 detect_compose_command() { if command -v docker &> /dev/null && docker compose version &> /dev/null; then COMPOSE_CMD="docker compose" elif command -v docker-compose &> /dev/null; then COMPOSE_CMD="docker-compose" elif command -v podman &> /dev/null && podman compose version &> /dev/null; then COMPOSE_CMD="podman compose" else print_error "未找到可用的 Compose 命令 (docker compose / docker-compose / podman compose)" exit 1 fi # 不再打印检测信息,保持清爽,除非出错 } # 查找 Compose 文件 find_compose_files() { local search_dir="${1:-.}" local -n result=$2 print_info "正在搜索 Compose 文件 (深度: $MAX_DEPTH)..." # 使用 mapfile 提高效率 (Bash 4.0+) # 排除 IGNORE_DIR if mapfile -d $'\0' result < <(find "$search_dir" -maxdepth "$MAX_DEPTH" -type f \ \( -name "compose.yaml" -o -name "compose.yml" -o \ -name "docker-compose.yaml" -o -name "docker-compose.yml" \) \ -not -path "*/$IGNORE_DIR/*" -print0 2>/dev/null); then : # Success else # Fallback for older bash while IFS= read -r -d '' file; do result+=("$file") done < <(find "$search_dir" -maxdepth "$MAX_DEPTH" -type f \ \( -name "compose.yaml" -o -name "compose.yml" -o \ -name "docker-compose.yaml" -o -name "docker-compose.yml" \) \ -not -path "*/$IGNORE_DIR/*" -print0 2>/dev/null) fi if [ ${#result[@]} -eq 0 ]; then print_warning "未找到任何 Compose 文件" return 0 fi print_success "找到 ${#result[@]} 个服务配置文件" } # 显示文件列表 show_files() { local -n files=$1 print_header "📋 服务列表:" local idx=1 for file in "${files[@]}"; do printf " ${CYAN}%-3s${NC} %s\n" "$idx." "$file" ((idx++)) done echo "" } # 核心:执行更新 update_all() { local -n files=$1 local -a report_success=() local -a report_failed=() local total=${#files[@]} local current=1 print_header "🚀 开始更新所有服务..." for file in "${files[@]}"; do local start_t start_t=$(date +%s) local dir dir=$(dirname "$file") local filename filename=$(basename "$file") print_line echo -e "${BOLD}${ICON_PKG} [$current/$total] 正在处理: ${BLUE}$dir${NC}" local step_failed=false local err_msg="" # 1. Pull (关键修复:添加 < /dev/null) echo -n " 🔹 拉取镜像... " if output=$(cd "$dir" && $COMPOSE_CMD -f "$filename" pull 2>&1 < /dev/null); then echo -e "${GREEN}完成${NC}" else echo -e "${RED}失败${NC}" echo -e "${YELLOW}--- 错误日志 ---${NC}" echo "$output" echo -e "${YELLOW}----------------${NC}" step_failed=true err_msg="镜像拉取失败" fi # 2. Down (如果拉取没严重错误) if [ "$step_failed" = false ]; then echo -n " 🔹 停止服务... " if (cd "$dir" && $COMPOSE_CMD -f "$filename" down 2>&1 < /dev/null) >/dev/null; then echo -e "${GREEN}完成${NC}" else echo -e "${RED}失败${NC}" step_failed=true err_msg="停止服务失败" fi fi # 3. Up if [ "$step_failed" = false ]; then echo -n " 🔹 启动服务... " if (cd "$dir" && $COMPOSE_CMD -f "$filename" up -d 2>&1 < /dev/null) >/dev/null; then echo -e "${GREEN}完成${NC}" else echo -e "${RED}失败${NC}" step_failed=true err_msg="启动服务失败" fi fi local end_t end_t=$(date +%s) local duration=$((end_t - start_t)) if [ "$step_failed" = false ]; then report_success+=("$dir ($(format_time $duration))") else report_failed+=("$dir - $err_msg") fi ((current++)) done print_summary report_success report_failed } # 停止所有 down_all() { local -n files=$1 local -a report_success=() local -a report_failed=() for file in "${files[@]}"; do local dir dir=$(dirname "$file") local filename filename=$(basename "$file") echo -n "🛑 停止 $dir ... " if (cd "$dir" && $COMPOSE_CMD -f "$filename" down 2>&1 < /dev/null) >/dev/null; then echo -e "${GREEN}成功${NC}" report_success+=("$dir") else echo -e "${RED}失败${NC}" report_failed+=("$dir") fi done # 简易汇总 echo "" print_line echo -e "停止完成: ${GREEN}${ICON_SUCCESS} ${#report_success[@]}${NC} | ${RED}${ICON_FAIL} ${#report_failed[@]}${NC}" } # 启动所有 up_all() { local -n files=$1 local -a report_success=() local -a report_failed=() for file in "${files[@]}"; do local dir dir=$(dirname "$file") local filename filename=$(basename "$file") echo -n "🚀 启动 $dir ... " if (cd "$dir" && $COMPOSE_CMD -f "$filename" up -d 2>&1 < /dev/null) >/dev/null; then echo -e "${GREEN}成功${NC}" report_success+=("$dir") else echo -e "${RED}失败${NC}" report_failed+=("$dir") fi done } # 打印精美汇总表 print_summary() { local -n success=$1 local -n failed=$2 local total_time=$(($(date +%s) - SCRIPT_START_TIME)) echo "" print_header "📊 执行报告" print_line printf "${BOLD}%-35s %-10s${NC}\n" "服务路径" "状态" print_line for item in "${success[@]}"; do printf "%-35s ${GREEN}✅ 成功${NC}\n" "$item" done for item in "${failed[@]}"; do printf "%-35s ${RED}❌ 失败${NC}\n" "$item" done print_line echo -e "总耗时: ${ICON_TIME} ${YELLOW}$(format_time $total_time)${NC} | 成功: ${GREEN}${#success[@]}${NC} | 失败: ${RED}${#failed[@]}${NC}" print_line echo "" } # 配置别名 setup_alias() { local alias_name="cpman" local script_url="https://www.mgrsc.top/http/linux/sh/cpman.sh" local alias_cmd="alias $alias_name='bash <(curl -fsSL $script_url)'" local config_file="" print_header "⚙️ 配置别名: $alias_name" # 1. 确定 Shell 配置文件 if [[ "$SHELL" =~ "zsh" ]]; then config_file="$HOME/.zshrc" elif [[ "$SHELL" =~ "bash" ]]; then # 如果存在 .bash_profile 且 .bashrc 不存在,则使用 .bash_profile if [ -f "$HOME/.bash_profile" ] && [ ! -f "$HOME/.bashrc" ]; then config_file="$HOME/.bash_profile" else config_file="$HOME/.bashrc" fi else print_error "不支持或无法识别您的 Shell ($SHELL)。请手动将别名添加到您的配置文件中。" echo "别名命令: $alias_cmd" return 1 fi print_info "检测到 Shell: $SHELL" print_info "目标配置文件: $config_file" if [ ! -f "$config_file" ]; then print_warning "配置文件 $config_file 不存在,将创建它。" touch "$config_file" fi # 2. 检查是否已存在别名 if grep -q "alias $alias_name=" "$config_file"; then # 如果存在,则更新 sed -i "/alias $alias_name=/c\\$alias_cmd" "$config_file" print_success "别名 '$alias_name' 已在 $config_file 中更新。" else # 如果不存在,则追加 echo -e "\n# Docker Compose Manager Alias" >> "$config_file" echo "$alias_cmd" >> "$config_file" print_success "别名 '$alias_name' 已添加到 $config_file。" fi print_line print_info "请执行以下命令使别名立即生效:" echo -e "${YELLOW}source $config_file${NC}" print_line return 0 } # 菜单 show_menu() { print_header "🐳 Docker Compose 批量管理工具" echo "1. 🔄 更新 (Pull -> Down -> Up)" echo "2. 🛑 停止 (Down)" echo "3. 🚀 启动 (Up -d)" echo "4. 🔍 重新扫描" echo "5. 🚪 退出" echo -n "请输入选项 [1-5]: " } # 主逻辑 main() { local compose_files=() local command="${1:-interactive}" # 1. 立即处理非操作命令 (help, alias-setup) case "$command" in help) show_help exit 0 ;; alias-setup) setup_alias exit 0 ;; *) # 对于需要操作的命令,继续执行初始化 ;; esac # 2. 检查 Docker Compose 命令 detect_compose_command # 3. 查找 Compose 文件 find_compose_files "." compose_files if [ ${#compose_files[@]} -eq 0 ] && [[ "$command" != "interactive" ]]; then print_warning "未找到任何 Compose 文件,且未进入交互模式,脚本退出。" exit 0 fi # 4. 执行操作命令 case "$command" in update) show_files compose_files update_all compose_files ;; down) show_files compose_files down_all compose_files ;; up) show_files compose_files up_all compose_files ;; list) show_files compose_files ;; interactive) show_files compose_files while true; do show_menu read -r choice case $choice in 1) update_all compose_files ;; 2) down_all compose_files ;; 3) up_all compose_files ;; 4) compose_files=() find_compose_files "." compose_files show_files compose_files ;; 5) exit 0 ;; *) print_error "无效输入" ;; esac done ;; *) print_error "无效命令: $command" echo "请使用 '$0 help' 查看完整用法。" exit 1 ;; esac } main "$@"