Skip to content

Instantly share code, notes, and snippets.

@Limbicnation
Last active May 19, 2025 18:08
Show Gist options
  • Save Limbicnation/6763b69ab6a406790f3b7d4b56a2f6e8 to your computer and use it in GitHub Desktop.
Save Limbicnation/6763b69ab6a406790f3b7d4b56a2f6e8 to your computer and use it in GitHub Desktop.
ubuntu_cleanup.sh A comprehensive system cleanup script for Ubuntu 24.04 that safely removes unnecessary files to free up disk space. This script includes system maintenance tasks like package cleanup, log rotation, cache removal, and system optimization. Features include progress tracking, disk space reporting, resource limiting, and extensive …
#!/usr/bin/env bash
# Enhanced Ubuntu Cleanup Script
# This script performs comprehensive system cleanup while maintaining security and robustness
# Configuration variables (can be moved to a separate config file)
CONFIG_FILE="${HOME}/.config/ubuntu_cleanup.conf"
LOG_DIR="/var/log/system_cleanup"
LOG_FILE="${LOG_DIR}/cleanup_$(date +%Y%m%d_%H%M%S).log"
DRY_RUN=0
VERBOSE=0
TIMEOUT_DURATION=60 # Timeout for user prompts in seconds
PARALLEL_JOBS=2 # Number of parallel jobs for supported operations
MAX_RESOURCE_USAGE=50 # Maximum CPU percentage to use
DEFAULT_RETENTION_DAYS=10 # Default number of days to keep logs
# Load configuration if available
if [ -f "$CONFIG_FILE" ]; then
echo "Loading configuration from $CONFIG_FILE"
source "$CONFIG_FILE"
fi
# Create log directory if it doesn't exist
mkdir -p "$LOG_DIR"
# Setup logging with rotation
exec > >(tee -a "$LOG_FILE") 2>&1
# Check for root privileges
check_root() {
if [[ $EUID -ne 0 ]]; then
echo "This script requires elevated privileges for some operations."
echo "You will be prompted for your password as needed."
HAS_ROOT=0
else
HAS_ROOT=1
fi
}
# Function to handle sudo commands
run_with_privileges() {
if [[ $HAS_ROOT -eq 1 ]]; then
eval "$@"
else
sudo "$@"
fi
}
# Function to execute with resource limits
run_with_limits() {
if command -v cpulimit >/dev/null 2>&1; then
cpulimit -l $MAX_RESOURCE_USAGE -- "$@"
else
echo "cpulimit not installed, running without CPU limitations"
"$@"
fi
}
# Function to display progress
total_steps=19
current_step=0
progress() {
current_step=$((current_step + 1))
percentage=$((current_step * 100 / total_steps))
bar_size=30
filled_size=$((percentage * bar_size / 100))
bar=$(printf "%${filled_size}s" | tr ' ' '#')
empty=$(printf "%$((bar_size - filled_size))s" | tr ' ' '-')
echo -ne "[$bar$empty] $percentage% - $1\r"
echo -e "[$bar$empty] $percentage% - $1"
}
# Function for calculating size difference
calculate_size_diff() {
local before=$1
local after=$2
# Remove letters and keep only numbers and decimal points
before_num=$(echo "$before" | sed 's/[^0-9.]//g')
after_num=$(echo "$after" | sed 's/[^0-9.]//g')
# Extract unit (G, M, etc.)
unit=$(echo "$before" | sed 's/[0-9.]//g')
# Calculate difference
diff=$(echo "$after_num - $before_num" | bc)
echo "$diff$unit"
}
# Backup functionality has been removed
# Function for timeout handling with user prompts
prompt_with_timeout() {
local prompt=$1
local timeout=$TIMEOUT_DURATION
local response
# Use read with timeout
read -t "$timeout" -p "$prompt" response || true
# Return default value if timeout occurred
if [ -z "$response" ]; then
echo "Timeout reached, assuming default answer (n)"
return 1
fi
# Process response
if [[ "$response" =~ ^[Yy]$ ]]; then
return 0
else
return 1
fi
}
# Function to check disk space in a directory
check_disk_space() {
local dir=$1
local threshold=${2:-90} # Default threshold of 90%
local usage=$(df -h "$dir" | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$usage" -gt "$threshold" ]; then
echo "Warning: Disk usage for $dir is critically high at $usage%"
return 1
else
return 0
fi
}
# Function to safely remove files
safe_remove() {
local target=$1
local force=${2:-0}
# Check if target exists and is not a system-critical path
if [ ! -e "$target" ]; then
echo "Warning: $target does not exist, skipping removal"
return 0
fi
# Check if path is too dangerous to remove
for dangerous_path in / /bin /boot /dev /etc /lib /proc /root /sbin /sys /usr /var; do
if [ "$target" = "$dangerous_path" ]; then
echo "Error: Refusing to remove critical system path $target"
return 1
fi
done
# Safe removal
if [ -d "$target" ] && [ ! -L "$target" ]; then
# It's a directory, remove contents but keep directory
echo "Removing contents of directory $target"
if [ $force -eq 1 ]; then
rm -rf "${target:?}"/* 2>/dev/null || true
else
rm -r "${target:?}"/* 2>/dev/null || true
fi
else
# It's a file or symbolic link
echo "Removing $target"
if [ $force -eq 1 ]; then
rm -f "$target" 2>/dev/null || true
else
rm "$target" 2>/dev/null || true
fi
fi
return $?
}
# Parse command line options
while getopts "dnvt:j:r:k:" opt; do
case $opt in
d|n) DRY_RUN=1 ;;
v) VERBOSE=1 ;;
t) TIMEOUT_DURATION=$OPTARG ;;
j) PARALLEL_JOBS=$OPTARG ;;
r) MAX_RESOURCE_USAGE=$OPTARG ;;
k) DEFAULT_RETENTION_DAYS=$OPTARG ;;
*) echo "Usage: $0 [-d|-n] [-v] [-t timeout] [-j jobs] [-r max_cpu] [-k retention_days]" >&2
echo " -d,-n Dry run (show what would be done)"
echo " -v Verbose output"
echo " -t Timeout for user prompts in seconds (default: 60)"
echo " -j Number of parallel jobs for supported operations (default: 2)"
echo " -r Maximum CPU percentage to use (default: 50)"
echo " -k Default retention days for logs and temp files (default: 10)"
exit 1 ;;
esac
done
# Check if required tools are installed
check_dependencies() {
local missing_tools=()
for tool in apt-get find grep awk md5sum bc; do
if ! command -v $tool &>/dev/null; then
missing_tools+=("$tool")
fi
done
# Check for optional tools
for tool in cpulimit deborphan bleachbit parallel; do
if ! command -v $tool &>/dev/null; then
echo "Optional tool $tool is not installed"
fi
done
if [ ${#missing_tools[@]} -gt 0 ]; then
echo "Error: The following required tools are missing: ${missing_tools[*]}"
echo "Please install them before running this script"
exit 1
fi
}
# Record initial disk space
record_disk_space() {
echo "Initial disk space usage:"
df -h / /home
initial_space=$(df -h / | awk 'NR==2 {print $4}')
initial_usage_percent=$(df -h / | awk 'NR==2 {print $5}')
}
# Check if we're running on Ubuntu
check_ubuntu() {
if [ -f /etc/os-release ]; then
. /etc/os-release
if [[ "$ID" == "ubuntu" ]]; then
echo "✓ Running on Ubuntu $VERSION_ID"
return 0
else
echo "Warning: This script is optimized for Ubuntu, but you're running $PRETTY_NAME"
echo "Some functionality may not work as expected"
return 1
fi
else
echo "Warning: Unable to determine OS version"
return 1
fi
}
# Main execution starts here
check_dependencies
check_root
check_ubuntu
record_disk_space
# Confirmation prompt
if [ $DRY_RUN -eq 0 ]; then
if prompt_with_timeout "Are you sure you want to proceed with the cleanup? (y/n): "; then
echo "Proceeding with cleanup..."
else
echo "Aborted."
exit 1
fi
fi
echo "Starting cleanup process..."
[ $DRY_RUN -eq 1 ] && echo "DRY RUN MODE - No changes will be made"
# No backup step - proceeding directly to cleanup
# Update package list
progress "Updating package list"
if [ $DRY_RUN -eq 0 ]; then
if run_with_privileges apt-get update; then
echo "✓ Package list updated successfully"
else
echo "✗ Failed to update package list"
fi
fi
# Clear user cache
progress "Clearing user cache"
if [ $DRY_RUN -eq 0 ]; then
# Get size before
cache_size_before=$(du -sh ~/.cache 2>/dev/null | cut -f1)
echo "Cache size before cleanup: $cache_size_before"
# Only remove cache files older than a certain number of days
if find ~/.cache -type f -mtime +3 -exec rm -f {} \; 2>/dev/null; then
echo "✓ User cache older than 3 days cleared"
else
echo "✗ Failed to clear user cache"
fi
# Get size after
cache_size_after=$(du -sh ~/.cache 2>/dev/null | cut -f1)
echo "Cache size after cleanup: $cache_size_after"
fi
# Clean up the APT cache
progress "Cleaning up APT cache"
if [ $DRY_RUN -eq 0 ]; then
if run_with_privileges apt-get clean; then
echo "✓ APT cache cleaned"
else
echo "✗ Failed to clean APT cache"
fi
fi
# Remove obsolete packages
progress "Removing obsolete packages"
if [ $DRY_RUN -eq 0 ]; then
if run_with_privileges apt-get autoclean; then
echo "✓ Obsolete packages removed"
else
echo "✗ Failed to remove obsolete packages"
fi
fi
# Remove unused packages and dependencies
progress "Removing unused packages and dependencies"
if [ $DRY_RUN -eq 0 ]; then
# List packages to be removed first
echo "Packages that will be removed:"
run_with_privileges apt-get autoremove -y --dry-run | grep "^Remv"
# Ask for confirmation
if prompt_with_timeout "Proceed with removing these packages? (y/n): "; then
if run_with_privileges apt-get autoremove -y; then
echo "✓ Unused packages and dependencies removed"
else
echo "✗ Failed to remove unused packages"
fi
else
echo "Skipping package removal"
fi
fi
# Remove old kernel versions with extra precautions
progress "Removing old kernel versions"
if [ $DRY_RUN -eq 0 ]; then
# Get current kernel version
current_kernel=$(uname -r)
echo "Current kernel: $current_kernel"
# List installed kernels
installed_kernels=$(run_with_privileges dpkg --list | grep -E 'linux-image-[0-9]+' | awk '{print $2}')
echo "Installed kernels:"
echo "$installed_kernels"
# Ask for confirmation
if prompt_with_timeout "Proceed with removing old kernels? (y/n): "; then
if run_with_privileges apt-get autoremove --purge -y; then
echo "✓ Old kernel versions removed"
else
echo "✗ Failed to remove old kernel versions"
fi
else
echo "Skipping kernel removal"
fi
fi
# BleachBit installation and cleanup with more control
progress "Running BleachBit cleanup"
if [ $DRY_RUN -eq 0 ]; then
if ! command -v bleachbit &> /dev/null; then
echo "BleachBit not installed"
if prompt_with_timeout "Do you want to install BleachBit? (y/n): "; then
echo "Installing BleachBit..."
if run_with_privileges apt-get install -y bleachbit; then
echo "✓ BleachBit installed successfully"
else
echo "✗ Failed to install BleachBit"
fi
else
echo "Skipping BleachBit installation"
fi
fi
if command -v bleachbit &> /dev/null; then
echo "BleachBit cleaners available:"
bleachbit --list | grep -v "^$"
echo "Running safe BleachBit cleaners..."
safe_cleaners="system.cache system.localizations system.tmp system.rotated_logs"
run_with_privileges bleachbit --clean $safe_cleaners
echo "✓ System caches cleaned using BleachBit"
if prompt_with_timeout "Run additional BleachBit cleaners? (y/n): "; then
additional_cleaners="system.clipboard firefox.cache firefox.cookies google_chrome.cache thumbnails.cache"
run_with_privileges bleachbit --clean $additional_cleaners
echo "✓ Additional caches cleaned using BleachBit"
fi
fi
fi
# Remove unused Snap packages with better reporting
progress "Checking for unused Snap packages"
if [ $DRY_RUN -eq 0 ] && command -v snap &> /dev/null; then
# List unused snap packages
unused_snaps=$(run_with_privileges snap list --all | awk '/disabled/{print $1, $3}')
if [ -n "$unused_snaps" ]; then
echo "Unused Snap packages found:"
echo "$unused_snaps"
if prompt_with_timeout "Do you want to remove these unused Snap packages? (y/n): "; then
echo "$unused_snaps" | while read snapname revision; do
echo "Removing $snapname revision $revision"
run_with_privileges snap remove "$snapname" --revision="$revision"
done
echo "✓ Unused Snap packages removed"
else
echo "Skipping Snap package removal"
fi
else
echo "No unused Snap packages found"
fi
fi
# Clean flatpak unused runtimes and applications
progress "Checking for unused Flatpak runtimes"
if [ $DRY_RUN -eq 0 ] && command -v flatpak &> /dev/null; then
echo "Removing unused Flatpak runtimes..."
run_with_privileges flatpak uninstall --unused -y
echo "✓ Unused Flatpak runtimes removed"
fi
# Clear thumbnail cache more selectively
progress "Clearing thumbnail cache"
if [ $DRY_RUN -eq 0 ]; then
thumbnail_size_before=$(du -sh ~/.cache/thumbnails 2>/dev/null | cut -f1)
echo "Thumbnail cache size before cleanup: $thumbnail_size_before"
# Remove thumbnails older than 30 days
if find ~/.cache/thumbnails -type f -mtime +30 -delete 2>/dev/null; then
echo "✓ Old thumbnail cache cleared"
else
echo "✗ Failed to clear thumbnail cache"
fi
thumbnail_size_after=$(du -sh ~/.cache/thumbnails 2>/dev/null | cut -f1)
echo "Thumbnail cache size after cleanup: $thumbnail_size_after"
fi
# Clean up systemd journal logs with better size control
progress "Cleaning up systemd journal logs"
if [ $DRY_RUN -eq 0 ] && command -v journalctl &> /dev/null; then
echo "Journal size before cleanup:"
run_with_privileges journalctl --disk-usage
# Keep logs from the last X days
if run_with_privileges journalctl --vacuum-time="${DEFAULT_RETENTION_DAYS}d"; then
echo "✓ Systemd journal logs cleaned (keeping last $DEFAULT_RETENTION_DAYS days)"
else
echo "✗ Failed to clean systemd journal logs by time"
fi
# Also limit by size
if run_with_privileges journalctl --vacuum-size=50M; then
echo "✓ Systemd journal logs cleaned (limiting to 50M)"
else
echo "✗ Failed to clean systemd journal logs by size"
fi
echo "Journal size after cleanup:"
run_with_privileges journalctl --disk-usage
fi
# Remove old files from /tmp directory with better safety checks
progress "Cleaning up /tmp directory"
if [ $DRY_RUN -eq 0 ]; then
tmp_size_before=$(du -sh /tmp 2>/dev/null | cut -f1)
echo "Temporary directory size before cleanup: $tmp_size_before"
# Only remove files older than X days and not in use
if run_with_privileges find /tmp -type f -atime +$DEFAULT_RETENTION_DAYS -not -exec fuser -s {} \; -delete; then
echo "✓ Old files from /tmp directory removed"
else
echo "✗ Failed to remove old files from /tmp"
fi
tmp_size_after=$(du -sh /tmp 2>/dev/null | cut -f1)
echo "Temporary directory size after cleanup: $tmp_size_after"
fi
# Clean Firefox profiles if present
progress "Cleaning browser caches"
if [ $DRY_RUN -eq 0 ]; then
# Firefox cache
if [ -d ~/.mozilla/firefox ]; then
echo "Cleaning Firefox cache..."
find ~/.mozilla/firefox -name "*Cache*" -type d -exec rm -rf {} \; 2>/dev/null || true
find ~/.mozilla/firefox -name "*cache*" -type d -exec rm -rf {} \; 2>/dev/null || true
echo "✓ Firefox cache cleaned"
fi
# Chrome/Chromium cache
for browser_dir in ~/.config/google-chrome ~/.config/chromium; do
if [ -d "$browser_dir" ]; then
echo "Cleaning $(basename "$browser_dir") cache..."
find "$browser_dir" -name "Cache" -type d -exec rm -rf {} \; 2>/dev/null || true
find "$browser_dir" -name "*cache*" -type d -exec rm -rf {} \; 2>/dev/null || true
echo "$(basename "$browser_dir") cache cleaned"
fi
done
fi
# Remove old log files from /var/log with improved compression
progress "Managing log files"
if [ $DRY_RUN -eq 0 ]; then
log_size_before=$(du -sh /var/log 2>/dev/null | cut -f1)
echo "Log directory size before cleanup: $log_size_before"
# Compress log files older than 7 days
if run_with_privileges find /var/log -type f -name "*.log" -mtime +7 -exec gzip -9 {} \; 2>/dev/null; then
echo "✓ Old log files compressed"
else
echo "✗ Failed to compress some old log files"
fi
# Remove rotated logs older than retention period
if run_with_privileges find /var/log -type f -name "*.gz" -mtime +$DEFAULT_RETENTION_DAYS -delete 2>/dev/null; then
echo "✓ Very old compressed logs removed"
else
echo "✗ Failed to remove some very old compressed logs"
fi
log_size_after=$(du -sh /var/log 2>/dev/null | cut -f1)
echo "Log directory size after cleanup: $log_size_after"
fi
# Clean up core dump files with more information
progress "Checking core dump files"
if [ $DRY_RUN -eq 0 ]; then
# Check for core dumps and their sizes
if [ -d /var/lib/apport/coredump ]; then
core_size=$(du -sh /var/lib/apport/coredump 2>/dev/null | cut -f1)
core_count=$(find /var/lib/apport/coredump -type f -name "core*" | wc -l)
echo "Found $core_count core dump files using $core_size of space"
if [ $core_count -gt 0 ] && prompt_with_timeout "Do you want to delete core dump files? (y/n): "; then
if run_with_privileges find /var/lib/apport/coredump -type f -name 'core*' -delete; then
echo "✓ Core dump files cleaned"
else
echo "✗ Failed to clean core dump files"
fi
else
echo "Skipping core dump removal"
fi
else
echo "No core dump directory found"
fi
fi
# Remove orphaned packages with detailed listing
progress "Checking for orphaned packages"
if [ $DRY_RUN -eq 0 ]; then
if command -v deborphan &> /dev/null; then
orphaned=$(deborphan)
if [ -n "$orphaned" ]; then
echo "Orphaned packages found:"
echo "$orphaned"
if prompt_with_timeout "Do you want to remove these orphaned packages? (y/n): "; then
run_with_privileges deborphan | xargs run_with_privileges apt-get -y remove --purge
echo "✓ Orphaned packages removed"
else
echo "Skipping orphaned package removal"
fi
else
echo "No orphaned packages found"
fi
else
echo "deborphan not installed, skipping orphaned package check"
if prompt_with_timeout "Do you want to install deborphan to check for orphaned packages? (y/n): "; then
run_with_privileges apt-get install -y deborphan
echo "✓ deborphan installed"
progress "Re-checking for orphaned packages"
orphaned=$(deborphan)
if [ -n "$orphaned" ]; then
echo "Orphaned packages found:"
echo "$orphaned"
if prompt_with_timeout "Do you want to remove these orphaned packages? (y/n): "; then
run_with_privileges deborphan | xargs run_with_privileges apt-get -y remove --purge
echo "✓ Orphaned packages removed"
fi
else
echo "No orphaned packages found"
fi
fi
fi
fi
# Clean package installation backup files
progress "Cleaning package backup files"
if [ $DRY_RUN -eq 0 ]; then
dpkg_backup_size=$(find /var/backups -name "dpkg.status*" | xargs du -ch 2>/dev/null | grep total$ | cut -f1)
echo "Package backup files size: $dpkg_backup_size"
# Keep only the most recent 5 backups
if run_with_privileges find /var/backups -name "dpkg.status.*" | sort -r | tail -n +6 | xargs rm -f 2>/dev/null; then
echo "✓ Old package backup files cleaned"
else
echo "✗ Failed to clean old package backup files"
fi
fi
# Clean temporary downloaded package files
progress "Cleaning downloaded package files"
if [ $DRY_RUN -eq 0 ]; then
apt_archives_size=$(du -sh /var/cache/apt/archives 2>/dev/null | cut -f1)
echo "Downloaded package files size: $apt_archives_size"
if run_with_privileges apt-get clean; then
echo "✓ Downloaded package files cleaned"
else
echo "✗ Failed to clean downloaded package files"
fi
fi
# Final system update and upgrade
progress "Performing final system update"
if [ $DRY_RUN -eq 0 ]; then
if prompt_with_timeout "Do you want to update all installed packages? (y/n): "; then
if run_with_privileges apt-get update && run_with_privileges apt-get upgrade -y; then
echo "✓ System packages updated successfully"
else
echo "✗ System update failed"
fi
else
echo "Skipping system update"
fi
fi
# Show final disk space usage and calculate savings
echo -e "\nFinal disk space usage:"
df -h / /home
final_space=$(df -h / | awk 'NR==2 {print $4}')
final_usage_percent=$(df -h / | awk 'NR==2 {print $5}')
# Try to calculate space saved
if [[ "$initial_space" =~ [0-9]+(\.[0-9]+)?[GM] ]] && [[ "$final_space" =~ [0-9]+(\.[0-9]+)?[GM] ]]; then
# Check if both have the same unit (G or M)
init_unit="${initial_space: -1}"
final_unit="${final_space: -1}"
if [ "$init_unit" = "$final_unit" ]; then
init_num=$(echo "$initial_space" | sed 's/[^0-9.]//g')
final_num=$(echo "$final_space" | sed 's/[^0-9.]//g')
saved=$(echo "$final_num - $init_num" | bc)
echo -e "\nSpace freed: $saved$init_unit"
else
echo -e "\nInitial free space: $initial_space"
echo "Final free space: $final_space"
echo "Units are different, can't calculate exact space saved"
fi
else
echo -e "\nInitial free space: $initial_space"
echo "Final free space: $final_space"
fi
echo "Initial usage: $initial_usage_percent"
echo "Final usage: $final_usage_percent"
echo "Log file: $LOG_FILE"
echo -e "\nCleanup process completed successfully!"
echo "This script has been executed with enhanced security measures and comprehensive reporting."
echo "NOTE: No backups were created. This is a non-reversible cleanup operation."
@5taras
Copy link

5taras commented Feb 26, 2025

./ubuntu_cleanup.sh: 27: Syntax error: redirection unexpected

@Limbicnation
Copy link
Author

@5taras I have changed it to: #!/usr/bin/env bash

This will fix your "redirection unexpected" error by making sure the script runs with bash, which supports the process substitution syntax you're using.

Please use the script with caution!

@5taras
Copy link

5taras commented Feb 27, 2025

Useful script. I'd add localepurge too. I found also a useful ucaresystem-core app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment