testsAndMisc/meta/scripts/disk_cleanup_check.sh
Krzysztof Rudnicki b83ce5d3c7
Some checks failed
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
billsplit CI / quality (push) Has been cancelled
reverse-survivors CI / check (push) Has been cancelled
Fail the build on any shellcheck finding; fix the 222 the old gate hid
The shellcheck hook ran with --severity=warning, so info- and style-level
findings never failed a commit. 222 had accumulated across 94 of 244 shell
files. They were not all style noise -- six were bugs:

  - Five scripts in single_use/misc/testsAndMisc-bash/ sourced
    $SCRIPT_DIR/../../lib/common.sh, one directory too shallow. Nothing exists
    at that path, so all five failed at runtime.
  - setup_personal_website.sh and setup_kcd2_dice_solver.sh called usage()
    (which ends in `exit 0`) and then `exit 1` for an unknown command. The
    exit 1 was unreachable, so a typo'd subcommand reported SUCCESS.
  - Six i3blocks scripts had a `# shellcheck source=` directive missing the
    scripts/periodic_background/ path segment. The stale path had silently
    disabled source-following for all of them.
  - diagnose_arch_performance.sh word-split a newline-joined file list; when
    the find came back empty, awk got no file operands, fell through to
    reading stdin, and the diagnostic hung.
  - lib/common.sh's handle_arg_help_or_unknown ended in an unreachable
    `return 0` while documenting "Returns: 0 if handled" -- a contract no code
    path could satisfy.
  - deploy.sh's SCRIPT_DIR is reassigned by config.sh, which it sources at top
    level, and config.sh can repoint it at the on-device path
    /data/local/tmp/focus_mode. Renamed to DEPLOY_DIR.

The SC1091 bulk (81 findings) was resolved rather than silenced: .shellcheckrc
sets external-sources + source-path=SCRIPTDIR so sourced files are actually
followed and analysed. That is what surfaced the i3blocks and deploy.sh bugs.

17 `# shellcheck disable=` directives remain, each with a written reason, and
only where the finding is provably wrong: injection-test payloads that must not
expand, literal source text being grepped for, POSIX sh scripts on the phone
where arrays do not exist, and source paths that only exist at runtime.
Everything else was fixed in code.

Verified in a CLEAN worktree, not just my own tree. config.sh sources the
gitignored config_secrets.sh, which exists locally but not in a fresh checkout;
a working-tree-only check passed while ci_mirror.sh -- which lints HEAD in a
clean worktree, exactly as CI does -- failed on it.

The gate is verified to bite: a file whose only finding is SC2016 (info) fails
the hook, while the old --severity=warning gate exits 0 on it.

third_party/ is excluded -- vendored upstream, matching the prettier hook.

Not fixed, reported: check_polling_antipatterns.sh carries a pre-existing
SC2034 disable that hides a real bug -- its `patterns` array is never used, so
the checker detects nothing. Confirmed unchanged by this commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 19:49:11 +02:00

330 lines
9.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# disk_cleanup_check.sh — Analyze disk usage and suggest (or perform) cleanup.
#
# Usage:
# ./disk_cleanup_check.sh # Dry-run: report only
# ./disk_cleanup_check.sh --clean # Interactive: prompt before each action
#
# Safe by default: nothing is deleted without --clean AND user confirmation.
RED=$'\033[0;31m'
GREEN=$'\033[0;32m'
YELLOW=$'\033[1;33m'
CYAN=$'\033[0;36m'
BOLD=$'\033[1m'
RESET=$'\033[0m'
CLEAN=false
if [[ "${1:-}" == "--clean" ]]; then
CLEAN=true
fi
TOTAL_RECLAIMABLE=0
# ───────────────────── helpers ─────────────────────
human_readable() {
local kb=$1
if ((kb >= 1048576)); then
printf "%.1f GB" "$(echo "scale=1; $kb / 1048576" | bc)"
elif ((kb >= 1024)); then
printf "%.1f MB" "$(echo "scale=1; $kb / 1024" | bc)"
else
printf "%d KB" "$kb"
fi
}
dir_size_kb() {
local dir="$1"
if [[ -d "$dir" ]]; then
du -sk "$dir" 2>/dev/null | awk '{print $1}'
else
echo 0
fi
}
report() {
local label="$1" size_kb="$2" detail="${3:-}"
if ((size_kb > 0)); then
local hr
hr=$(human_readable "$size_kb")
printf "${YELLOW}%-40s${RESET} %10s" "$label" "$hr"
if [[ -n "$detail" ]]; then
printf " ${CYAN}(%s)${RESET}" "$detail"
fi
printf "\n"
TOTAL_RECLAIMABLE=$((TOTAL_RECLAIMABLE + size_kb))
fi
}
# Returns 0 if user confirms, 1 otherwise. Always 1 in dry-run.
confirm() {
local prompt="$1"
if $CLEAN; then
printf "${BOLD} → %s [y/N]: ${RESET}" "$prompt"
read -r ans
if [[ "$ans" =~ ^[Yy]$ ]]; then
return 0
fi
fi
return 1
}
# Safe wrapper: confirm + action, never fails under errexit
try_clean() {
local prompt="$1"
shift
if confirm "$prompt"; then
"$@"
printf "%s ✓ Done%s\n" "${GREEN}" "${RESET}"
fi
return 0
}
# ───────────────────── checks ─────────────────────
printf "\n%s=== Disk Cleanup Analysis ===%s\n\n" "${BOLD}" "${RESET}"
printf "${BOLD}%-40s %10s${RESET}\n" "Category" "Reclaimable"
printf '%s\n' "$(printf '%.0s─' {1..60})"
# 1. Trash
trash_dir="$HOME/.local/share/Trash"
size=$(dir_size_kb "$trash_dir")
report "Trash" "$size" "empty trash"
if ((size > 0)); then
try_clean "Empty trash?" \
rm -rf "${trash_dir}/files" "${trash_dir}/info" "${trash_dir}/expunged"
fi
# 2. Pacman cache
pacman_cache_size=$(dir_size_kb "/var/cache/pacman/pkg")
if ((pacman_cache_size > 0)); then
if command -v paccache &>/dev/null; then
pkg_count=$(find /var/cache/pacman/pkg/ -maxdepth 1 -type f -name '*.pkg.tar*' 2>/dev/null | wc -l)
report "Pacman cache ($pkg_count pkgs)" "$pacman_cache_size" \
"sudo paccache -rk2"
try_clean "Clean pacman cache (keep last 2 versions)?" \
sudo paccache -rk2
else
report "Pacman cache (install pacman-contrib)" "$pacman_cache_size" \
"pacman -S pacman-contrib && paccache -rk2"
fi
fi
# 3. Yay build cache
yay_cache="$HOME/.cache/yay"
size=$(dir_size_kb "$yay_cache")
report "Yay AUR build cache" "$size" "rm -rf ~/.cache/yay"
if ((size > 0)); then
try_clean "Clear yay build cache?" rm -rf "${yay_cache:?}"
fi
# 4. Pip cache
pip_cache="$HOME/.cache/pip"
size=$(dir_size_kb "$pip_cache")
report "Pip cache" "$size" "pip cache purge"
if ((size > 0)); then
try_clean "Clear pip cache?" rm -rf "${pip_cache:?}"
fi
# 5. uv cache
uv_cache="$HOME/.cache/uv"
size=$(dir_size_kb "$uv_cache")
report "uv cache" "$size" "uv cache clean"
if ((size > 0)); then
try_clean "Clear uv cache?" rm -rf "${uv_cache:?}"
fi
# 6. HuggingFace cache
hf_cache="$HOME/.cache/huggingface"
size=$(dir_size_kb "$hf_cache")
report "HuggingFace models" "$size" "manual review recommended"
# 7. Suno cache
suno_cache="$HOME/.cache/suno"
size=$(dir_size_kb "$suno_cache")
report "Suno AI cache" "$size" "manual review recommended"
# 8. Go build cache
go_cache="$HOME/.cache/go-build"
size=$(dir_size_kb "$go_cache")
report "Go build cache" "$size" "go clean -cache"
if ((size > 0)); then
try_clean "Clear Go build cache?" rm -rf "${go_cache:?}"
fi
# 9. Yarn cache
yarn_cache="$HOME/.cache/yarn"
size=$(dir_size_kb "$yarn_cache")
report "Yarn cache" "$size" "yarn cache clean"
if ((size > 0)); then
try_clean "Clear yarn cache?" rm -rf "${yarn_cache:?}"
fi
# 10. npm cache
npm_cache="$HOME/.npm/_cacache"
size=$(dir_size_kb "$npm_cache")
report "npm cache" "$size" "npm cache clean --force"
if ((size > 0)); then
try_clean "Clear npm cache?" rm -rf "${npm_cache:?}"
fi
# 11. Electron cache
electron_cache="$HOME/.cache/electron"
size=$(dir_size_kb "$electron_cache")
report "Electron cache" "$size"
if ((size > 0)); then
try_clean "Clear Electron cache?" rm -rf "${electron_cache:?}"
fi
# 12. Thumbnails
thumb_cache="$HOME/.cache/thumbnails"
size=$(dir_size_kb "$thumb_cache")
report "Thumbnails cache" "$size"
if ((size > 0)); then
try_clean "Clear thumbnails?" rm -rf "${thumb_cache:?}"
fi
# 13. vscode-cpptools cache
cpptools_cache="$HOME/.cache/vscode-cpptools"
size=$(dir_size_kb "$cpptools_cache")
if ((size > 102400)); then
report "VS Code cpptools cache" "$size"
try_clean "Clear cpptools cache?" rm -rf "${cpptools_cache:?}"
fi
# 14. Bazel cache
bazel_cache="$HOME/.cache/bazel"
size=$(dir_size_kb "$bazel_cache")
report "Bazel cache" "$size"
if ((size > 0)); then
try_clean "Clear bazel cache?" rm -rf "${bazel_cache:?}"
fi
# 15. Browser caches (report only)
for browser_cache in \
"$HOME/.cache/thorium" \
"$HOME/.cache/google-chrome" \
"$HOME/.cache/mozilla" \
"$HOME/.cache/BraveSoftware"; do
if [[ -d "$browser_cache" ]]; then
name=$(basename "$browser_cache")
size=$(dir_size_kb "$browser_cache")
if ((size > 102400)); then
report "Browser cache: $name" "$size" "close browser first"
fi
fi
done
# 16. Docker
if command -v docker &>/dev/null; then
docker_dir="/var/lib/docker"
size=$(dir_size_kb "$docker_dir" || echo 0)
if ((size > 1048576)); then
report "Docker (images+build cache)" "$size" "docker system prune -a"
try_clean "Docker system prune (removes all unused)?" \
docker system prune -af --volumes
fi
fi
# 17. Orphan packages
orphans=$(pacman -Qdtq 2>/dev/null || true)
orphan_count=0
if [[ -n "$orphans" ]]; then
orphan_count=$(echo "$orphans" | wc -l)
fi
if ((orphan_count > 0)); then
orphan_size=$(echo "$orphans" | xargs pacman -Qi 2>/dev/null |
awk '/Installed Size/{
size=$4; unit=$5;
if (unit ~ /GiB/) total += size*1048576;
else if (unit ~ /MiB/) total += size*1024;
else if (unit ~ /KiB/) total += size;
} END{printf "%d", total}' || echo 0)
report "Orphan packages ($orphan_count)" "$orphan_size" \
"sudo pacman -Rns \$(pacman -Qdtq)"
if confirm "Remove $orphan_count orphan packages?"; then
# shellcheck disable=SC2046
sudo pacman -Rns $(pacman -Qdtq) --noconfirm
printf "%s ✓ Orphans removed%s\n" "${GREEN}" "${RESET}"
fi
fi
# 18. Journal logs — vacuum to 200M
journal_line=$(journalctl --disk-usage 2>&1 || true)
journal_kb=0
if [[ "$journal_line" =~ ([0-9.]+)G ]]; then
journal_kb=$(echo "${BASH_REMATCH[1]} * 1048576 / 1" | bc)
elif [[ "$journal_line" =~ ([0-9.]+)M ]]; then
journal_kb=$(echo "${BASH_REMATCH[1]} * 1024 / 1" | bc)
fi
if ((journal_kb > 204800)); then
excess=$((journal_kb - 204800))
report "Journal logs (excess over 200M)" "$excess" "journalctl --vacuum-size=200M"
try_clean "Vacuum journal to 200M?" \
sudo journalctl --vacuum-size=200M
fi
# 19. AUR source dirs — old archives (>30 days)
aur_dir="$HOME/aur"
if [[ -d "$aur_dir" ]]; then
old_archives_kb=$(find "$aur_dir" \
\( -name "*.pkg.tar.zst" -o -name "*.tar.gz" -o -name "*.tar.xz" \
-o -name "*.zip" -o -name "*.tar.bz2" \) \
-mtime +30 -printf '%k\n' 2>/dev/null |
awk '{t+=$1} END{print t+0}')
if ((old_archives_kb > 0)); then
report "AUR old archives (>30d)" "$old_archives_kb" \
"find ~/aur '*.pkg.tar.zst' -mtime +30 -delete"
if confirm "Delete old AUR archives (>30 days)?"; then
find "$aur_dir" \
\( -name "*.pkg.tar.zst" -o -name "*.tar.gz" -o -name "*.tar.xz" \
-o -name "*.zip" -o -name "*.tar.bz2" \) \
-mtime +30 -delete
printf "%s ✓ Old AUR archives deleted%s\n" "${GREEN}" "${RESET}"
fi
fi
fi
# ───────────────────── report-only items ─────────────────────
printf "\n%s--- Report-only (manual review needed) ---%s\n" "${BOLD}" "${RESET}"
for manual_entry in \
"$HOME/Downloads/too_big:~/Downloads/too_big" \
"$HOME/Downloads:~/Downloads total" \
"$HOME/inne:~/inne" \
"/Games:/Games — still playing?"; do
dir="${manual_entry%%:*}"
label="${manual_entry#*:}"
size=$(dir_size_kb "$dir")
if ((size > 0)); then
hr=$(human_readable "$size")
printf "${RED}%-40s %10s${RESET} ${CYAN}(review manually)${RESET}\n" \
"$label" "$hr"
fi
done
# ───────────────────── summary ─────────────────────
printf "\n%s\n" "$(printf '%.0s─' {1..60})"
total_hr=$(human_readable "$TOTAL_RECLAIMABLE")
printf "${BOLD}Total auto-reclaimable: ${GREEN}%s${RESET}\n" "$total_hr"
read -r used_kb total_kb used_pct <<<"$(df -k / | awk 'NR==2{print $3, $2, $5}')"
used_pct="${used_pct%\%}"
printf "${BOLD}Current usage: %s / %s (%s%%)${RESET}\n" \
"$(human_readable "$used_kb")" "$(human_readable "$total_kb")" "$used_pct"
if ((TOTAL_RECLAIMABLE > 0)); then
new_used=$((used_kb - TOTAL_RECLAIMABLE))
if ((new_used < 0)); then new_used=0; fi
new_pct=$((new_used * 100 / total_kb))
printf "${BOLD}After cleanup: %s / %s (%d%%)${RESET}\n" \
"$(human_readable "$new_used")" "$(human_readable "$total_kb")" "$new_pct"
fi
if ! $CLEAN; then
printf "\n%sRun with --clean to interactively clean each category.%s\n" "${CYAN}" "${RESET}"
fi
printf "\n"