Created
February 5, 2026 01:48
-
-
Save mlevkov/4238d302fb54e40e342f9219ca6b268a to your computer and use it in GitHub Desktop.
Batch 'cargo clean' for multiple Rust projects - safely removes target directories with progress bar and space savings summary
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Safely run 'cargo clean' on all Rust project roots | |
| # | |
| # SAFETY: This script ONLY executes 'cargo clean' command. | |
| # It does NOT use rm, rmdir, or any direct deletion commands. | |
| # Don't use set -e as we handle errors manually and want to continue on failures | |
| ROOT_DIR="${1:-.}" | |
| # Function to check if a directory is inside another Cargo project | |
| is_nested_project() { | |
| local dir="$1" | |
| local check_dir=$(dirname "$dir") | |
| local abs_root=$(realpath "$ROOT_DIR") | |
| while [[ "$check_dir" != "." && "$check_dir" != "/" ]]; do | |
| [[ "$(realpath "$check_dir")" == "$abs_root" ]] && break | |
| if [[ -f "$check_dir/Cargo.toml" ]]; then | |
| return 0 | |
| fi | |
| check_dir=$(dirname "$check_dir") | |
| done | |
| return 1 | |
| } | |
| # Convert size string (e.g., "5.0GiB", "100MiB") to bytes | |
| size_to_bytes() { | |
| local size_str="$1" | |
| local num unit | |
| # Extract number and unit | |
| num=$(echo "$size_str" | grep -oE '[0-9]+\.?[0-9]*') | |
| unit=$(echo "$size_str" | grep -oE '[A-Za-z]+') | |
| # Convert to bytes using awk for floating point | |
| case "$unit" in | |
| B) echo "$num" | awk '{printf "%.0f", $1}' ;; | |
| KiB) echo "$num" | awk '{printf "%.0f", $1 * 1024}' ;; | |
| MiB) echo "$num" | awk '{printf "%.0f", $1 * 1024 * 1024}' ;; | |
| GiB) echo "$num" | awk '{printf "%.0f", $1 * 1024 * 1024 * 1024}' ;; | |
| TiB) echo "$num" | awk '{printf "%.0f", $1 * 1024 * 1024 * 1024 * 1024}' ;; | |
| *) echo "0" ;; | |
| esac | |
| } | |
| # Convert bytes to human-readable format | |
| bytes_to_human() { | |
| local bytes=$1 | |
| if (( bytes >= 1099511627776 )); then | |
| awk "BEGIN {printf \"%.2f TiB\", $bytes / 1099511627776}" | |
| elif (( bytes >= 1073741824 )); then | |
| awk "BEGIN {printf \"%.2f GiB\", $bytes / 1073741824}" | |
| elif (( bytes >= 1048576 )); then | |
| awk "BEGIN {printf \"%.2f MiB\", $bytes / 1048576}" | |
| elif (( bytes >= 1024 )); then | |
| awk "BEGIN {printf \"%.2f KiB\", $bytes / 1024}" | |
| else | |
| echo "$bytes B" | |
| fi | |
| } | |
| # Collect all project roots | |
| echo "Scanning for Rust projects in: $(realpath "$ROOT_DIR")" | |
| echo "" | |
| projects=() | |
| while IFS= read -r cargo_file; do | |
| project_dir=$(dirname "$cargo_file") | |
| if ! is_nested_project "$project_dir"; then | |
| projects+=("$project_dir") | |
| fi | |
| done < <(find "$ROOT_DIR" -name "target" -prune -o -name "Cargo.toml" -print 2>/dev/null) | |
| total=${#projects[@]} | |
| if [[ $total -eq 0 ]]; then | |
| echo "No Rust projects found." | |
| exit 0 | |
| fi | |
| # Display all locations that will be cleaned | |
| echo "Found $total Rust project(s) to clean:" | |
| echo "========================================" | |
| for i in "${!projects[@]}"; do | |
| printf " %3d. %s\n" "$((i + 1))" "${projects[$i]}" | |
| done | |
| echo "========================================" | |
| echo "" | |
| # Ask for confirmation | |
| read -p "Proceed with 'cargo clean' on all $total projects? [y/N] " -n 1 -r | |
| echo "" | |
| if [[ ! $REPLY =~ ^[Yy]$ ]]; then | |
| echo "Aborted." | |
| exit 0 | |
| fi | |
| echo "" | |
| # Progress bar function | |
| show_progress() { | |
| local current=$1 | |
| local total=$2 | |
| local width=40 | |
| local percent=$((current * 100 / total)) | |
| local filled=$((current * width / total)) | |
| local empty=$((width - filled)) | |
| printf "\r[" | |
| printf "%${filled}s" | tr ' ' '#' | |
| printf "%${empty}s" | tr ' ' '-' | |
| printf "] %3d%% (%d/%d)" "$percent" "$current" "$total" | |
| } | |
| # Run cargo clean on each project | |
| cleaned=0 | |
| failed=0 | |
| total_files=0 | |
| total_bytes=0 | |
| for i in "${!projects[@]}"; do | |
| project="${projects[$i]}" | |
| show_progress "$((i + 1))" "$total" | |
| # ONLY cargo clean - no other deletion commands | |
| # Capture output to parse removed files/size | |
| output=$(cd "$project" && cargo clean 2>&1) || true | |
| if [[ "$output" == *"Removed"* ]]; then | |
| ((cleaned++)) || true | |
| # Parse: "Removed 21708 files, 5.0GiB total" | |
| files=$(echo "$output" | grep -oE 'Removed [0-9]+' | grep -oE '[0-9]+') | |
| size=$(echo "$output" | grep -oE '[0-9]+\.?[0-9]*[KMGT]?i?B') | |
| if [[ -n "$files" ]]; then | |
| total_files=$((total_files + files)) | |
| fi | |
| if [[ -n "$size" ]]; then | |
| bytes=$(size_to_bytes "$size") | |
| total_bytes=$((total_bytes + bytes)) | |
| fi | |
| elif [[ -z "$output" ]]; then | |
| # No output means nothing to clean (already clean) | |
| ((cleaned++)) || true | |
| else | |
| ((failed++)) || true | |
| echo "" | |
| echo " Warning: cargo clean issue in $project: $output" | |
| fi | |
| done | |
| echo "" | |
| echo "" | |
| echo "========================================" | |
| echo "Complete: $cleaned cleaned, $failed failed" | |
| echo "Removed $total_files files, $(bytes_to_human $total_bytes) total" | |
| echo "========================================" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment