#!/bin/bash # ============================================================ # 清理评测后遗留的 Docker 镜像 # # 用途: # SWE-bench、Terminal-Bench 等 benchmark 会拉取大量 Docker 镜像, # 评测完后很占磁盘。本脚本按命名空间批量删除这些镜像。 # # 用法: # # 默认只预览,不会真的删除 # bash bash/cleanup_docker_images.sh --preset swe # # # 确认删除 # bash bash/cleanup_docker_images.sh --preset swe --yes # # # 删除 terminal-bench 镜像 # bash bash/cleanup_docker_images.sh --preset terminal --yes # # # 删除所有常见 benchmark 镜像 # bash bash/cleanup_docker_images.sh --preset all-bench --yes # # # 自定义匹配规则(支持多个,逗号分隔) # bash bash/cleanup_docker_images.sh --pattern 'swebench/*,alexgshaw/*' --yes # # # 同时清理 dangling 镜像和未使用卷 # bash bash/cleanup_docker_images.sh --preset all-bench --yes --prune # ============================================================ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # 预设的 benchmark 镜像命名空间 PRESET_SWE=('swebench/*') PRESET_TERMINAL=('alexgshaw/*') PRESET_ALL_BENCH=() PRESET_ALL_BENCH+=("${PRESET_SWE[@]}") PRESET_ALL_BENCH+=("${PRESET_TERMINAL[@]}") DRY_RUN=1 PRESET='' PATTERNS='' PRUNE=0 usage() { sed -n '7,34p' "$0" } while [[ $# -gt 0 ]]; do case "$1" in --preset) PRESET="${2:-}" shift 2 ;; --pattern) PATTERNS="${2:-}" shift 2 ;; --yes) DRY_RUN=0 shift ;; --prune) PRUNE=1 shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown option: $1" usage exit 1 ;; esac done # 解析出要删除的 pattern 列表 PATTERN_LIST=() if [[ -n "$PRESET" ]]; then case "$PRESET" in swe) PATTERN_LIST+=("${PRESET_SWE[@]}") ;; terminal) PATTERN_LIST+=("${PRESET_TERMINAL[@]}") ;; all-bench) PATTERN_LIST+=("${PRESET_ALL_BENCH[@]}") ;; *) echo "ERROR: unknown preset '$PRESET'. Available: swe, terminal, all-bench" exit 1 ;; esac fi if [[ -n "$PATTERNS" ]]; then IFS=',' read -ra CUSTOM_PATTERNS <<< "$PATTERNS" PATTERN_LIST+=("${CUSTOM_PATTERNS[@]}") fi if [[ ${#PATTERN_LIST[@]} -eq 0 ]]; then echo "ERROR: 请指定 --preset 或 --pattern" usage exit 1 fi echo "============================================================" echo "清理目标 pattern:" for p in "${PATTERN_LIST[@]}"; do echo " - $p" done echo "============================================================" # 收集所有匹配且没有被运行中容器使用的镜像 ID MATCHED_IMAGES=() RUNNING_IMAGES=$(docker ps --format '{{.Image}}' | sort -u) for pattern in "${PATTERN_LIST[@]}"; do while IFS= read -r img; do [[ -z "$img" ]] && continue repo="${img%:*}" # 检查是否有运行中容器在使用该镜像 if echo "$RUNNING_IMAGES" | grep -qx "$img"; then echo "SKIP (running container uses): $img" continue fi # 避免重复 if [[ ! " ${MATCHED_IMAGES[*]} " =~ " ${img} " ]]; then MATCHED_IMAGES+=("$img") fi done < <(docker images --format '{{.Repository}}:{{.Tag}}' --filter=reference="$pattern") done if [[ ${#MATCHED_IMAGES[@]} -eq 0 ]]; then echo "没有匹配到可删除的镜像" exit 0 fi echo "" echo "匹配到 ${#MATCHED_IMAGES[@]} 个镜像:" for img in "${MATCHED_IMAGES[@]}"; do size=$(docker images --format '{{.Size}}' "$img" | head -1) echo " $img ($size)" done # 计算标记总大小(注意共享层会被多次计算,仅作参考) TOTAL_SIZE_GB=$(docker images --format '{{.Size}} {{.Repository}}:{{.Tag}}' \ | awk -v img_list="${MATCHED_IMAGES[*]}" ' BEGIN { n = split(img_list, imgs, " "); for (i=1; i<=n; i++) target[imgs[i]] = 1; } { size = $1; img = $2; if (!(img in target)) next; unit = substr(size, length(size)-1); val = substr(size, 1, length(size)-2); if (unit == "GB") sum += val * 1024 * 1024 * 1024; else if (unit == "MB") sum += val * 1024 * 1024; else if (unit == "kB") sum += val * 1024; } END { printf "%.2f", sum / 1024 / 1024 / 1024; }') echo "" echo "标记总大小约: ${TOTAL_SIZE_GB} GB(含共享层,实际释放会小于该值)" if [[ "$DRY_RUN" -eq 1 ]]; then echo "" echo "当前是预览模式,不会删除。如需删除请加上 --yes" exit 0 fi echo "" echo "开始删除..." FAILED=0 for img in "${MATCHED_IMAGES[@]}"; do if docker rmi -f "$img" >/dev/null 2>&1; then echo " DELETED $img" else echo " FAILED $img" FAILED=1 fi done if [[ "$PRUNE" -eq 1 ]]; then echo "" echo "清理 dangling 镜像和未使用卷..." docker image prune -f docker volume prune -f fi echo "" echo "============================================================" if [[ "$FAILED" -eq 0 ]]; then echo "清理完成" else echo "部分镜像删除失败" fi echo "============================================================"