Skip to content

Instantly share code, notes, and snippets.

@mikedh
Created July 7, 2026 20:49
Show Gist options
  • Select an option

  • Save mikedh/9c22414d21cddf3007223168daf79fda to your computer and use it in GitHub Desktop.

Select an option

Save mikedh/9c22414d21cddf3007223168daf79fda to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
#
# clean-build-dirs.sh — find and delete build/env directories:
# - Rust `target/` next to a Cargo.toml
# - Python `.venv/` next to a pyproject.toml
#
# A directory named `target` is treated as a Rust build dir only if it is a
# sibling of a Cargo.toml, OR it contains a Cargo build-cache marker
# (CACHEDIR.TAG / .rustc_info.json). A `.venv` is treated as a Python virtual
# env only if it is a sibling of a pyproject.toml, OR it contains a
# pyvenv.cfg marker. This avoids deleting unrelated dirs by the same name.
#
# Usage:
# ./clean-rust-targets.sh [ROOT] # dry run: list candidates + sizes
# ./clean-rust-targets.sh [ROOT] --delete # actually delete them
#
# ROOT defaults to the current directory.
set -euo pipefail
ROOT="."
DELETE=0
for arg in "$@"; do
case "$arg" in
--delete) DELETE=1 ;;
-*) echo "unknown option: $arg" >&2; exit 2 ;;
*) ROOT="$arg" ;;
esac
done
if [ ! -d "$ROOT" ]; then
echo "not a directory: $ROOT" >&2
exit 2
fi
# Collect matching dirs into an array (NUL-safe for odd paths).
# Match `target` (Rust) and `.venv` (Python), each with its own validation rule.
targets=()
while IFS= read -r -d '' d; do
parent=$(dirname "$d")
case "$(basename "$d")" in
target)
if [ -f "$parent/Cargo.toml" ] || [ -f "$d/CACHEDIR.TAG" ] || [ -f "$d/.rustc_info.json" ]; then
targets+=("$d")
fi
;;
.venv)
if [ -f "$parent/pyproject.toml" ] || [ -f "$d/pyvenv.cfg" ]; then
targets+=("$d")
fi
;;
esac
done < <(find "$ROOT" -type d \( -name target -o -name .venv \) -prune -print0 2>/dev/null)
if [ "${#targets[@]}" -eq 0 ]; then
echo "No Rust target/ or Python .venv/ directories found under: $ROOT"
exit 0
fi
# Report sizes, largest first.
echo "Found ${#targets[@]} build/env directories under: $ROOT"
echo
printf '%s\0' "${targets[@]}" \
| du -sk --files0-from=- 2>/dev/null \
| sort -rn \
| awk '{ printf "%8.2f GB\t%s\n", $1/1024/1024, $2 }'
total_kb=$(printf '%s\0' "${targets[@]}" | du -sk --files0-from=- 2>/dev/null | awk '{s+=$1} END{print s}')
echo
awk -v kb="$total_kb" 'BEGIN { printf "TOTAL: %.2f GB\n", kb/1024/1024 }'
if [ "$DELETE" -ne 1 ]; then
echo
echo "Dry run. Re-run with --delete to remove these directories."
exit 0
fi
echo
read -r -p "Delete all ${#targets[@]} directories above? [y/N] " reply
case "$reply" in
y|Y|yes|YES) ;;
*) echo "Aborted."; exit 1 ;;
esac
for d in "${targets[@]}"; do
echo "rm -rf $d"
rm -rf -- "$d"
done
echo "Done."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment