-
-
Save Limbicnation/6763b69ab6a406790f3b7d4b56a2f6e8 to your computer and use it in GitHub Desktop.
| #!/usr/bin/env bash | |
| # Security-Hardened Ubuntu Cleanup Script | |
| # This script performs comprehensive system cleanup with enterprise-grade security | |
| # EXCLUDES: hy3dgen folder from any deletion operations | |
| # | |
| # Security improvements: | |
| # - Comprehensive error handling with trap handlers | |
| # - Safe configuration loading without arbitrary code execution | |
| # - APT and script-level locking mechanisms | |
| # - Kernel retention validation (N-1 policy) | |
| # - System snapshot before destructive operations | |
| # - Syslog integration for audit trail | |
| # - Resource limit enforcement | |
| # - Proper exit codes for monitoring | |
| # Exit codes | |
| readonly EXIT_SUCCESS=0 | |
| readonly EXIT_LOCK_FAILED=1 | |
| readonly EXIT_APT_LOCKED=2 | |
| readonly EXIT_NO_PRIVILEGES=3 | |
| readonly EXIT_DEPENDENCY_MISSING=4 | |
| readonly EXIT_USER_ABORT=5 | |
| readonly EXIT_OPERATION_FAILED=10 | |
| readonly EXIT_CONFIG_INVALID=11 | |
| # Strict error handling | |
| set -euo pipefail | |
| IFS=$'\n\t' | |
| # Configuration variables | |
| 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" | |
| LOCK_FILE="/var/lock/ubuntu_cleanup.lock" | |
| LOCK_FD=200 | |
| DRY_RUN=0 | |
| VERBOSE=0 | |
| TIMEOUT_DURATION=60 | |
| PARALLEL_JOBS=2 | |
| MAX_RESOURCE_USAGE=50 | |
| DEFAULT_RETENTION_DAYS=10 | |
| HAS_ROOT=0 | |
| # EXCLUSION PATTERNS - Add folders/files to protect here | |
| EXCLUDED_PATTERNS=( | |
| "hy3dgen" | |
| "Hunyuan3D" | |
| "huggingface" | |
| ".git" | |
| ".venv" | |
| "node_modules" | |
| "venv" | |
| "env" | |
| ".env" | |
| ) | |
| # Logging function with syslog integration | |
| log_operation() { | |
| local severity=$1 | |
| local message=$2 | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$severity] $message" | |
| # Send to syslog if logger is available | |
| if command -v logger &>/dev/null; then | |
| logger -t ubuntu_cleanup -p "user.$severity" "$message" | |
| fi | |
| } | |
| # Cleanup handler for trap | |
| cleanup_on_error() { | |
| local exit_code=$? | |
| if [ $exit_code -ne 0 ]; then | |
| log_operation "err" "Script failed with exit code $exit_code at line ${BASH_LINENO[0]}" | |
| log_operation "err" "Failed command: ${BASH_COMMAND}" | |
| fi | |
| # Release lock if held | |
| release_lock | |
| exit $exit_code | |
| } | |
| # Set trap handlers | |
| trap cleanup_on_error ERR EXIT | |
| trap 'log_operation "warning" "Script interrupted by user"; exit $EXIT_USER_ABORT' SIGTERM SIGINT | |
| # Create log directory if it doesn't exist with proper permissions | |
| mkdir -p "$LOG_DIR" 2>/dev/null || true | |
| chmod 750 "$LOG_DIR" 2>/dev/null || true | |
| # Setup logging with rotation | |
| exec > >(tee -a "$LOG_FILE") 2>&1 | |
| # Safe configuration loader - no arbitrary code execution | |
| load_safe_config() { | |
| if [ ! -f "$CONFIG_FILE" ]; then | |
| return 0 | |
| fi | |
| log_operation "info" "Loading configuration from $CONFIG_FILE" | |
| # Validate config file syntax | |
| if ! grep -qE '^\s*(#|[A-Z_]+=|$)' "$CONFIG_FILE"; then | |
| log_operation "err" "Config file contains invalid syntax" | |
| return $EXIT_CONFIG_INVALID | |
| fi | |
| # Load only whitelisted variables with validation | |
| while IFS='=' read -r key value; do | |
| # Skip comments and empty lines | |
| [[ "$key" =~ ^#.*$ || -z "$key" ]] && continue | |
| # Remove leading/trailing whitespace | |
| key=$(echo "$key" | xargs) | |
| value=$(echo "$value" | xargs) | |
| case "$key" in | |
| TIMEOUT_DURATION|PARALLEL_JOBS|MAX_RESOURCE_USAGE|DEFAULT_RETENTION_DAYS) | |
| # Validate value is numeric and within reasonable range | |
| if [[ "$value" =~ ^[0-9]+$ ]] && [ "$value" -ge 1 ] && [ "$value" -le 9999 ]; then | |
| declare -g "$key=$value" | |
| log_operation "info" "Loaded config: $key=$value" | |
| else | |
| log_operation "warning" "Invalid value for $key: $value (skipped)" | |
| fi | |
| ;; | |
| *) | |
| log_operation "warning" "Unknown config variable: $key (skipped)" | |
| ;; | |
| esac | |
| done < <(grep -v '^#' "$CONFIG_FILE" | grep -v '^$') | |
| } | |
| # Script-level locking mechanism | |
| acquire_lock() { | |
| exec 200>"$LOCK_FILE" | |
| if ! flock -n 200; then | |
| log_operation "err" "Another instance is already running" | |
| exit $EXIT_LOCK_FAILED | |
| fi | |
| echo $$ >&200 | |
| log_operation "info" "Lock acquired (PID: $$)" | |
| } | |
| release_lock() { | |
| if [ -n "${LOCK_FD:-}" ]; then | |
| flock -u $LOCK_FD 2>/dev/null || true | |
| rm -f "$LOCK_FILE" 2>/dev/null || true | |
| fi | |
| } | |
| # APT lock checking with timeout | |
| check_apt_lock() { | |
| local max_wait=300 # 5 minutes | |
| local waited=0 | |
| local lock_files=( | |
| "/var/lib/dpkg/lock-frontend" | |
| "/var/lib/apt/lists/lock" | |
| "/var/cache/apt/archives/lock" | |
| ) | |
| while true; do | |
| local locked=0 | |
| for lock_file in "${lock_files[@]}"; do | |
| if fuser "$lock_file" >/dev/null 2>&1; then | |
| locked=1 | |
| break | |
| fi | |
| done | |
| if [ $locked -eq 0 ]; then | |
| return 0 | |
| fi | |
| if [ $waited -ge $max_wait ]; then | |
| log_operation "err" "APT lock held for $max_wait seconds, aborting" | |
| exit $EXIT_APT_LOCKED | |
| fi | |
| log_operation "warning" "Waiting for APT lock to be released... ($waited/$max_wait seconds)" | |
| sleep 5 | |
| waited=$((waited + 5)) | |
| done | |
| } | |
| # Check for root privileges | |
| check_root() { | |
| if [[ $EUID -eq 0 ]]; then | |
| HAS_ROOT=1 | |
| log_operation "info" "Running with root privileges" | |
| else | |
| HAS_ROOT=0 | |
| log_operation "info" "Running without root, will use sudo for privileged operations" | |
| # Test sudo access | |
| if ! sudo -n true 2>/dev/null; then | |
| log_operation "warning" "Sudo password may be required" | |
| fi | |
| fi | |
| } | |
| # Function to handle privileged commands (no eval!) | |
| run_with_privileges() { | |
| if [[ $HAS_ROOT -eq 1 ]]; then | |
| "$@" | |
| else | |
| sudo "$@" | |
| fi | |
| } | |
| # Resource limit enforcement | |
| enforce_resource_limits() { | |
| # Set nice priority for CPU | |
| renice -n 10 -p $$ >/dev/null 2>&1 || true | |
| # Set ionice for disk I/O (idle class) | |
| if command -v ionice &>/dev/null; then | |
| ionice -c 3 -p $$ >/dev/null 2>&1 || true | |
| fi | |
| log_operation "info" "Resource limits enforced: nice=10, ionice=idle" | |
| } | |
| # Enhanced path validation with parent directory checking | |
| is_safe_path() { | |
| local path=$1 | |
| # Resolve to absolute path | |
| local abs_path | |
| abs_path=$(readlink -f "$path" 2>/dev/null) || { | |
| log_operation "warning" "Cannot resolve path: $path" | |
| return 1 | |
| } | |
| # Critical system paths and their subdirectories | |
| local dangerous_paths=( | |
| "/" | |
| "/bin" | |
| "/boot" | |
| "/dev" | |
| "/etc" | |
| "/lib" | |
| "/lib64" | |
| "/proc" | |
| "/root" | |
| "/sbin" | |
| "/sys" | |
| "/usr" | |
| ) | |
| for dangerous_path in "${dangerous_paths[@]}"; do | |
| # Check if path is exactly the dangerous path or under it | |
| if [[ "$abs_path" == "$dangerous_path" ]] || [[ "$abs_path" == "$dangerous_path"/* ]]; then | |
| log_operation "err" "Refusing to remove path under critical system directory: $abs_path" | |
| return 1 | |
| fi | |
| done | |
| return 0 | |
| } | |
| # Function to check if path should be excluded | |
| is_excluded() { | |
| local path=$1 | |
| for pattern in "${EXCLUDED_PATTERNS[@]}"; do | |
| if [[ "$path" == *"$pattern"* ]]; then | |
| log_operation "info" "Excluding protected path: $path (matches pattern: $pattern)" | |
| return 0 | |
| fi | |
| done | |
| return 1 | |
| } | |
| # Function for timeout handling with user prompts | |
| prompt_with_timeout() { | |
| local prompt=$1 | |
| local timeout=$TIMEOUT_DURATION | |
| local response | |
| read -t "$timeout" -p "$prompt" response || true | |
| if [ -z "$response" ]; then | |
| echo "Timeout reached, assuming default answer (n)" | |
| return 1 | |
| fi | |
| if [[ "$response" =~ ^[Yy]$ ]]; then | |
| return 0 | |
| else | |
| return 1 | |
| fi | |
| } | |
| # Function to safely remove files with exclusion check | |
| safe_remove() { | |
| local target=$1 | |
| local force=${2:-0} | |
| # Check if target should be excluded | |
| if is_excluded "$target"; then | |
| return 0 | |
| fi | |
| # Check if path is safe | |
| if ! is_safe_path "$target"; then | |
| return 1 | |
| fi | |
| # Check if target exists | |
| if [ ! -e "$target" ]; then | |
| [ $VERBOSE -eq 1 ] && log_operation "info" "Target does not exist, skipping: $target" | |
| return 0 | |
| fi | |
| # Safe removal | |
| if [ -d "$target" ] && [ ! -L "$target" ]; then | |
| log_operation "info" "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 | |
| log_operation "info" "Removing $target" | |
| if [ $force -eq 1 ]; then | |
| rm -f "$target" 2>/dev/null || true | |
| else | |
| rm "$target" 2>/dev/null || true | |
| fi | |
| fi | |
| return 0 | |
| } | |
| # Enhanced find command that excludes protected patterns | |
| safe_find() { | |
| local base_path=$1 | |
| shift | |
| local find_args=("$@") | |
| # Build exclusion arguments for find | |
| local exclude_args=() | |
| for pattern in "${EXCLUDED_PATTERNS[@]}"; do | |
| exclude_args+=(-not -path "*${pattern}*") | |
| done | |
| find "$base_path" "${exclude_args[@]}" "${find_args[@]}" 2>/dev/null || true | |
| } | |
| # System snapshot before destructive operations | |
| create_system_snapshot() { | |
| local snapshot_dir="/var/backups/ubuntu_cleanup_$(date +%Y%m%d_%H%M%S)" | |
| log_operation "info" "Creating system snapshot: $snapshot_dir" | |
| run_with_privileges mkdir -p "$snapshot_dir" | |
| # Backup package state | |
| dpkg --get-selections > "$snapshot_dir/package_selections.txt" 2>/dev/null || true | |
| apt-mark showmanual > "$snapshot_dir/manual_packages.txt" 2>/dev/null || true | |
| # Backup kernel list | |
| dpkg -l | grep -E 'linux-image|linux-headers' > "$snapshot_dir/kernel_list.txt" 2>/dev/null || true | |
| # Backup sources list | |
| run_with_privileges cp -r /etc/apt/sources.list* "$snapshot_dir/" 2>/dev/null || true | |
| log_operation "info" "System snapshot created: $snapshot_dir" | |
| echo "$snapshot_dir" > /tmp/ubuntu_cleanup_snapshot.path | |
| } | |
| # Safe kernel cleanup with N-1 retention validation | |
| safe_kernel_cleanup() { | |
| local current_kernel=$(uname -r) | |
| local installed_kernels | |
| installed_kernels=$(dpkg -l | grep -E '^ii.*linux-image-[0-9]' | awk '{print $2}' | grep -v "linux-image-generic" || true) | |
| local kernel_count=$(echo "$installed_kernels" | grep -v '^$' | wc -l) | |
| log_operation "info" "Current kernel: $current_kernel" | |
| log_operation "info" "Installed kernels count: $kernel_count" | |
| if [ $kernel_count -le 2 ]; then | |
| log_operation "warning" "Only $kernel_count kernels installed. Skipping cleanup to maintain N-1 policy (minimum 2 kernels required)" | |
| return 0 | |
| fi | |
| echo "Installed kernels:" | |
| echo "$installed_kernels" | |
| echo "" | |
| echo "This will retain current kernel + at least 1 previous version" | |
| if prompt_with_timeout "Remove old kernels (keeping current + 1)? (y/n): "; then | |
| check_apt_lock | |
| if run_with_privileges apt-get autoremove --purge -y; then | |
| log_operation "info" "Kernel cleanup completed successfully" | |
| # Verify we still have enough kernels | |
| local remaining_kernels | |
| remaining_kernels=$(dpkg -l | grep -E '^ii.*linux-image-[0-9]' | awk '{print $2}' | grep -v "linux-image-generic" | wc -l) | |
| if [ $remaining_kernels -lt 2 ]; then | |
| log_operation "err" "CRITICAL: Less than 2 kernels remaining after cleanup!" | |
| return 1 | |
| fi | |
| log_operation "info" "Remaining kernels: $remaining_kernels" | |
| else | |
| log_operation "err" "Kernel cleanup failed" | |
| return 1 | |
| fi | |
| fi | |
| } | |
| # Log rotation for script logs | |
| rotate_old_logs() { | |
| log_operation "info" "Rotating old cleanup logs" | |
| # Keep only last 30 days of cleanup logs | |
| find "$LOG_DIR" -name "cleanup_*.log" -mtime +30 -delete 2>/dev/null || true | |
| # Keep max 50 log files | |
| local log_files | |
| log_files=$(ls -t "$LOG_DIR"/cleanup_*.log 2>/dev/null | tail -n +51) | |
| if [ -n "$log_files" ]; then | |
| echo "$log_files" | xargs rm -f 2>/dev/null || true | |
| fi | |
| } | |
| # Check dependencies | |
| check_dependencies() { | |
| local missing_tools=() | |
| for tool in apt-get find grep awk bc fuser flock; do | |
| if ! command -v "$tool" &>/dev/null; then | |
| missing_tools+=("$tool") | |
| fi | |
| done | |
| if [ ${#missing_tools[@]} -gt 0 ]; then | |
| log_operation "err" "Missing required tools: ${missing_tools[*]}" | |
| exit $EXIT_DEPENDENCY_MISSING | |
| fi | |
| } | |
| # Record initial disk space | |
| record_disk_space() { | |
| log_operation "info" "Initial disk space usage:" | |
| df -h / /home | |
| initial_space=$(df / | awk 'NR==2 {print $4}') | |
| } | |
| # 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 (default: 2)" | |
| echo " -r Maximum CPU percentage (default: 50)" | |
| echo " -k Retention days for logs (default: 10)" | |
| exit 1 ;; | |
| esac | |
| done | |
| # Progress function | |
| total_steps=20 | |
| current_step=0 | |
| progress() { | |
| current_step=$((current_step + 1)) | |
| percentage=$((current_step * 100 / total_steps)) | |
| log_operation "info" "[$current_step/$total_steps - $percentage%] $1" | |
| } | |
| # Main execution starts here | |
| log_operation "info" "=== Ubuntu Cleanup Script (Hardened) Started ===" | |
| log_operation "info" "PID: $$" | |
| # Acquire lock first | |
| acquire_lock | |
| # Load configuration safely | |
| load_safe_config || exit $EXIT_CONFIG_INVALID | |
| # Initialize | |
| check_dependencies | |
| check_root | |
| enforce_resource_limits | |
| rotate_old_logs | |
| record_disk_space | |
| log_operation "info" "Protected patterns: ${EXCLUDED_PATTERNS[*]}" | |
| [ $DRY_RUN -eq 1 ] && log_operation "warning" "DRY RUN MODE - No changes will be made" | |
| # Confirmation prompt | |
| if [ $DRY_RUN -eq 0 ]; then | |
| if ! prompt_with_timeout "Proceed with cleanup? (y/n): "; then | |
| log_operation "warning" "User aborted cleanup" | |
| exit $EXIT_USER_ABORT | |
| fi | |
| # Create system snapshot before any destructive operations | |
| create_system_snapshot | |
| fi | |
| # 1. Update package list | |
| progress "Updating package list" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| check_apt_lock | |
| run_with_privileges apt-get update || log_operation "warning" "Failed to update package list" | |
| fi | |
| # 2. Clear user cache (with exclusions) | |
| progress "Clearing user cache (excluding protected folders)" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| cache_size_before=$(du -sh ~/.cache 2>/dev/null | cut -f1) | |
| log_operation "info" "Cache size before: $cache_size_before" | |
| # Remove cache files older than 3 days, excluding protected patterns | |
| safe_find ~/.cache -type f -mtime +3 -delete | |
| cache_size_after=$(du -sh ~/.cache 2>/dev/null | cut -f1) | |
| log_operation "info" "Cache size after: $cache_size_after" | |
| fi | |
| # 3. Clean APT cache | |
| progress "Cleaning APT cache" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| check_apt_lock | |
| run_with_privileges apt-get clean | |
| fi | |
| # 4. Remove obsolete packages | |
| progress "Removing obsolete packages" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| check_apt_lock | |
| run_with_privileges apt-get autoclean | |
| fi | |
| # 5. Remove unused packages | |
| progress "Removing unused packages" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| check_apt_lock | |
| log_operation "info" "Packages to be removed:" | |
| run_with_privileges apt-get autoremove -y --dry-run | grep "^Remv" || log_operation "info" "No packages to remove" | |
| if prompt_with_timeout "Remove these packages? (y/n): "; then | |
| run_with_privileges apt-get autoremove -y | |
| fi | |
| fi | |
| # 6. Remove old kernels (SAFE with N-1 validation) | |
| progress "Checking old kernel versions" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| safe_kernel_cleanup | |
| fi | |
| # 7. Clean Snap packages | |
| progress "Cleaning Snap packages" | |
| if [ $DRY_RUN -eq 0 ] && command -v snap &> /dev/null; then | |
| run_with_privileges snap list --all | awk '/disabled/{print $1, $3}' | \ | |
| while read -r snapname revision; do | |
| log_operation "info" "Removing $snapname revision $revision" | |
| run_with_privileges snap remove "$snapname" --revision="$revision" || true | |
| done | |
| fi | |
| # 8. Clean Flatpak | |
| progress "Cleaning Flatpak" | |
| if [ $DRY_RUN -eq 0 ] && command -v flatpak &> /dev/null; then | |
| run_with_privileges flatpak uninstall --unused -y || true | |
| fi | |
| # 9. Clear thumbnails (with age limit) | |
| progress "Clearing old thumbnails" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| safe_find ~/.cache/thumbnails -type f -mtime +30 -delete | |
| fi | |
| # 10. Clean journal logs | |
| progress "Cleaning systemd journal logs" | |
| if [ $DRY_RUN -eq 0 ] && command -v journalctl &> /dev/null; then | |
| run_with_privileges journalctl --vacuum-time="${DEFAULT_RETENTION_DAYS}d" | |
| run_with_privileges journalctl --vacuum-size=50M | |
| fi | |
| # 11. Clean /tmp (carefully) | |
| progress "Cleaning /tmp directory" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| # Only remove files older than retention days and not in use | |
| run_with_privileges find /tmp -type f -atime +$DEFAULT_RETENTION_DAYS \ | |
| -not -exec fuser -s {} \; -delete 2>/dev/null || true | |
| fi | |
| # 12. Clean browser caches (with exclusions) | |
| progress "Cleaning browser caches" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| # Firefox | |
| if [ -d ~/.mozilla/firefox ]; then | |
| safe_find ~/.mozilla/firefox -name "*Cache*" -type d -exec rm -rf {} + 2>/dev/null || true | |
| fi | |
| # Chrome/Chromium | |
| for browser_dir in ~/.config/google-chrome ~/.config/chromium; do | |
| if [ -d "$browser_dir" ]; then | |
| safe_find "$browser_dir" -name "Cache" -type d -exec rm -rf {} + 2>/dev/null || true | |
| fi | |
| done | |
| fi | |
| # 13. Manage log files | |
| progress "Managing log files" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| # Compress old logs | |
| run_with_privileges find /var/log -type f -name "*.log" -mtime +7 ! -exec fuser -s {} \; -exec gzip -9 {} \; 2>/dev/null || true | |
| # Remove very old compressed logs | |
| run_with_privileges find /var/log -type f -name "*.gz" -mtime +$DEFAULT_RETENTION_DAYS -delete 2>/dev/null || true | |
| fi | |
| # 14. Check core dumps | |
| progress "Checking core dumps" | |
| if [ $DRY_RUN -eq 0 ] && [ -d /var/lib/apport/coredump ]; then | |
| core_count=$(find /var/lib/apport/coredump -type f -name "core*" 2>/dev/null | wc -l) | |
| if [ $core_count -gt 0 ]; then | |
| log_operation "info" "Found $core_count core dump files" | |
| if prompt_with_timeout "Delete core dumps? (y/n): "; then | |
| run_with_privileges find /var/lib/apport/coredump -type f -name 'core*' -delete | |
| fi | |
| fi | |
| fi | |
| # 15. Clean package backups | |
| progress "Cleaning package backups" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| # Keep only recent backups (last 5) | |
| run_with_privileges bash -c "ls -t /var/backups/dpkg.status.* 2>/dev/null | tail -n +6 | xargs rm -f" 2>/dev/null || true | |
| fi | |
| # 16. Clean pip cache | |
| progress "Cleaning pip cache" | |
| if [ $DRY_RUN -eq 0 ] && command -v pip &> /dev/null; then | |
| pip cache purge 2>/dev/null || true | |
| fi | |
| # 17. Clean conda cache | |
| progress "Cleaning conda cache" | |
| if [ $DRY_RUN -eq 0 ] && command -v conda &> /dev/null; then | |
| conda clean --all -y 2>/dev/null || true | |
| fi | |
| # 18. Clean npm cache | |
| progress "Cleaning npm cache" | |
| if [ $DRY_RUN -eq 0 ] && [ -d ~/.npm ]; then | |
| # Exclude node_modules and other important npm folders | |
| safe_find ~/.npm/_cache -type f -mtime +7 -delete 2>/dev/null || true | |
| fi | |
| # 19. REMOVED: Automatic PPA addition (security risk) | |
| progress "Skipping automatic third-party repository addition" | |
| log_operation "info" "Automatic PPA addition removed for security. Install ucaresystem-core manually if needed." | |
| # 20. Final update | |
| progress "Final system update" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| if prompt_with_timeout "Update all packages? (y/n): "; then | |
| check_apt_lock | |
| run_with_privileges apt-get update && run_with_privileges apt-get upgrade -y | |
| fi | |
| fi | |
| # Show results | |
| log_operation "info" "Final disk space usage:" | |
| df -h / /home | |
| final_space=$(df / | awk 'NR==2 {print $4}') | |
| log_operation "info" "=== Cleanup completed successfully ===" | |
| log_operation "info" "Initial free space: $initial_space KB" | |
| log_operation "info" "Final free space: $final_space KB" | |
| space_freed=$((final_space - initial_space)) | |
| log_operation "info" "Space freed: $space_freed KB" | |
| log_operation "info" "Log file: $LOG_FILE" | |
| log_operation "info" "Protected patterns excluded: ${EXCLUDED_PATTERNS[*]}" | |
| if [ -f /tmp/ubuntu_cleanup_snapshot.path ]; then | |
| snapshot_path=$(cat /tmp/ubuntu_cleanup_snapshot.path) | |
| log_operation "info" "System snapshot available at: $snapshot_path" | |
| rm -f /tmp/ubuntu_cleanup_snapshot.path | |
| fi | |
| exit $EXIT_SUCCESS |
@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!
Useful script. I'd add localepurge too. I found also a useful ucaresystem-core app.
I updated the script to make it safer, and now you can be more selective about which folders you want to remove from the .cache
✅ Shebang line: Uses #!/usr/bin/env bash (already implemented)
✅ localepurge: Added with automatic installation prompt
✅ ucaresystem-core: Added with automatic installation prompt
What these tools do:
localepurge:
Removes unnecessary locale files (language packs)
Can free up significant space if you only use one or two languages
Automatically configures which locales to keep during installation
ucaresystem-core:
All-in-one Ubuntu maintenance tool
Performs: updates, removes old kernels, cleans apt cache, removes orphaned packages
Basically does many of the same tasks but in one command
The -u flag runs it in unattended mode
This script is specifically designed for Ubuntu/Debian systems and uses Linux-specific commands that don't exist on macOS.
I am using a macos theme, this is Ubuntu 24.04 : )
Hi, thanks for reporting this issue!
I've identified the problem - the interactive localepurge dialog in Step #19 was causing the script to hang. I've updated the script to resolve this issue for now.
Fix: Step #19 has been replaced with:
# 19. Localepurge - Remove unnecessary locale files (SKIPPED)
progress "Skipping locale files cleanup"
if [ $DRY_RUN -eq 0 ]; then
echo "Localepurge step skipped to avoid interactive dialog"
echo "✓ Locale cleanup skipped"
fiThank you so much : )
You're welcome! Thank you for reporting the issue. :)

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