|
#!/usr/bin/env bash |
|
# bersihkan - Pembersih project: hapus node_modules, build output, cache framework secara rekursif |
|
# v2.5.1 - Disclaimer: push ke git dulu sebelum hapus |
|
# Usage: bersihkan [PATH] [OPTIONS] |
|
|
|
set -e |
|
|
|
GREEN='\033[0;32m' |
|
YELLOW='\033[1;33m' |
|
RED='\033[0;31m' |
|
CYAN='\033[0;36m' |
|
DIM='\033[2m' |
|
NC='\033[0m' |
|
|
|
TARGET="." |
|
DRY_RUN=false |
|
INCLUDE_VENDOR=false |
|
VERBOSE=false |
|
AUTO_YES=false |
|
SORT="name" # name | size |
|
EXCLUDE=() |
|
|
|
usage() { |
|
cat <<'EOF' |
|
Usage: bersihkan [PATH] [OPTIONS] |
|
|
|
PATH Folder target (default: current folder ".") |
|
Scan dilakukan REKURSIF dari PATH tersebut. |
|
|
|
OPTIONS: |
|
-n, --dry-run Hanya tampilkan apa yang akan dihapus (tidak menghapus) |
|
-v, --verbose Tampilkan detail + ukuran (lebih lambat) |
|
--vendor Juga hapus vendor/ (PHP/Laravel/Go) |
|
--sort <mode> Urutan: name (default, A-Z) | size (terbesar dulu) |
|
-e, --exclude <pat> Kecualikan folder/file/ekstensi (bisa dipakai berulang) |
|
Contoh: -e vendor -e "*.log" -e "bin" -e ".next" |
|
-h, --help Tampilkan bantuan ini |
|
-y, --yes Langsung hapus tanpa konfirmasi |
|
|
|
Contoh: |
|
bersihkan # urut nama |
|
bersihkan --sort=size # urut ukuran |
|
bersihkan -e vendor -e bin # jangan hapus vendor & bin |
|
bersihkan --exclude="*.log" --dry-run # jangan hapus *.log |
|
bersihkan -e ".next" -e "dist" -y # kecualikan .next & dist |
|
bersihkan . --vendor -y # hapus termasuk vendor/ |
|
|
|
Yang dihapus (rekursif, semua ditemukan akan dihapus): |
|
JS/Node/React/Next/Nuxt/Astro/Vite: |
|
node_modules, .next, .nuxt, .output, .vercel, .turbo, out, dist, build, |
|
.parcel-cache, .vite, coverage, .nyc_output, .cache |
|
|
|
Laravel/PHP: |
|
bootstrap/cache/*.php, storage/framework/views/*, |
|
storage/framework/cache/*, storage/framework/sessions/*, |
|
storage/logs/*.log (+ vendor/ jika --vendor) |
|
|
|
Go: |
|
bin/, vendor/ (jika --vendor), *.out, *.test |
|
|
|
Rust: target/ (jika ada Cargo.toml) |
|
Java/Kotlin: target/, build/, .gradle/, out/ (jika ada pom.xml/build.gradle) |
|
Flutter/Dart: .dart_tool/, build/ (jika ada pubspec.yaml) |
|
Terraform: .terraform/ (jika ada *.tf) |
|
|
|
Python: __pycache__, .pytest_cache, .mypy_cache, .ruff_cache |
|
Umum: .DS_Store |
|
EOF |
|
} |
|
|
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
-h|--help) usage; exit 0 ;; |
|
-n|--dry-run) DRY_RUN=true; shift ;; |
|
-v|--verbose) VERBOSE=true; shift ;; |
|
--vendor) INCLUDE_VENDOR=true; shift ;; |
|
--sort) SORT="$2"; shift 2 ;; |
|
--sort=*) SORT="${1#--sort=}"; shift ;; |
|
-e|--exclude) EXCLUDE+=("$2"); shift 2 ;; |
|
--exclude=*) EXCLUDE+=("${1#--exclude=}"); shift ;; |
|
-y|--yes) AUTO_YES=true; shift ;; |
|
--) shift; break ;; |
|
-*) echo -e "${RED}Unknown option: $1${NC}"; usage; exit 1 ;; |
|
*) TARGET="$1"; shift ;; |
|
esac |
|
done |
|
|
|
if [[ "$SORT" != "name" && "$SORT" != "size" ]]; then |
|
echo -e "${RED}✗ --sort harus 'name' atau 'size' (dapat: $SORT)${NC}" |
|
exit 1 |
|
fi |
|
if [[ "$SORT" == "size" ]]; then |
|
VERBOSE=true |
|
fi |
|
|
|
if [[ ! -d "$TARGET" ]]; then |
|
echo -e "${RED}✗ Folder tidak ditemukan: $TARGET${NC}" |
|
exit 1 |
|
fi |
|
|
|
TARGET="$(cd "$TARGET" && pwd)" |
|
|
|
echo -e "${CYAN}🧹 bersihkan${NC} ${DIM}→ $TARGET${NC}" |
|
if $DRY_RUN; then echo -e "${YELLOW} [DRY-RUN] tidak ada yang benar-benar dihapus${NC}"; fi |
|
if [[ ${#EXCLUDE[@]} -gt 0 ]]; then |
|
echo -e "${DIM} Exclude: ${EXCLUDE[*]}${NC}" |
|
fi |
|
echo "" |
|
|
|
# ---- Daftar target ---- |
|
DIR_NAMES=( |
|
"node_modules" |
|
".next" |
|
".nuxt" |
|
".output" |
|
".vercel" |
|
".turbo" |
|
"out" |
|
"dist" |
|
"build" |
|
".parcel-cache" |
|
".vite" |
|
"coverage" |
|
".nyc_output" |
|
".cache" |
|
"__pycache__" |
|
".pytest_cache" |
|
".mypy_cache" |
|
".ruff_cache" |
|
# Tambahan umum (aman, selalu cache/build) |
|
"target" |
|
".gradle" |
|
".dart_tool" |
|
".terraform" |
|
"_build" |
|
"Pods" |
|
".tox" |
|
) |
|
if $INCLUDE_VENDOR; then |
|
DIR_NAMES+=("vendor") |
|
fi |
|
|
|
# Build find expression untuk dirs |
|
DIR_EXPR="" |
|
for i in "${!DIR_NAMES[@]}"; do |
|
if [[ $i -eq 0 ]]; then |
|
DIR_EXPR="-name \"${DIR_NAMES[$i]}\"" |
|
else |
|
DIR_EXPR="$DIR_EXPR -o -name \"${DIR_NAMES[$i]}\"" |
|
fi |
|
done |
|
|
|
FOUND_FILE=$(mktemp /tmp/bersihkan.XXXXXX) |
|
trap 'rm -f "$FOUND_FILE"' EXIT |
|
|
|
# ---- Live scanning indicator ---- |
|
SPIN_CHARS='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' |
|
spin_pid="" |
|
|
|
start_spinner() { |
|
local target="$1" |
|
local found_file="$2" |
|
local i=0 |
|
printf "\033[?25l" 2>/dev/null || true |
|
while true; do |
|
local cnt=$(wc -l < "$found_file" 2>/dev/null | tr -d ' '); cnt=${cnt:-0} |
|
local last=$(tail -n 1 "$found_file" 2>/dev/null | sed "s|^$target/||" 2>/dev/null) |
|
if [[ ${#last} -gt 42 ]]; then last="…${last: -41}"; fi |
|
local ch="${SPIN_CHARS:$((i % 10)):1}" |
|
if [[ -n "$last" ]]; then |
|
printf "\r${DIM}%s Mencari... ${YELLOW}%s found${NC} ${DIM}→ %s${NC} " "$ch" "$cnt" "$last" |
|
else |
|
printf "\r${DIM}%s Mencari di %s ... ${YELLOW}%s found${NC} " "$ch" "$(basename "$target")" "$cnt" |
|
fi |
|
sleep 0.12 |
|
i=$((i+1)) |
|
done |
|
} |
|
|
|
stop_spinner() { |
|
if [[ -n "$spin_pid" ]] && kill -0 "$spin_pid" 2>/dev/null; then |
|
kill "$spin_pid" 2>/dev/null || true |
|
wait "$spin_pid" 2>/dev/null || true |
|
fi |
|
printf "\r\033[K" 2>/dev/null || printf "\r%60s\r" " " |
|
printf "\033[?25h" 2>/dev/null || true |
|
} |
|
|
|
# ---- OPTIMIZED SINGLE FIND ---- |
|
start_spinner "$TARGET" "$FOUND_FILE" & |
|
spin_pid=$! |
|
|
|
# Utama: single find |
|
eval "find \"$TARGET\" \ |
|
\( -type d -name \".git\" -prune \) -o \ |
|
\( -type d \( $DIR_EXPR \) -prune -print \) -o \ |
|
\( -type f -name \".DS_Store\" -print \) \ |
|
2>/dev/null" > "$FOUND_FILE" || true |
|
|
|
# Laravel cache |
|
if [[ -d "$TARGET/bootstrap" ]] || find "$TARGET" -maxdepth 4 -name "artisan" -type f 2>/dev/null | grep -q .; then |
|
for pat in "bootstrap/cache/*.php" "storage/framework/views/*" "storage/logs/*.log"; do |
|
dir_part="$(dirname "$pat")" |
|
file_pat="$(basename "$pat")" |
|
find "$TARGET" \( -type d -name ".git" -prune \) -o -type f -path "*/$dir_part/$file_pat" ! -name ".gitignore" ! -name ".gitkeep" -print 2>/dev/null >> "$FOUND_FILE" || true |
|
done |
|
for d in "storage/framework/cache/data" "storage/framework/sessions"; do |
|
find "$TARGET" \( -type d -name ".git" -prune \) -o -type f -path "*/$d/*" ! -name ".gitignore" ! -name ".gitkeep" -print 2>/dev/null | head -n 300 >> "$FOUND_FILE" || true |
|
done |
|
fi |
|
|
|
# Go artefacts |
|
if find "$TARGET" -maxdepth 4 -name "go.mod" -type f -print -quit 2>/dev/null | grep -q .; then |
|
while IFS= read -r gomod; do |
|
godir=$(dirname "$gomod") |
|
if [[ -d "$godir/bin" ]]; then |
|
echo "$godir/bin" >> "$FOUND_FILE" |
|
fi |
|
find "$godir" -maxdepth 2 -type d -name "bin" -prune -print 2>/dev/null | while IFS= read -r b; do |
|
if [[ "$b" != "$godir/bin" ]]; then |
|
echo "$b" >> "$FOUND_FILE" |
|
fi |
|
done |
|
done < <(find "$TARGET" -name "go.mod" -type f 2>/dev/null | head -n 20) |
|
find "$TARGET" \( -type d -name ".git" -prune \) -o -type f -name "*.out" -print 2>/dev/null | head -n 200 >> "$FOUND_FILE" || true |
|
find "$TARGET" \( -type d -name ".git" -prune \) -o -type f -name "*.test" -print 2>/dev/null | head -n 200 >> "$FOUND_FILE" || true |
|
fi |
|
|
|
stop_spinner |
|
|
|
# Dedup |
|
if [[ ! -s "$FOUND_FILE" ]]; then |
|
echo -e "${GREEN}✨ Sudah bersih! Tidak ada yang perlu dihapus.${NC}" |
|
exit 0 |
|
fi |
|
|
|
SORTED=$(mktemp /tmp/bersihkan-sorted.XXXXXX) |
|
sort -u "$FOUND_FILE" > "$SORTED" |
|
mv "$SORTED" "$FOUND_FILE" |
|
|
|
# ---- Exclude filter ---- |
|
if [[ ${#EXCLUDE[@]} -gt 0 ]]; then |
|
FILTERED=$(mktemp /tmp/bersihkan-filtered.XXXXXX) |
|
EXCLUDED_COUNT=0 |
|
while IFS= read -r p; do |
|
skip=false |
|
rel="${p#$TARGET/}" |
|
base=$(basename "$p") |
|
for pat in "${EXCLUDE[@]}"; do |
|
# Glob match: coba rel, full path, dan basename |
|
case "$rel" in $pat) skip=true; break ;; esac |
|
case "$p" in $pat) skip=true; break ;; esac |
|
case "$base" in $pat) skip=true; break ;; esac |
|
# Untuk pattern tanpa wildcard seperti "vendor" atau "bin", |
|
# juga match jika ada segment path yang exact |
|
if [[ "$pat" != *"*"* && "$pat" != *"?"* && "$pat" != *"["* ]]; then |
|
if [[ "$rel" == "$pat" ]] || [[ "$rel" == "$pat"/* ]] || [[ "$rel" == */"$pat" ]] || [[ "$rel" == */"$pat"/* ]]; then |
|
skip=true; break |
|
fi |
|
fi |
|
done |
|
if $skip; then |
|
EXCLUDED_COUNT=$((EXCLUDED_COUNT+1)) |
|
else |
|
echo "$p" >> "$FILTERED" |
|
fi |
|
done < "$FOUND_FILE" |
|
mv "$FILTERED" "$FOUND_FILE" |
|
if [[ $EXCLUDED_COUNT -gt 0 ]]; then |
|
echo -e "${DIM} → $EXCLUDED_COUNT item di-exclude (${EXCLUDE[*]})${NC}" |
|
fi |
|
if [[ ! -s "$FOUND_FILE" ]]; then |
|
echo -e "${GREEN}✨ Semua item ter-exclude, tidak ada yang dihapus.${NC}" |
|
exit 0 |
|
fi |
|
fi |
|
|
|
# Sorting |
|
if [[ "$SORT" == "size" ]]; then |
|
echo -e "${DIM}Mengurutkan berdasarkan ukuran (menghitung size, bisa agak lambat)...${NC}" |
|
SIZE_FILE=$(mktemp /tmp/bersihkan-size.XXXXXX) |
|
SIZE_SORTED=$(mktemp /tmp/bersihkan-size-sorted.XXXXXX) |
|
while IFS= read -r p; do |
|
sz=$(du -sk "$p" 2>/dev/null | cut -f1) |
|
sz=${sz:-0} |
|
human=$(du -sh "$p" 2>/dev/null | cut -f1) |
|
human=${human:-0B} |
|
printf "%s\t%s\t%s\n" "$sz" "$human" "$p" >> "$SIZE_FILE" |
|
done < "$FOUND_FILE" |
|
sort -nr -k1,1 "$SIZE_FILE" > "$SIZE_SORTED" |
|
cut -f3 "$SIZE_SORTED" > "$FOUND_FILE" |
|
SIZE_MAP="$SIZE_SORTED" |
|
else |
|
SORTED2=$(mktemp /tmp/bersihkan-sorted2.XXXXXX) |
|
sort -u "$FOUND_FILE" > "$SORTED2" |
|
mv "$SORTED2" "$FOUND_FILE" |
|
SIZE_MAP="" |
|
fi |
|
|
|
COUNT=$(wc -l < "$FOUND_FILE" | tr -d ' ') |
|
if [[ "$SORT" == "size" ]]; then |
|
echo -e "${YELLOW}Ditemukan $COUNT item (urut: terbesar dulu):${NC}" |
|
else |
|
echo -e "${YELLOW}Ditemukan $COUNT item (urut: nama A-Z):${NC}" |
|
fi |
|
echo "" |
|
|
|
# Tampilkan daftar |
|
if $VERBOSE; then |
|
if [[ "$SORT" == "size" && -n "$SIZE_MAP" && -f "$SIZE_MAP" ]]; then |
|
while IFS=$'\t' read -r sz human p; do |
|
printf " ${DIM}%-8s${NC} %s\n" "$human" "${p#$TARGET/}" |
|
done < "$SIZE_MAP" |
|
else |
|
while IFS= read -r p; do |
|
size=$(du -sh "$p" 2>/dev/null | cut -f1 || echo "") |
|
if [[ -n "$size" ]]; then |
|
printf " ${DIM}%-8s${NC} %s\n" "$size" "${p#$TARGET/}" |
|
else |
|
printf " ${DIM} ... ${NC} %s\n" "${p#$TARGET/}" |
|
fi |
|
done < "$FOUND_FILE" |
|
fi |
|
else |
|
while IFS= read -r p; do |
|
echo " • ${p#$TARGET/}" |
|
done < "$FOUND_FILE" |
|
fi |
|
|
|
# Total size |
|
TOTAL="" |
|
if [[ "$SORT" == "size" && -n "${SIZE_FILE:-}" && -f "$SIZE_FILE" ]]; then |
|
TOTAL_KB=$(awk -F'\t' '{s+=$1} END {print s}' "$SIZE_FILE" 2>/dev/null || echo "?") |
|
if [[ "$TOTAL_KB" != "?" && "$TOTAL_KB" =~ ^[0-9]+$ ]]; then |
|
TOTAL=$(du -shc $(cat "$FOUND_FILE") 2>/dev/null | tail -1 | cut -f1 || echo "${TOTAL_KB}K") |
|
else |
|
TOTAL=$(du -shc $(cat "$FOUND_FILE") 2>/dev/null | tail -1 | cut -f1 || echo "?") |
|
fi |
|
elif $VERBOSE; then |
|
if [[ "$COUNT" -gt 50 ]]; then |
|
echo -e " ${DIM}Menghitung total ukuran...${NC}" |
|
fi |
|
TOTAL=$(du -shc $(cat "$FOUND_FILE") 2>/dev/null | tail -1 | cut -f1 || echo "?") |
|
else |
|
if [[ "$COUNT" -gt 80 ]]; then |
|
echo -e " ${DIM}Menghitung total ukuran ($COUNT item, bisa agak lambat)...${NC}" |
|
fi |
|
TOTAL=$(du -shc $(cat "$FOUND_FILE") 2>/dev/null | tail -1 | cut -f1 || echo "?") |
|
fi |
|
|
|
if [[ -n "$TOTAL" && "$TOTAL" != "?" ]]; then |
|
echo -e " ${CYAN}Total: $TOTAL${NC} ${DIM}($COUNT item)${NC}" |
|
if ! $VERBOSE && $DRY_RUN; then |
|
echo -e " ${DIM}Tips: pakai -v atau --sort=size untuk lihat ukuran per-item${NC}" |
|
fi |
|
else |
|
echo -e " ${DIM}($COUNT item)${NC}" |
|
fi |
|
rm -f "${SIZE_MAP:-}" 2>/dev/null || true |
|
echo "" |
|
|
|
if $DRY_RUN; then |
|
echo -e "${YELLOW}Dry-run selesai. Jalankan tanpa --dry-run untuk menghapus.${NC}" |
|
exit 0 |
|
fi |
|
|
|
if $AUTO_YES; then |
|
echo -e "${YELLOW}⚠️ Pastikan sudah git push / backup! Segala risiko kehilangan data bukan tanggung jawab kami.${NC}" |
|
else |
|
echo -e "${RED}⚠️ Pastikan sudah git push / backup sebelum melanjutkan!${NC}" |
|
echo -e "${DIM} Segala risiko kehilangan data bukan tanggung jawab kami.${NC}" |
|
echo -ne "${YELLOW}Hapus semua di atas? [y/N] ${NC}" |
|
read -r ans |
|
case "$ans" in |
|
y|Y|yes|YES) ;; |
|
*) echo "Dibatalkan."; exit 0 ;; |
|
esac |
|
fi |
|
|
|
echo "" |
|
FAILED=0 |
|
DELETED=0 |
|
while IFS= read -r p; do |
|
if rm -rf "$p" 2>/dev/null; then |
|
echo -e "${GREEN} ✓${NC} ${p#$TARGET/}" |
|
DELETED=$((DELETED+1)) |
|
else |
|
echo -e "${RED} ✗ Gagal: ${p}${NC}" |
|
FAILED=$((FAILED+1)) |
|
fi |
|
done < "$FOUND_FILE" |
|
|
|
echo "" |
|
if [[ -z "$TOTAL" || "$TOTAL" == "?" ]]; then |
|
TOTAL_MSG="$DELETED item" |
|
else |
|
TOTAL_MSG="$TOTAL" |
|
fi |
|
rm -f "${SIZE_FILE:-}" 2>/dev/null || true |
|
|
|
if [[ $FAILED -eq 0 ]]; then |
|
if [[ "$TOTAL_MSG" == "$DELETED item" ]]; then |
|
echo -e "${GREEN}✨ Selesai! $DELETED item dibersihkan${NC}" |
|
else |
|
echo -e "${GREEN}✨ Selesai! $DELETED item dibersihkan ${DIM}($TOTAL_MSG dibebaskan)${NC}" |
|
fi |
|
else |
|
if [[ "$TOTAL_MSG" == "$DELETED item" ]]; then |
|
echo -e "${YELLOW}Selesai dengan $FAILED gagal, $DELETED berhasil.${NC}" |
|
else |
|
echo -e "${YELLOW}Selesai dengan $FAILED gagal, $DELETED berhasil ${DIM}($TOTAL_MSG dibebaskan)${NC}" |
|
fi |
|
fi |