一键脚本
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/compose_all_upgrade.sh) /vol1/1000/compose "排除目录1 排除目录2"
bash <(curl -sL ../sh/compose_all_upgrade.sh) /vol1/1000/compose "排除目录1 排除目录2"
# DRY-RUN 模式预览操作(不实际执行)
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/compose_all_upgrade.sh) /vol1/1000/compose --dry-run
| 模式 | 命令 | 说明 |
|---|---|---|
| 交互模式 | ./脚本名.sh |
交互式选择项目 |
| 传参模式 | ./脚本名.sh 项目根目录 |
直接管理指定项目 |
| 排除目录 | ./脚本名.sh 目标目录 "排除1 排除2" |
目标目录在前,排除列表在后 |
| 排除目录 | ./脚本名.sh "排除1 排除2" 目标目录 |
排除列表在前,目标目录在后 |
| DRY-RUN | ./脚本名.sh 目标目录 --dry-run |
预览还原操作,不实际执行 |
效果预览
补充说明
该脚本用于批量递归更新指定目录下所有 Docker Compose 项目,基于 docker-compose/docker compose 命令实现,适合需要一键更新多个容器项目的场景。
功能特点
- 全自动批量更新:递归扫描并拉取最新镜像、重新创建容器
- 智能更新检测:区分镜像更新、容器更新、无变化等情况
- 项目名识别:支持从 docker-compose.yml、.env 中提取项目名称
- 状态报告生成:更新前自动扫描并生成项目状态报告(/tmp/compose-status.txt)
- 项目状态还原:支持根据状态报告还原项目(停止运行中的、停止已停止的、下线已删除的)
- DRY-RUN 模式:
--dry-run参数预览还原操作,不实际执行 - 自动清理镜像:更新后执行 docker image prune 清理无用镜像
- 容器状态显示:更新后展示每个项目的容器运行状态
- 详细统计报告:显示总计、成功、失败、有更新、无更新的项目数量
- 耗时统计:记录整个更新过程的开始和结束时间
- 灵活参数解析:支持目标目录与排除目录任意顺序,自动识别
输出说明
脚本输出包含以下字段:
| 字段 | 说明 |
|---|---|
| 开始更新时间 | 显示更新开始的时间戳 |
| 目标目录 | 显示扫描的目标目录路径 |
| 排除目录 | 显示配置的排除目录列表 |
| 扫描进度 | 显示正在扫描的目标目录 |
| 状态报告 | 生成项目状态报告文件路径及统计汇总 |
| 处理进度 | 显示当前处理的目录和项目名称 |
| 镜像拉取状态 | 显示 pull 命令的执行结果 |
| 容器更新状态 | 显示 up 命令的执行结果和更新类型 |
| 容器状态 | 更新后的容器数量和运行状态 |
| 镜像清理 | 显示无用镜像清理结果 |
| 统计信息 | 总计项目、成功、失败数量 |
| 更新项目列表 | 列出有实际更新的项目 |
| 无更新项目列表 | 列出无变化的项目 |
| 还原操作 | DRY-RUN 模式下预览还原操作,或实际执行还原 |
| 更新用时 | 显示整个更新过程的总耗时 |
注意事项
- 脚本需要 Docker 和 docker-compose/docker compose 命令可用
- 更新操作会重新创建容器,但保留数据卷
- 仅扫描目标目录的直接子目录(非递归深度扫描)
- 支持 docker-compose.yml、docker-compose.yaml、compose.yml、compose.yaml 四种文件名
- 更新失败的项目会显示错误输出(最多5行)
- 建议更新后检查容器健康状态,确保服务正常运行
- 脚本会自动检测并使用可用的 compose 命令(docker-compose 或 docker compose)
- 排除目录匹配基于目录名或相对路径(基于目标目录)
- 状态报告文件
/tmp/compose-status.txt会在还原阶段结束后自动删除 - DRY-RUN 模式(
--dry-run)仅预览还原操作,不实际执行 stop/down 命令
脚本源码
#!/bin/bash
# ============================================================================
# 颜色定义
# ============================================================================
gl_hui='\033[37m'
gl_hong='\033[31m'
gl_lv='\033[32m'
gl_huang='\033[33m'
gl_lan='\033[34m'
gl_bai='\033[97m'
gl_zi='\033[35m'
gl_bufan='\033[96m'
gl_info='\033[94m'
gl_reset='\033[0m'
# ============================================================================
# 全局变量
# ============================================================================
TARGET_DIR=""
EXCLUDE_DIRS=()
DRY_RUN=0
OUTPUT_FILE="/tmp/compose-status.txt"
TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')"
# 统计变量
SUCCESS=0
FAIL=0
UPDATED_PROJECTS=()
NO_UPDATE_PROJECTS=()
# ============================================================================
# 辅助函数
# ============================================================================
is_directory() { [[ -d "$1" ]] && return 0 || return 1; }
show_help() {
cat << HELPTEXT
用法: $0 [选项] [目标目录] ["排除目录1 排除目录2 ..."] 或 $0 [选项] ["排除目录1 排除目录2 ..."] [目标目录]
选项:
--dry-run 模拟执行,不实际修改容器状态
--help 显示此帮助
说明:
- 目标目录和排除目录的顺序可以互换,脚本会自动识别存在的目录作为目标目录。
- 排除目录用空格分隔的字符串表示,支持相对路径(相对于目标目录)或目录名。
示例:
$0 /vol1/1000/compose "test1 test2 test3" # 目标目录在前
$0 "test1 test2 test3" /vol1/1000/compose # 排除列表在前
$0 /vol1/1000/compose # 仅目标目录
$0 --dry-run /vol1/1000/compose # 模拟运行
HELPTEXT
}
# ============================================================================
# 参数解析(支持 --dry-run 和 --help)
# ============================================================================
parse_args() {
local args=()
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
--help) show_help; exit 0 ;;
*) args+=("$arg") ;;
esac
done
if [[ ${#args[@]} -eq 0 ]]; then
TARGET_DIR="."
return
fi
if [[ ${#args[@]} -eq 1 ]]; then
TARGET_DIR="${args[0]}"
return
fi
if [[ ${#args[@]} -eq 2 ]]; then
if is_directory "${args[0]}"; then
TARGET_DIR="${args[0]}"
read -ra EXCLUDE_DIRS <<< "${args[1]}"
elif is_directory "${args[1]}"; then
TARGET_DIR="${args[1]}"
read -ra EXCLUDE_DIRS <<< "${args[0]}"
else
echo -e "${gl_hong}❌ 错误: 无法识别目标目录,请确保其中一个参数是存在的目录路径${gl_bai}"
exit 1
fi
return
fi
echo -e "${gl_huang}⚠️ 参数过多,将使用位置参数解析 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
TARGET_DIR="${args[0]}"
local combined_excludes="${args[1]}"
shift 2
for extra in "$@"; do
combined_excludes="$combined_excludes $extra"
done
read -ra EXCLUDE_DIRS <<< "$combined_excludes"
}
parse_args "$@"
# ============================================================================
# 基础检查
# ============================================================================
if [[ -d "$TARGET_DIR" ]]; then
TARGET_DIR=$(realpath "$TARGET_DIR")
else
echo -e "${gl_hong}❌ 错误: 目标目录不存在: $TARGET_DIR${gl_bai}"
exit 1
fi
if ! command -v docker &>/dev/null; then
echo -e "${gl_hong}❌ 未找到 docker 命令,请确保 Docker 已安装。${gl_bai}"
exit 1
fi
COMPOSE_CMD=$(command -v docker-compose || echo "docker compose")
if ! $COMPOSE_CMD version &>/dev/null; then
echo -e "${gl_hong}❌ 未找到可用的 docker compose 命令。${gl_bai}"
exit 1
fi
# ============================================================================
# 函数:判断项目是否被排除
# ============================================================================
is_excluded() {
local dir="$1"
local dir_name=$(basename "$dir")
for pattern in "${EXCLUDE_DIRS[@]}"; do
if [[ "$dir_name" == "$pattern" || "$dir" == *"/$pattern" ]]; then
return 0
fi
done
return 1
}
# ============================================================================
# 函数:获取 Compose 项目名称(优先从 compose 文件或 .env 读取)
# ============================================================================
get_project_name() {
local dir="${1:-}"
if [[ -z "$dir" ]]; then
echo "unknown"
return
fi
local dir_name=$(basename "$dir")
local project_name="$dir_name"
local compose_file=""
for f in docker-compose.yml docker-compose.yaml compose.yml compose.yaml; do
if [[ -f "$dir/$f" ]]; then
compose_file="$dir/$f"
break
fi
done
if [[ -n "$compose_file" ]] && grep -q "^name:" "$compose_file" 2>/dev/null; then
local extracted_name
extracted_name=$(grep "^name:" "$compose_file" | head -1 | sed 's/^name:[[:space:]]*//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr -d '\r' | tr -d "'\"")
[[ -n "$extracted_name" ]] && project_name="$extracted_name"
fi
if [[ -f "$dir/.env" ]] && grep -q "COMPOSE_PROJECT_NAME" "$dir/.env" 2>/dev/null; then
local env_name
env_name=$(grep "COMPOSE_PROJECT_NAME" "$dir/.env" | head -1 | cut -d'=' -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr -d '\r' | tr -d "'\"")
[[ -n "$env_name" ]] && project_name="$env_name"
fi
echo "$project_name"
}
# ============================================================================
# 函数:显示容器状态
# ============================================================================
display_container_status() {
local compose_path="$1"
local container_count running_count
container_count=$($COMPOSE_CMD -f "$compose_path" ps -q 2>/dev/null | wc -l)
running_count=$($COMPOSE_CMD -f "$compose_path" ps --filter status=running -q 2>/dev/null | wc -l)
if [[ $container_count -gt 0 ]]; then
echo -e "${gl_bai}容器状态: ${gl_lv}✓${gl_bai} 发现 ${gl_bufan}$container_count ${gl_bai}个容器,其中 ${gl_bufan}$running_count ${gl_bai}个在运行"
else
echo -e "${gl_bai}容器状态: ${gl_huang}⚠️ 未发现运行中的容器${gl_bai}"
fi
}
# ============================================================================
# 函数:更新单个项目(串行),并记录更新状态
# ============================================================================
update_project() {
local dir="$1"
local index="$2"
local project_name=$(get_project_name "$dir")
local ret=0
echo -e "${gl_bai}[${gl_bufan}$index${gl_bai}]${gl_zi} >>> 处理目录: ${gl_huang}$dir${gl_bai}"
echo -e "${gl_bai}项目名称: ${gl_huang}$project_name${gl_bai}"
# 找到 compose 文件
local compose_file=""
for f in docker-compose.yml docker-compose.yaml compose.yml compose.yaml; do
if [[ -f "$dir/$f" ]]; then
compose_file="$f"
break
fi
done
if [[ -z "$compose_file" ]]; then
echo -e "${gl_hong}❌ 未找到 compose 文件${gl_bai}"
return 1
fi
local compose_path="$dir/$compose_file"
if [[ $DRY_RUN -eq 1 ]]; then
echo -e " ${gl_bufan}[DRY-RUN]${gl_bai} $COMPOSE_CMD -f $compose_path pull --quiet"
echo -e " ${gl_bufan}[DRY-RUN]${gl_bai} $COMPOSE_CMD -f $compose_path up -d --remove-orphans"
return 0
fi
echo -e "${gl_bai}正在拉取镜像中 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
local pull_output
pull_output=$($COMPOSE_CMD -f "$compose_path" pull --quiet 2>&1)
local pull_exit=$?
echo -e "${gl_bai}正在更新容器中 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
local up_output
up_output=$($COMPOSE_CMD -f "$compose_path" up -d --remove-orphans 2>&1)
local up_exit=$?
# 判断是否有实际更新
local has_update=false update_type=""
if [[ $pull_exit -eq 0 ]] && echo "$pull_output" | grep -q -E "Downloaded newer image|Status: Downloaded newer image"; then
has_update=true; update_type="镜像更新"
fi
if [[ $up_exit -eq 0 ]] && echo "$up_output" | grep -q -E "Recreating|Creating|Starting|Started"; then
if [[ -n "$update_type" ]]; then update_type="镜像+容器更新"; else has_update=true; update_type="容器更新"; fi
fi
if [[ "$has_update" == "true" ]]; then
echo -e "${gl_lv}✅ 更新成功 ${gl_huang}(${update_type})${gl_bai}"
UPDATED_PROJECTS+=("$project_name")
else
echo -e "${gl_lv}✅ 更新完成 (无变化)${gl_bai}"
NO_UPDATE_PROJECTS+=("$project_name")
fi
if [[ $pull_exit -ne 0 ]] || [[ $up_exit -ne 0 ]]; then
echo -e "${gl_hong}❌ 更新失败${gl_bai}"
[[ $pull_exit -ne 0 ]] && echo -e "${gl_huang}Pull错误: ${gl_hui}$(echo "$pull_output" | head -5)${gl_bai}"
[[ $up_exit -ne 0 ]] && echo -e "${gl_huang}Up错误: ${gl_hui}$(echo "$up_output" | head -5)${gl_bai}"
ret=1
else
display_container_status "$compose_path"
fi
return $ret
}
# ============================================================================
# 主流程
# ============================================================================
echo ""
start_time=$(date '+%F %T'); start_ts=$(date +%s)
echo -e "${gl_bai}开始更新时间:${gl_lv}$start_time${gl_bai}"
echo -e "${gl_bai}目标目录:${gl_huang}$TARGET_DIR${gl_bai}"
if [[ ${#EXCLUDE_DIRS[@]} -gt 0 ]]; then
echo -e "${gl_bai}排除目录:${gl_huang}${EXCLUDE_DIRS[*]}${gl_bai}"
fi
if [[ $DRY_RUN -eq 1 ]]; then
echo -e "${gl_bufan}🔍 模拟运行模式 (--dry-run)${gl_bai}"
fi
# ----------------------------------------------------------------------------
# 第一步:扫描所有项目,收集信息并生成状态报告
# ----------------------------------------------------------------------------
echo -e "${gl_hui}🔍 正在扫描 $TARGET_DIR ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
declare -a project_infos=()
cnt_running=0
cnt_stopped=0
cnt_removed=0
cnt_total=0
tmp_running=$(mktemp)
tmp_stopped=$(mktemp)
tmp_removed=$(mktemp)
trap 'rm -f "$tmp_running" "$tmp_stopped" "$tmp_removed" "$OUTPUT_FILE" 2>/dev/null' EXIT
for project_dir in "$TARGET_DIR"/*/; do
[[ -d "$project_dir" ]] || continue
project_abs="${project_dir%/}"
compose_file=""
for f in docker-compose.yml docker-compose.yaml compose.yml compose.yaml; do
if [[ -f "$project_dir/$f" ]]; then
compose_file="$f"
break
fi
done
[[ -z "$compose_file" ]] && continue
cnt_total=$((cnt_total + 1))
project_name=$(get_project_name "$project_abs")
containers=$($COMPOSE_CMD -f "$project_dir/$compose_file" ps -a --format '{{.Name}}|{{.State}}' 2>/dev/null)
if [[ -z "$containers" ]]; then
cnt_removed=$((cnt_removed + 1))
status="removed"
detail="无容器"
echo "$project_name|$project_abs|$compose_file|$status|$detail" >> "$tmp_removed"
else
has_running=0
state_detail=""
while IFS='|' read -r cname cstate; do
[[ -z "$cname" ]] && continue
if [[ "$cstate" == "running" ]]; then
has_running=1
fi
state_detail="${state_detail}${cname}:${cstate}, "
done <<< "$containers"
state_detail=${state_detail%, }
if [[ $has_running -eq 1 ]]; then
cnt_running=$((cnt_running + 1))
status="running"
echo "$project_name|$project_abs|$compose_file|$status|$state_detail" >> "$tmp_running"
else
cnt_stopped=$((cnt_stopped + 1))
status="stopped"
echo "$project_name|$project_abs|$compose_file|$status|$state_detail" >> "$tmp_stopped"
fi
fi
project_infos+=("$project_abs|$compose_file|$project_name|$status")
done
# 生成状态报告文件
{
echo "Docker Compose 项目状态报告"
echo "扫描时间: $TIMESTAMP"
echo "扫描目录: $TARGET_DIR"
echo "项目总数: $cnt_total"
echo "运行中: $cnt_running"
echo "已停止: $cnt_stopped"
echo "已删除: $cnt_removed"
echo ""
echo "运行中项目:"
if [[ -s "$tmp_running" ]]; then
while IFS='|' read -r pname ppath pfile pstate pdetail; do
echo "$pname $ppath $pfile $pstate $pdetail"
done < "$tmp_running"
else
echo "(无)"
fi
echo ""
echo "已停止项目:"
if [[ -s "$tmp_stopped" ]]; then
while IFS='|' read -r pname ppath pfile pstate pdetail; do
echo "$pname $ppath $pfile $pstate $pdetail"
done < "$tmp_stopped"
else
echo "(无)"
fi
echo ""
echo "已删除项目:"
if [[ -s "$tmp_removed" ]]; then
while IFS='|' read -r pname ppath pfile pstate pdetail; do
echo "$pname $ppath $pfile $pstate $pdetail"
done < "$tmp_removed"
else
echo "(无)"
fi
echo ""
echo "报告生成完毕"
} > "$OUTPUT_FILE"
echo -e "${gl_lv}✅ 报告已生成: $OUTPUT_FILE${gl_bai}"
echo -e "${gl_hui}📊 总计: ${gl_huang}$cnt_total ${gl_bai}个项目 | 运行: ${gl_lv}$cnt_running${gl_bai} | 停止: ${gl_hong}$cnt_stopped${gl_bai} | 删除: ${gl_zi}$cnt_removed${gl_bai}"
# 如果没有项目,退出
if [[ $cnt_total -eq 0 ]]; then
echo -e "${gl_huang}⚠️ 未找到任何项目,退出。${gl_bai}"
exit 0
fi
# ----------------------------------------------------------------------------
# 第二步:过滤出需要更新的项目(排除已排除的)
# ----------------------------------------------------------------------------
echo -e "${gl_bai}开始更新直接子目录中的 Docker Compose 项目 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
filtered_projects=()
for info in "${project_infos[@]}"; do
IFS='|' read -r dir compose_file pname status <<< "$info"
if is_excluded "$dir"; then
echo -e "${gl_hui}⏭️ 跳过已排除目录: $(basename "$dir")${gl_bai}"
else
filtered_projects+=("$dir|$compose_file|$pname")
fi
done
total_projects=${#filtered_projects[@]}
if [[ $total_projects -eq 0 ]]; then
echo -e "${gl_huang}⚠️ 所有找到的目录均被排除,无项目可更新。${gl_bai}"
else
echo -e "${gl_bai}待更新项目数: ${gl_bufan}$total_projects${gl_bai}"
echo ""
# 串行更新每个项目
job_index=0
for entry in "${filtered_projects[@]}"; do
IFS='|' read -r dir compose_file pname <<< "$entry"
((job_index++))
if update_project "$dir" "$job_index"; then
((SUCCESS++))
else
((FAIL++))
fi
echo "" # 空行分隔
done
fi
# ----------------------------------------------------------------------------
# 第三步:清理无用镜像(只删除 dangling)
# ----------------------------------------------------------------------------
echo ""
echo -e "${gl_bai}正在清理无用镜像 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
if [[ $DRY_RUN -eq 1 ]]; then
echo -e " ${gl_bufan}[DRY-RUN]${gl_bai} docker image prune -f --filter 'dangling=true'"
else
docker image prune -f --filter "dangling=true" >/dev/null 2>&1 && echo -e "${gl_bai}镜像清理: ${gl_lv}♻️ 清理完成${gl_bai}"
fi
# ----------------------------------------------------------------------------
# 第四步:还原项目状态(根据状态文件)
# ----------------------------------------------------------------------------
echo ""
echo -e "${gl_zi} >>> 还原项目状态${gl_bai}"
echo -e "${gl_huang}📄 正在解析 ${gl_lv}$OUTPUT_FILE ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
if [[ ! -f "$OUTPUT_FILE" ]]; then
echo -e "${gl_hong}❌ 状态文件不存在: $OUTPUT_FILE${gl_bai}"
else
current_section=""
cnt_stop=0
cnt_down=0
cnt_skip=0
cnt_ignore=0
run_cmd() {
if [[ $DRY_RUN -eq 1 ]]; then
echo -e " ${gl_bufan}[DRY-RUN]${gl_bai} $*"
else
echo -e " ${gl_hui}→${gl_bai} $*"
"$@" >/dev/null 2>&1
fi
}
while IFS= read -r line; do
case "$line" in
*"运行中项目"*) current_section="running"; continue ;;
*"已停止项目"*) current_section="stopped"; continue ;;
*"已删除项目"*) current_section="removed"; continue ;;
*"查询超时项目"*) current_section="timeout"; continue ;;
esac
echo "$line" | grep -q '/vol1/1000/compose/' || continue
if [[ "$current_section" == "running" ]]; then
cnt_ignore=$((cnt_ignore + 1))
continue
fi
pname=$(echo "$line" | awk '{print $1}')
ppath=$(echo "$line" | awk '{print $2}')
pfile=$(echo "$line" | awk '{print $3}')
compose_full="${ppath}/${pfile}"
if [[ ! -f "$compose_full" ]]; then
echo -e " ${gl_hong}⚠ 跳过(文件不存在): $compose_full${gl_bai}"
cnt_skip=$((cnt_skip + 1))
continue
fi
case "$current_section" in
stopped)
echo -e "${gl_bufan}【已停止】${gl_bai} $pname"
run_cmd $COMPOSE_CMD -f "$compose_full" stop
cnt_stop=$((cnt_stop + 1))
;;
removed)
echo -e "${gl_hong}【已删除】${gl_bai} $pname"
run_cmd $COMPOSE_CMD -f "$compose_full" down
cnt_down=$((cnt_down + 1))
;;
timeout)
echo -e "${gl_hong}【超时跳过】${gl_bai} $pname (状态查询超时,需手动确认)"
cnt_skip=$((cnt_skip + 1))
;;
esac
echo ""
done < "$OUTPUT_FILE"
echo -e "${gl_lv}执行完毕${gl_bai}: 停止项目:${gl_hong}${cnt_stop} ${gl_bai}删除项目:${gl_zi}${cnt_down} ${gl_bai}忽略(运行中):${gl_huang}${cnt_ignore} ${gl_bai}跳过:${gl_lv}${cnt_skip}${gl_bai}"
rm -f "$OUTPUT_FILE"
echo -e "${gl_hui}已删除状态文件: $OUTPUT_FILE${gl_bai}"
fi
# ============================================================================
# 最终统计与打印更新/无更新项目列表
# ============================================================================
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e " ${gl_bai}统计信息${gl_bai}"
echo -e " ${gl_bai}总计成功: ${gl_lv}$SUCCESS${gl_bai}"
echo -e " ${gl_bai}总计失败: ${gl_hong}$FAIL${gl_bai}"
if [[ ${#UPDATED_PROJECTS[@]} -gt 0 ]]; then
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e " ${gl_bai}有实际更新的项目 (${gl_lv}${#UPDATED_PROJECTS[@]}${gl_bai}个):"
for i in "${!UPDATED_PROJECTS[@]}"; do
project_name="${UPDATED_PROJECTS[$i]}"
echo -e " ${gl_lv}✓${gl_bai} $((i+1)). $project_name"
done
else
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e " ${gl_hui}无项目更新${gl_bai}"
fi
if [[ ${#NO_UPDATE_PROJECTS[@]} -gt 0 ]]; then
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e " ${gl_bai}无更新的项目 (${gl_bufan}${#NO_UPDATE_PROJECTS[@]}${gl_bai}个):"
for i in "${!NO_UPDATE_PROJECTS[@]}"; do
project_name="${NO_UPDATE_PROJECTS[$i]}"
echo -e " ${gl_lv}○${gl_bai} $((i+1)). $project_name"
done
fi
# 结束时间
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
end_time=$(date '+%F %T'); end_ts=$(date +%s)
total=$((end_ts - start_ts))
printf -v dur "%d时%02d分%02d秒" $((total/3600)) $(((total%3600)/60)) $((total%60))
echo -e "${gl_bai}结束更新时间:${gl_hong}$end_time${gl_bai}"
echo -e "${gl_bai}更新用时共计:${gl_lv}$dur${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if [[ $DRY_RUN -eq 0 ]]; then
if [[ ${#UPDATED_PROJECTS[@]} -gt 0 ]]; then
echo -e "${gl_zi}💡 提示: 有 ${gl_lv}${#UPDATED_PROJECTS[@]}${gl_zi} 个项目已更新,建议进行健康检查${gl_bai}"
fi
if [[ $FAIL -gt 0 ]]; then
echo -e "${gl_huang}⚠️ 注意: 有 ${gl_hong}$FAIL${gl_huang} 个项目更新失败,请检查日志${gl_bai}"
fi
fi
创建本地脚本
new_script="new_test.sh"
cat > "$new_script" << 'EOF'
#!/bin/bash
# 粘贴脚本源码
EOF
# 保留本地脚本,去掉 rm -f "$new_script"
chmod +x "$new_script" && ./"$new_script" && rm -f "$new_script"