Initial commit

This commit is contained in:
2026-04-19 21:43:13 +07:00
commit b66b6589c8
6 changed files with 1206 additions and 0 deletions
Executable
+859
View File
@@ -0,0 +1,859 @@
#!/bin/bash
# cleanup.sh
# Script dọn dẹp các ứng dụng nặng và các bản cũ trên macOS
# Sử dụng: bash cleanup.sh
# -----------------------------
# Function: cleanup chung cho thư mục chứa version
# $1 = label (vd: "Edge", "Edge Updater")
# $2 = đường dẫn thư mục
# -----------------------------
cleanup_versions() {
local label="$1"
local target_dir="$2"
echo "--- [$label] ---"
echo " $target_dir"
if [ ! -d "$target_dir" ]; then
echo " ⚠ Thư mục không tồn tại."
return
fi
pushd "$target_dir" > /dev/null || return
# Lấy các thư mục version dạng x.y.z.w và sort theo version số.
local raw_dirs=()
local d
shopt -s nullglob
for d in [0-9]*.[0-9]*.[0-9]*.[0-9]*; do
[[ -d "$d" ]] && raw_dirs+=("$d")
done
shopt -u nullglob
if [ ${#raw_dirs[@]} -eq 0 ]; then
echo " ⚠ Thư mục rỗng, không có phiên bản nào."
popd > /dev/null
return
fi
local dirs=()
while IFS= read -r d; do
[[ -n "$d" ]] && dirs+=("$d")
done < <(
printf '%s\n' "${raw_dirs[@]}" |
awk -F'.' '{ printf "%05d.%05d.%05d.%05d\t%s\n", $1, $2, $3, $4, $0 }' |
sort |
cut -f2-
)
local last_index=$(( ${#dirs[@]} - 1 ))
local keep="${dirs[$last_index]}"
# Helper: lấy size thư mục
dir_size() { du -sh "$1" 2>/dev/null | cut -f1 | xargs; }
if [ ${#dirs[@]} -le 1 ]; then
echo " ✓ Phiên bản hiện tại: $keep ($(dir_size "$keep")) — không có bản cũ."
popd > /dev/null
return
fi
echo " ✓ Giữ lại: $keep ($(dir_size "$keep"))"
echo " ✗ Sẽ xóa:"
local i
for ((i = 0; i < ${#dirs[@]} - 1; i++)); do
echo " - ${dirs[$i]} ($(dir_size "${dirs[$i]}"))"
done
local confirm
read -r -p " Xác nhận xóa? (Y/n): " confirm
if [[ -n "$confirm" && ! "$confirm" =~ ^([yY]|[yY][eE][sS])$ ]]; then
echo " Đã hủy."
popd > /dev/null
return
fi
for ((i = 0; i < ${#dirs[@]} - 1; i++)); do
echo " Đã xóa: ${dirs[$i]}"
rm -rf -- "${dirs[$i]}"
done
popd > /dev/null
}
# -----------------------------
# Function: Cleanup Go mod cache
# -----------------------------
cleanup_go() {
echo "--- [Go Mod Cache] ---"
if ! command -v go &> /dev/null; then
echo " ⚠ go command not found."
return
fi
local GOMODCACHE
GOMODCACHE=$(go env GOMODCACHE)
if [ -d "$GOMODCACHE" ]; then
echo " $GOMODCACHE"
local size
size=$(du -sh "$GOMODCACHE" 2>/dev/null | cut -f1 | xargs)
echo " ✗ Sẽ xóa bộ nhớ cache: $size"
local confirm
read -r -p " Xác nhận xóa? (Y/n): " confirm
if [[ -n "$confirm" && ! "$confirm" =~ ^([yY]|[yY][eE][sS])$ ]]; then
echo " Đã hủy."
return
fi
go clean -modcache
echo " Đã xong."
else
echo " ✓ Bộ nhớ cache rỗng hoặc không tồn tại."
fi
}
# -----------------------------
# Function: Cleanup NPM cache
# -----------------------------
cleanup_npm() {
echo "--- [NPM Cache] ---"
if ! command -v npm &> /dev/null; then
echo " ⚠ npm command not found."
return
fi
local NPM_CACHE
NPM_CACHE=$(npm config get cache)
if [ -d "$NPM_CACHE" ]; then
echo " $NPM_CACHE"
local size
# Lưu ý: npm cache size thực tế thường lớn hơn kết quả du vì cấu trúc thư mục phức tạp
size=$(du -sh "$NPM_CACHE" 2>/dev/null | cut -f1 | xargs)
echo " ✗ Sẽ xóa bộ nhớ cache: $size"
local confirm
read -r -p " Xác nhận xóa? (Y/n): " confirm
if [[ -n "$confirm" && ! "$confirm" =~ ^([yY]|[yY][eE][sS])$ ]]; then
echo " Đã hủy."
return
fi
npm cache clean --force
echo " Đã xong."
else
echo " ✓ Bộ nhớ cache rỗng hoặc không tồn tại."
fi
}
# -----------------------------
# Function: Cleanup old VS Code extensions
# Giữ lại phiên bản mới nhất của mỗi extension
# -----------------------------
cleanup_vscode_extensions() {
echo "--- [VS Code Extensions] ---"
local EXT_DIR="$HOME/.vscode/extensions"
echo " $EXT_DIR"
if [ ! -d "$EXT_DIR" ]; then
echo " ⚠ Thư mục không tồn tại."
return
fi
# Build "ext_id<TAB>dirname" pairs
local entries=()
local basename ext_id
shopt -s nullglob
for dir in "$EXT_DIR"/*/; do
basename=$(command basename "$dir")
[[ "$basename" == .* ]] && continue
# Extract extension ID: everything before -<version>
ext_id=$(echo "$basename" | sed 's/-[0-9][0-9.]*.*$//')
[ -z "$ext_id" ] && continue
entries+=("${ext_id}"$'\t'"${basename}")
done
shopt -u nullglob
if [ ${#entries[@]} -eq 0 ]; then
echo " ⚠ Không có extension nào."
return
fi
# Sort by ext_id (field 1), then by dirname (field 2) for version ordering
local sorted
sorted=$(printf '%s\n' "${entries[@]}" | sort -t$'\t' -k1,1 -k2,2)
# Group by ext_id, find old versions
local to_delete=()
local to_keep=()
local group=()
local prev_id=""
while IFS=$'\t' read -r ext_id dirname; do
if [ -n "$prev_id" ] && [ "$ext_id" != "$prev_id" ]; then
# Process previous group
if [ ${#group[@]} -gt 1 ]; then
local last=$(( ${#group[@]} - 1 ))
to_keep+=("${group[$last]}")
local g
for ((g = 0; g < last; g++)); do
to_delete+=("${group[$g]}")
done
fi
group=()
fi
group+=("$dirname")
prev_id="$ext_id"
done <<< "$sorted"
# Process last group
if [ ${#group[@]} -gt 1 ]; then
local last=$(( ${#group[@]} - 1 ))
to_keep+=("${group[$last]}")
local g
for ((g = 0; g < last; g++)); do
to_delete+=("${group[$g]}")
done
fi
if [ ${#to_delete[@]} -eq 0 ]; then
echo " ✓ Không có extension cũ để xóa."
return
fi
echo " ✓ Giữ lại (mới nhất):"
local k
for k in "${to_keep[@]}"; do
local size
size=$(du -sh "$EXT_DIR/$k" 2>/dev/null | cut -f1 | xargs)
echo " + $k ($size)"
done
echo " ✗ Sẽ xóa (cũ):"
local d
for d in "${to_delete[@]}"; do
local size
size=$(du -sh "$EXT_DIR/$d" 2>/dev/null | cut -f1 | xargs)
echo " - $d ($size)"
done
local confirm
read -r -p " Xác nhận xóa? (Y/n): " confirm
if [[ -n "$confirm" && ! "$confirm" =~ ^([yY]|[yY][eE][sS])$ ]]; then
echo " Đã hủy."
return
fi
for d in "${to_delete[@]}"; do
echo " Đã xóa: $d"
rm -rf -- "$EXT_DIR/$d"
done
}
# -----------------------------
# Function: Cleanup old Antigravity browser recordings
# -----------------------------
cleanup_agy_recordings() {
local days="${1:-7}"
local rec_dir="$HOME/.gemini/antigravity/browser_recordings"
echo "--- [Antigravity Recordings] ---"
echo " $rec_dir"
if [ ! -d "$rec_dir" ]; then
echo " ⚠ Thư mục không tồn tại."
return
fi
local to_delete=()
local total_size=0
local dir
shopt -s nullglob
for dir in "$rec_dir"/*/; do
[ -d "$dir" ] || continue
local dirname
dirname=$(basename "$dir")
local size mod_date
size=$(du -sh "$dir" 2>/dev/null | cut -f1 | xargs)
mod_date=$(stat -f "%Sm" -t "%Y-%m-%d" "$dir")
if [ "$(find "$dir" -maxdepth 0 -mtime +"$days" 2>/dev/null)" ]; then
echo " - $dirname ($size, $mod_date)"
to_delete+=("$dir")
total_size=$((total_size + $(du -sk "$dir" | cut -f1)))
else
echo " + $dirname ($size, $mod_date) [KEEP]"
fi
done
shopt -u nullglob
if [ ${#to_delete[@]} -eq 0 ]; then
echo " ✓ Không có recording cũ (>${days} ngày) để xóa."
return
fi
local total_human
total_human=$(echo "scale=1; $total_size / 1024" | bc)
echo " ✗ Sẽ xóa ${#to_delete[@]} recording(s), tổng: ${total_human} MiB"
local confirm
read -r -p " Xác nhận xóa? (Y/n): " confirm
if [[ -n "$confirm" && ! "$confirm" =~ ^([yY]|[yY][eE][sS])$ ]]; then
echo " Đã hủy."
return
fi
for dir in "${to_delete[@]}"; do
echo " Đã xóa: $(basename "$dir")"
rm -rf -- "$dir"
done
}
# -----------------------------
# Function: Cleanup Homebrew
# -----------------------------
cleanup_homebrew() {
echo "--- [Homebrew] ---"
if ! command -v brew &> /dev/null; then
echo " ⚠ brew command not found."
return
fi
echo " Đang cập nhật Homebrew..."
brew update --quiet
local outdated
outdated=$(brew outdated 2>/dev/null)
if [ -n "$outdated" ]; then
echo " ✗ Các package cần upgrade:"
echo "$outdated" | sed 's/^/ - /'
local confirm
read -r -p " Upgrade tất cả? (Y/n): " confirm
if [[ -z "$confirm" || "$confirm" =~ ^([yY]|[yY][eE][sS])$ ]]; then
brew upgrade --quiet
echo " Đã upgrade xong."
else
echo " Bỏ qua upgrade."
fi
else
echo " ✓ Tất cả package đã cập nhật."
fi
# Cleanup cache
local cache_size
cache_size=$(brew --cache 2>/dev/null)
if [ -d "$cache_size" ]; then
local size
size=$(du -sh "$cache_size" 2>/dev/null | cut -f1 | xargs)
echo " ✗ Cache: $size ($cache_size)"
fi
echo " Đang dọn dẹp cache và autoremove..."
brew cleanup --prune=all -s 2>/dev/null
brew autoremove 2>/dev/null
echo " Đã xong."
}
# -----------------------------
# Function: Cleanup VS Code C++ Tools cache
# -----------------------------
cleanup_vscode_cpptools() {
local cache_dir="$HOME/Library/Caches/vscode-cpptools"
echo "--- [VS Code C++ Tools Cache] ---"
echo " $cache_dir"
if [ ! -d "$cache_dir" ]; then
echo " ⚠ Thư mục không tồn tại."
return
fi
local total_size
total_size=$(du -sh "$cache_dir" 2>/dev/null | cut -f1 | xargs)
echo " Tổng dung lượng: $total_size"
# ipch (IntelliSense cache) - thường chiếm nhiều nhất
if [ -d "$cache_dir/ipch" ]; then
local ipch_size
ipch_size=$(du -sh "$cache_dir/ipch" 2>/dev/null | cut -f1 | xargs)
echo " - ipch (IntelliSense cache): $ipch_size"
fi
# Các thư mục hash cache khác
local hash_count=0
local hash_size=0
local d
shopt -s nullglob
for d in "$cache_dir"/*/; do
local dirname
dirname=$(basename "$d")
[[ "$dirname" == "ipch" ]] && continue
hash_count=$((hash_count + 1))
hash_size=$((hash_size + $(du -sk "$d" | cut -f1)))
done
shopt -u nullglob
if [ $hash_count -gt 0 ]; then
local hash_human
hash_human=$(echo "scale=1; $hash_size / 1024" | bc)
echo " - $hash_count thư mục cache khác: ${hash_human} MiB"
fi
echo " ✗ Sẽ xóa toàn bộ cache"
local confirm
read -r -p " Xác nhận xóa? (Y/n): " confirm
if [[ -n "$confirm" && ! "$confirm" =~ ^([yY]|[yY][eE][sS])$ ]]; then
echo " Đã hủy."
return
fi
rm -rf -- "$cache_dir"/*
echo " Đã xóa toàn bộ cache. VS Code sẽ tạo lại khi cần."
}
# -----------------------------
# Function: Cleanup IDE logs (JetBrains, Google, etc.)
# Giữ lại phiên bản mới nhất của mỗi IDE
# $1 = label, $2 = đường dẫn thư mục logs
# -----------------------------
cleanup_ide_logs() {
local label="$1"
local logs_dir="$2"
echo "--- [$label Logs] ---"
echo " $logs_dir"
if [ ! -d "$logs_dir" ]; then
echo " ⚠ Thư mục không tồn tại."
return
fi
local to_delete=()
local all_dirs=()
shopt -s nullglob
for d in "$logs_dir"/*/; do
local dirname
dirname=$(basename "$d")
[[ "$dirname" == "Toolbox" || "$dirname" == ".DS_Store" ]] && continue
all_dirs+=("$dirname")
done
shopt -u nullglob
if [ ${#all_dirs[@]} -eq 0 ]; then
echo " ✓ Không có log nào."
return
fi
local sorted
sorted=$(printf '%s\n' "${all_dirs[@]}" | sort)
local prev_ide=""
local group=()
while IFS= read -r dirname; do
[ -z "$dirname" ] && continue
local ide_name
ide_name=$(echo "$dirname" | sed 's/[0-9].*$//')
[ -z "$ide_name" ] && continue
if [ -n "$prev_ide" ] && [ "$ide_name" != "$prev_ide" ]; then
if [ ${#group[@]} -gt 0 ]; then
local last_index=$(( ${#group[@]} - 1 ))
local keep="${group[$last_index]}"
local keep_size
keep_size=$(du -sh "$logs_dir/$keep" 2>/dev/null | cut -f1 | xargs)
echo " [$prev_ide]"
echo " + $keep ($keep_size) [KEEP]"
local i
for ((i = 0; i < last_index; i++)); do
local old="${group[$i]}"
local old_size
old_size=$(du -sh "$logs_dir/$old" 2>/dev/null | cut -f1 | xargs)
echo " - $old ($old_size)"
to_delete+=("$logs_dir/$old")
done
fi
group=()
fi
group+=("$dirname")
prev_ide="$ide_name"
done <<< "$sorted"
# Process the last group
if [ ${#group[@]} -gt 0 ]; then
local last_index=$(( ${#group[@]} - 1 ))
local keep="${group[$last_index]}"
local keep_size
keep_size=$(du -sh "$logs_dir/$keep" 2>/dev/null | cut -f1 | xargs)
echo " [$prev_ide]"
echo " + $keep ($keep_size) [KEEP]"
local i
for ((i = 0; i < last_index; i++)); do
local old="${group[$i]}"
local old_size
old_size=$(du -sh "$logs_dir/$old" 2>/dev/null | cut -f1 | xargs)
echo " - $old ($old_size)"
to_delete+=("$logs_dir/$old")
done
fi
if [ ${#to_delete[@]} -eq 0 ]; then
echo " ✓ Không có log cũ để xóa."
return
fi
local confirm
read -r -p " Xác nhận xóa ${#to_delete[@]} thư mục cũ? (Y/n): " confirm
if [[ -n "$confirm" && ! "$confirm" =~ ^([yY]|[yY][eE][sS])$ ]]; then
echo " Đã hủy."
return
fi
for d in "${to_delete[@]}"; do
echo " Đã xóa: $(basename "$d")"
rm -rf -- "$d"
done
}
# -----------------------------
# Function: Cleanup pip cache
# -----------------------------
cleanup_pip() {
echo "--- [Pip Cache] ---"
if ! command -v pip3 &> /dev/null; then
echo " ⚠ pip3 command not found."
return
fi
local cache_dir
cache_dir=$(pip3 cache dir 2>/dev/null)
if [ -d "$cache_dir" ]; then
local size
size=$(du -sh "$cache_dir" 2>/dev/null | cut -f1 | xargs)
echo " $cache_dir"
echo " ✗ Cache: $size"
local confirm
read -r -p " Xác nhận xóa? (Y/n): " confirm
if [[ -n "$confirm" && ! "$confirm" =~ ^([yY]|[yY][eE][sS])$ ]]; then
echo " Đã hủy."
return
fi
pip3 cache purge 2>/dev/null
echo " Đã xong."
else
echo " ✓ Cache rỗng."
fi
}
# -----------------------------
# Function: Cleanup deno cache
# -----------------------------
cleanup_deno() {
echo "--- [Deno Cache] ---"
local deno_cache="$HOME/Library/Caches/deno"
echo " $deno_cache"
if [ ! -d "$deno_cache" ]; then
echo " ⚠ Thư mục không tồn tại."
return
fi
local size
size=$(du -sh "$deno_cache" 2>/dev/null | cut -f1 | xargs)
echo " ✗ Cache: $size"
local confirm
read -r -p " Xác nhận xóa? (Y/n): " confirm
if [[ -n "$confirm" && ! "$confirm" =~ ^([yY]|[yY][eE][sS])$ ]]; then
echo " Đã hủy."
return
fi
rm -rf -- "$deno_cache"/*
echo " Đã xong."
}
# -----------------------------
# Function: Cleanup go build cache
# -----------------------------
cleanup_go_build() {
echo "--- [Go Build Cache] ---"
if ! command -v go &> /dev/null; then
echo " ⚠ go command not found."
return
fi
local cache_dir
cache_dir=$(go env GOCACHE)
if [ -d "$cache_dir" ]; then
local size
size=$(du -sh "$cache_dir" 2>/dev/null | cut -f1 | xargs)
echo " $cache_dir"
echo " ✗ Cache: $size"
local confirm
read -r -p " Xác nhận xóa? (Y/n): " confirm
if [[ -n "$confirm" && ! "$confirm" =~ ^([yY]|[yY][eE][sS])$ ]]; then
echo " Đã hủy."
return
fi
go clean -cache
echo " Đã xong."
else
echo " ✓ Cache rỗng."
fi
}
# -----------------------------
# Function: Cleanup generic cache directory
# $1 = label, $2 = đường dẫn thư mục
# -----------------------------
cleanup_cache_dir() {
local label="$1"
local cache_dir="$2"
echo "--- [$label Cache] ---"
echo " $cache_dir"
if [ ! -d "$cache_dir" ]; then
echo " ⚠ Thư mục không tồn tại."
return
fi
local size
size=$(du -sh "$cache_dir" 2>/dev/null | cut -f1 | xargs)
echo " ✗ Dung lượng: $size"
local confirm
read -r -p " Xác nhận xóa? (Y/n): " confirm
if [[ -n "$confirm" && ! "$confirm" =~ ^([yY]|[yY][eE][sS])$ ]]; then
echo " Đã hủy."
return
fi
rm -rf -- "$cache_dir"/*
echo " Đã xong."
}
# =============================================================================
# Interactive Menu System
# =============================================================================
# Tên các nhóm cleanup
MENU_ITEMS=(
"All"
"Browsers (Edge, Mozilla, Playwright)"
"IDE (VS Code, AntiGravity)"
"Dev Caches (Go, NPM, Pip, Deno)"
"IDE Logs (JetBrains, Google)"
"Other Caches (Raspberry Pi)"
"Homebrew"
)
# Trạng thái chọn (0=off, 1=on), mặc định All=on
MENU_SELECTED=(1 1 1 1 1 1 1)
MENU_CURSOR=0
MENU_COUNT=${#MENU_ITEMS[@]}
# Vẽ menu lên terminal
draw_menu() {
# Di chuyển cursor lên đầu menu để vẽ lại
if [ "$1" = "redraw" ]; then
printf "\033[%dA" "$((MENU_COUNT + 2))"
fi
echo " Chọn mục cần dọn dẹp (↑↓: di chuyển | Space: chọn/bỏ chọn | Enter: chạy các mục có [✓]):"
echo ""
local i
for ((i = 0; i < MENU_COUNT; i++)); do
local prefix=" "
[ $i -eq $MENU_CURSOR ] && prefix="▸ "
local check="[ ]"
[ "${MENU_SELECTED[$i]}" -eq 1 ] && check="[✓]"
local label="${MENU_ITEMS[$i]}"
# In dòng, xóa hết ký tự cũ trên dòng
printf "\r\033[K%s%s %s\n" "$prefix" "$check" "$label"
done
}
# Toggle All: chọn hoặc bỏ tất cả
toggle_all() {
local new_val="${MENU_SELECTED[0]}"
# Nếu All đang bật → tắt hết, ngược lại → bật hết
if [ "$new_val" -eq 1 ]; then
new_val=0
else
new_val=1
fi
local i
for ((i = 0; i < MENU_COUNT; i++)); do
MENU_SELECTED[$i]=$new_val
done
}
# Cập nhật lại trạng thái All dựa trên các item con
sync_all() {
local all_on=1
local i
for ((i = 1; i < MENU_COUNT; i++)); do
if [ "${MENU_SELECTED[$i]}" -eq 0 ]; then
all_on=0
break
fi
done
MENU_SELECTED[0]=$all_on
}
# Chạy interactive menu
run_menu() {
echo ""
echo "===== 🧹 Cleanup macOS ====="
echo ""
draw_menu
while true; do
# Đọc 1 ký tự (không echo, không buffer)
IFS= read -rsn1 key
case "$key" in
# ESC sequence (arrow keys)
$'\x1b')
read -rsn2 seq
case "$seq" in
'[A') # Up
MENU_CURSOR=$(( (MENU_CURSOR - 1 + MENU_COUNT) % MENU_COUNT ))
;;
'[B') # Down
MENU_CURSOR=$(( (MENU_CURSOR + 1) % MENU_COUNT ))
;;
esac
;;
# Space = toggle
' ')
if [ $MENU_CURSOR -eq 0 ]; then
toggle_all
else
if [ "${MENU_SELECTED[$MENU_CURSOR]}" -eq 1 ]; then
MENU_SELECTED[$MENU_CURSOR]=0
else
MENU_SELECTED[$MENU_CURSOR]=1
fi
sync_all
fi
;;
# Enter = confirm
'')
echo ""
break
;;
esac
draw_menu "redraw"
done
}
# =============================================================================
# Hàm chạy từng nhóm
# =============================================================================
run_browsers() {
cleanup_versions "Edge" "$HOME/Library/Application Support/Microsoft/EdgeUpdater/apps/msedge-stable"
echo ""
cleanup_versions "Edge Updater" "$HOME/Library/Application Support/Microsoft/EdgeUpdater/apps/msedge-updater"
echo ""
cleanup_cache_dir "Mozilla" "$HOME/Library/Caches/Mozilla"
echo ""
cleanup_cache_dir "Playwright" "$HOME/Library/Caches/ms-playwright"
echo ""
}
run_ide() {
cleanup_vscode_extensions
echo ""
cleanup_vscode_cpptools
echo ""
cleanup_cache_dir "VS Code ShipIt" "$HOME/Library/Caches/com.microsoft.VSCode.ShipIt"
echo ""
cleanup_agy_recordings
echo ""
}
run_dev_caches() {
cleanup_go
echo ""
cleanup_go_build
echo ""
cleanup_npm
echo ""
cleanup_pip
echo ""
cleanup_deno
echo ""
}
run_ide_logs() {
cleanup_ide_logs "JetBrains" "$HOME/Library/Logs/JetBrains"
echo ""
cleanup_ide_logs "Google" "$HOME/Library/Logs/Google"
echo ""
}
run_other_caches() {
cleanup_cache_dir "Raspberry Pi" "$HOME/Library/Caches/Raspberry Pi"
echo ""
}
run_homebrew() {
cleanup_homebrew
echo ""
}
# =============================================================================
# Main
# =============================================================================
# Map index → function
GROUP_FUNCS=(
"" # 0 = All (handled separately)
"run_browsers" # 1
"run_ide" # 2
"run_dev_caches" # 3
"run_ide_logs" # 4
"run_other_caches" # 5
"run_homebrew" # 6
)
run_menu
# Kiểm tra có mục nào được chọn không
has_selection=0
for ((i = 1; i < MENU_COUNT; i++)); do
if [ "${MENU_SELECTED[$i]}" -eq 1 ]; then
has_selection=1
break
fi
done
if [ "$has_selection" -eq 0 ]; then
echo "Không có mục nào được chọn. Thoát."
exit 0
fi
# Chạy các nhóm đã chọn
echo "===== Bắt đầu cleanup ====="
echo ""
for ((i = 1; i < MENU_COUNT; i++)); do
if [ "${MENU_SELECTED[$i]}" -eq 1 ]; then
${GROUP_FUNCS[$i]}
fi
done
echo "===== ✅ Cleanup hoàn tất ====="