Skip to content

Instantly share code, notes, and snippets.

@szymonrucinski
Forked from mircobabini/mac-cleanup
Last active May 15, 2025 15:54
Show Gist options
  • Save szymonrucinski/d7760198b5d0366d98994e3d0e4f7542 to your computer and use it in GitHub Desktop.
Save szymonrucinski/d7760198b5d0366d98994e3d0e4f7542 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# inspired by
# https://gist.github.com/JannikArndt/feb720c1f5d210b4820b880af23f2a07
# which was inspired by
# https://github.com/fwartner/mac-cleanup/blob/master/cleanup.sh
# https://gist.github.com/jamesrampton/4503412
# https://github.com/mengfeng/clean-my-mac/blob/master/clean_my_mac.sh
# https://github.com/szymonkaliski/Dotfiles/blob/master/Scripts/clean-my-mac
# http://brettterpstra.com/2015/10/27/vacuuming-mail-dot-app-on-el-capitan/ / https://github.com/pbihq/tools/blob/master/MailDBOptimiser.sh
# Function to format bytes to human-readable format
bytesToHuman() {
b=${1:-0}; d=''; s=0; S=(Bytes {K,M,G,T,E,P,Y,Z}iB)
while ((b > 1024)); do
d="$(printf ".%02d" $((b % 1024 * 100 / 1024)))"
b=$((b / 1024))
let s++
done
echo "$b$d ${S[$s]}"
}
# Function to display disk usage in a nice formatted way
showDiskUsage() {
echo "==== Disk Space Information ===="
echo " Size Used Avail Use% Mount"
# Get the main disk usage (filtering out virtual filesystems)
df -h | grep -v "devfs\|map\|com.apple\|/dev/disk[1-9]" | grep "/dev/disk" | while read line; do
# Parse the line
local disk=$(echo "$line" | awk '{print $1}')
local size=$(echo "$line" | awk '{print $2}')
local used=$(echo "$line" | awk '{print $3}')
local avail=$(echo "$line" | awk '{print $4}')
local capacity=$(echo "$line" | awk '{print $5}')
local mount=$(echo "$line" | awk '{print $9}')
# Format for nicer display
if [[ "$mount" == "/" ]]; then
echo "Root Volume: $size $used $avail $capacity"
elif [[ "$mount" == "/System/Volumes/Data" ]]; then
echo "Data Volume: $size $used $avail $capacity"
else
echo "$mount: $size $used $avail $capacity"
fi
done
# Show total space for simulator volumes which can take a lot of space
local simSize=$(du -ch /Library/Developer/CoreSimulator/Volumes 2>/dev/null | grep total | cut -f1)
if [[ -n "$simSize" && "$simSize" != "0B" ]]; then
echo "iOS Simulators: $simSize total"
fi
# Show Docker usage if Docker is installed
if command -v docker &>/dev/null && docker info &>/dev/null; then
local dockerDiskUsage=$(docker system df --format "{{.Size}}" 2>/dev/null | head -1)
if [[ -n "$dockerDiskUsage" ]]; then
echo "Docker Usage: $dockerDiskUsage"
fi
fi
echo "=================================="
}
# Show initial disk usage
echo "BEFORE CLEANUP:"
showDiskUsage
# Check if Time Machine is running
if [ `tmutil status 2>/dev/null | grep -c "Running = 1"` -ne 0 ]; then
echo "Time Machine is currently running. Let it finish first!"
exit
fi
# Check for last Time Machine backup and exit if it's longer than 1 hour ago
lastBackupDateString=`tmutil latestbackup 2>/dev/null | grep -E -o "[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}"`
if [ "$lastBackupDateString" == "" ]; then
read -n 1 -p "$(tput setaf 3)Last Time Machine backup cannot be found. Proceed anyway?$(tput sgr0) (y/n) " RESP
echo ""
if [ "$RESP" != "y" ]; then
exit
fi
else
lastBackupDate=`date -j -f "%Y-%m-%d-%H%M%S" $lastBackupDateString "+%s"`
if [ $((`date +%s` - $lastBackupDate)) -gt 3600 ]
then
printf "Time Machine has not backed up since `date -j -f %s $lastBackupDate` (more than 60 minutes)!"
exit 1003
else
echo "Last Time Machine backup was on `date -j -f %s $lastBackupDate`. "
fi
fi
# Ask for the administrator password upfront
if [ "$EUID" -ne 0 ]; then
echo "Please run as root"
exit
fi
# Record starting disk space
oldAvailable=$(df / | tail -1 | awk '{print $4}')
oldUsed=$(df / | tail -1 | awk '{print $3}')
totalSize=$(df / | tail -1 | awk '{print $2}')
# Get the data volume space (where most user data is stored)
dataVolumeAvailable=$(df /System/Volumes/Data 2>/dev/null | tail -1 | awk '{print $4}')
dataVolumeUsed=$(df /System/Volumes/Data 2>/dev/null | tail -1 | awk '{print $3}')
dataVolumeSize=$(df /System/Volumes/Data 2>/dev/null | tail -1 | awk '{print $2}')
echo "Starting cleanup operations..."
echo 'Empty the Trash on all mounted volumes and the main HDD...'
sudo rm -rfv /Volumes/*/.Trashes &>/dev/null
sudo rm -rfv ~/.Trash &>/dev/null
echo 'Clean temporary files...'
sudo -S rm -rfv /tmp/*
sudo -S rm -rfv /private/var/tmp/Processing/
sudo -S rm -rfv /private/var/tmp/Xcode/
sudo -S rm -rfv /private/var/tmp/tmp*
echo 'Clear Mail Downloads...'
sudo rm -rfv ~/Library/Containers/com.apple.mail/Data/Library/Mail\ Downloads/* &>/dev/null
echo 'Clear System Log Files...'
sudo -S rm -rfv /private/var/log/* &>/dev/null
sudo -S rm -rfv /Library/Logs/* &>/dev/null
sudo -S rm -rfv ~/Library/Logs/* &>/dev/null
sudo -S rm -rfv ~/Library/Application\ Support/Adobe/Common/Media\ Cache\ Files/* &>/dev/null
sudo rm -rfv /private/var/log/asl/*.asl &>/dev/null
rm -rfv ~/Library/Containers/com.apple.mail/Data/Library/Logs/Mail/* &>/dev/null
rm -rfv ~/Library/Logs/CoreSimulator/* &>/dev/null
echo 'Clean .DS_Store files in ~...'
sudo -S find ~ / -name ".DS_Store" -exec rm {} &>/dev/null
echo 'Vacuum Mail Envelope Index...'
sqlite3 ~/Library/Mail/V4/MailData/Envelope\ Index vacuum &>/dev/null
echo 'Clean System Caches...'
sudo -S rm -rf /Library/Caches/*
sudo -S rm -rf ~/Library/Caches/*
echo 'Clean Application Caches...'
for x in $(ls ~/Library/Containers/)
do
rm -rfv ~/Library/Containers/$x/Data/Library/Caches/*
done
echo 'Clear Adobe Cache Files...'
sudo rm -rfv ~/Library/Application\ Support/Adobe/Common/Media\ Cache\ Files/* &>/dev/null
echo 'Cleanup iOS Applications...'
rm -rfv ~/Music/iTunes/iTunes\ Media/Mobile\ Applications/* &>/dev/null
echo 'Remove iOS Device Backups...'
rm -rfv ~/Library/Application\ Support/MobileSync/Backup/* &>/dev/null
echo 'Performing comprehensive Xcode cleanup...'
# Standard Xcode cleanup
echo ' - Removing Derived Data...'
rm -rfv ~/Library/Developer/Xcode/DerivedData/* &>/dev/null
echo ' - Removing Archives...'
rm -rfv ~/Library/Developer/Xcode/Archives/* &>/dev/null
echo ' - Removing Module Caches...'
rm -rfv ~/Library/Developer/Xcode/iOS\ DeviceSupport/* &>/dev/null
rm -rfv ~/Library/Developer/Xcode/watchOS\ DeviceSupport/* &>/dev/null
rm -rfv ~/Library/Developer/Xcode/tvOS\ DeviceSupport/* &>/dev/null
echo ' - Removing Device Support Files...'
rm -rfv ~/Library/Developer/CoreSimulator/Caches/* &>/dev/null
echo ' - Removing Documentation Cache...'
rm -rfv ~/Library/Caches/com.apple.dt.Xcode/* &>/dev/null
echo ' - Removing Playgrounds Cache...'
rm -rfv ~/Library/Developer/XCPGDevices/* &>/dev/null
# Additional simulator cleanup
echo ' - Clearing Simulator Cache...'
rm -rfv ~/Library/Developer/CoreSimulator/Devices/*/data/Library/Caches/* &>/dev/null
rm -rfv ~/Library/Developer/CoreSimulator/Devices/*/data/Library/SplashBoard/Snapshots/* &>/dev/null
echo ' - Removing orphaned simulator device logs (this may take a moment)...'
find ~/Library/Logs/CoreSimulator -type f -name "*.log" -mtime +7 -delete &>/dev/null
echo 'Cleanup Homebrew Cache...'
brew cleanup --force -s &>/dev/null
# The command below is deprecated in newer versions of Homebrew
# Commenting it out to avoid errors
# brew cask cleanup &>/dev/null
rm -rfv /Library/Caches/Homebrew/* &>/dev/null
brew tap --repair &>/dev/null
echo 'Purge CoreSimulator...'
echo ' - Removing unavailable simulator runtimes...'
xcrun simctl delete unavailable &>/dev/null
echo ' - Would you like to delete all simulators? (y/n)'
read -n 1 -p "This will remove all simulator devices and you'll need to recreate them: " DELETE_SIMULATORS
echo ""
if [ "$DELETE_SIMULATORS" = "y" ]; then
echo ' - Removing all simulator devices...'
xcrun simctl erase all &>/dev/null
fi
echo 'Cleanup any old versions of gems...'
gem cleanup &>/dev/null
# Enhanced Docker cleanup section
echo '==== Starting comprehensive Docker cleanup ===='
echo 'Checking if Docker is installed and running...'
if command -v docker &>/dev/null; then
if ! docker info &>/dev/null; then
echo "Docker is installed but not running. Skipping Docker cleanup."
else
echo 'Removing dangling Docker containers...'
docker rm $(docker ps -q -f status=exited) &>/dev/null || echo "No exited containers to remove."
echo 'Removing all stopped containers...'
docker container prune -f &>/dev/null
echo 'Removing unused Docker volumes...'
docker volume prune -f &>/dev/null
echo 'Removing unused Docker networks...'
docker network prune -f &>/dev/null
echo 'Removing dangling Docker images...'
docker image prune -f &>/dev/null
echo 'Would you like to remove ALL unused Docker images? (y/n)'
read -n 1 -p "This will remove all images not used by containers (could be several GB): " REMOVE_IMAGES
echo ""
if [ "$REMOVE_IMAGES" = "y" ]; then
echo 'Removing ALL unused Docker images...'
docker image prune -a -f &>/dev/null
fi
echo 'Would you like to perform a full Docker system prune? (y/n)'
read -n 1 -p "This will remove all unused containers, networks, images and volumes: " FULL_PRUNE
echo ""
if [ "$FULL_PRUNE" = "y" ]; then
echo 'Performing full Docker system prune...'
docker system prune -a -f --volumes &>/dev/null
fi
echo 'Docker cleanup completed!'
fi
else
echo "Docker is not installed. Skipping Docker cleanup."
fi
echo '==== Docker cleanup finished ===='
echo 'Purge inactive memory...'
sudo purge
echo 'Rebuild Spotlight...'
sudo mdutil -E /
clear
echo "=========================="
echo "CLEANUP SUMMARY"
echo "=========================="
# Calculate freed space on root volume
newAvailable=$(df / | tail -1 | awk '{print $4}')
newUsed=$(df / | tail -1 | awk '{print $3}')
freedSpace=$((newAvailable-oldAvailable))
freedSpaceBytes=$(( $freedSpace * 512)) # Convert blocks to bytes
freedSpaceHuman=$(bytesToHuman $freedSpaceBytes)
# Calculate freed space on data volume if it exists
if [[ -n "$dataVolumeAvailable" ]]; then
newDataVolumeAvailable=$(df /System/Volumes/Data | tail -1 | awk '{print $4}')
newDataVolumeUsed=$(df /System/Volumes/Data | tail -1 | awk '{print $3}')
dataFreedSpace=$((newDataVolumeAvailable-dataVolumeAvailable))
dataFreedSpaceBytes=$(( $dataFreedSpace * 512)) # Convert blocks to bytes
dataFreedSpaceHuman=$(bytesToHuman $dataFreedSpaceBytes)
fi
# Display space freed
echo "Space freed on root volume: $freedSpaceHuman"
if [[ -n "$dataVolumeAvailable" ]]; then
echo "Space freed on data volume: $dataFreedSpaceHuman"
fi
# Calculate total space freed across both volumes
totalFreedSpaceBytes=$freedSpaceBytes
if [[ -n "$dataVolumeAvailable" ]]; then
# Only add data volume if it's separate from root (to avoid double counting)
if [[ "$dataVolumeSize" != "$totalSize" ]]; then
totalFreedSpaceBytes=$((freedSpaceBytes + dataFreedSpaceBytes))
fi
fi
totalFreedSpaceHuman=$(bytesToHuman $totalFreedSpaceBytes)
echo "Total space freed: $totalFreedSpaceHuman"
echo ""
# Show final disk usage
echo "AFTER CLEANUP:"
showDiskUsage
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment