diff --git a/lib/core/app_protection.sh b/lib/core/app_protection.sh index 05955cd..0a22392 100755 --- a/lib/core/app_protection.sh +++ b/lib/core/app_protection.sh @@ -13,7 +13,7 @@ _MOLE_CORE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" [[ -z "${MOLE_BASE_LOADED:-}" ]] && source "$_MOLE_CORE_DIR/base.sh" # Declare WHITELIST_PATTERNS if not already set (used by is_path_whitelisted) -if ! declare -p WHITELIST_PATTERNS &>/dev/null; then +if ! declare -p WHITELIST_PATTERNS &> /dev/null; then declare -a WHITELIST_PATTERNS=() fi @@ -439,12 +439,12 @@ is_critical_system_component() { lower=$(echo "$token" | LC_ALL=C tr '[:upper:]' '[:lower:]') case "$lower" in - *backgroundtaskmanagement* | *loginitems* | *systempreferences* | *systemsettings* | *settings* | *preferences* | *controlcenter* | *biometrickit* | *sfl* | *tcc*) - return 0 - ;; - *) - return 1 - ;; + *backgroundtaskmanagement* | *loginitems* | *systempreferences* | *systemsettings* | *settings* | *preferences* | *controlcenter* | *biometrickit* | *sfl* | *tcc*) + return 0 + ;; + *) + return 1 + ;; esac } @@ -522,25 +522,25 @@ should_protect_path() { # 2. Protect caches critical for system UI rendering # These caches are essential for modern macOS (Sonoma/Sequoia) system UI rendering case "$path" in - # System Settings and Control Center caches (CRITICAL - prevents blank panel bug) - *com.apple.systempreferences.cache* | *com.apple.Settings.cache* | *com.apple.controlcenter.cache*) - return 0 - ;; - # Finder and Dock (system essential) - *com.apple.finder.cache* | *com.apple.dock.cache*) - return 0 - ;; - # System XPC services and sandboxed containers - */Library/Containers/com.apple.Settings* | */Library/Containers/com.apple.SystemSettings* | */Library/Containers/com.apple.controlcenter*) - return 0 - ;; - */Library/Group\ Containers/com.apple.systempreferences* | */Library/Group\ Containers/com.apple.Settings*) - return 0 - ;; - # Shared file lists for System Settings (macOS Sequoia) - Issue #136 - */com.apple.sharedfilelist/*com.apple.Settings* | */com.apple.sharedfilelist/*com.apple.SystemSettings* | */com.apple.sharedfilelist/*systempreferences*) - return 0 - ;; + # System Settings and Control Center caches (CRITICAL - prevents blank panel bug) + *com.apple.systempreferences.cache* | *com.apple.Settings.cache* | *com.apple.controlcenter.cache*) + return 0 + ;; + # Finder and Dock (system essential) + *com.apple.finder.cache* | *com.apple.dock.cache*) + return 0 + ;; + # System XPC services and sandboxed containers + */Library/Containers/com.apple.Settings* | */Library/Containers/com.apple.SystemSettings* | */Library/Containers/com.apple.controlcenter*) + return 0 + ;; + */Library/Group\ Containers/com.apple.systempreferences* | */Library/Group\ Containers/com.apple.Settings*) + return 0 + ;; + # Shared file lists for System Settings (macOS Sequoia) - Issue #136 + */com.apple.sharedfilelist/*com.apple.Settings* | */com.apple.sharedfilelist/*com.apple.SystemSettings* | */com.apple.sharedfilelist/*systempreferences*) + return 0 + ;; esac # 3. Extract bundle ID from sandbox paths @@ -555,24 +555,24 @@ should_protect_path() { # 4. Check for specific hardcoded critical patterns case "$path" in - *com.apple.Settings* | *com.apple.SystemSettings* | *com.apple.controlcenter* | *com.apple.finder* | *com.apple.dock*) - return 0 - ;; + *com.apple.Settings* | *com.apple.SystemSettings* | *com.apple.controlcenter* | *com.apple.finder* | *com.apple.dock*) + return 0 + ;; esac # 5. Protect critical preference files and user data case "$path" in - */Library/Preferences/com.apple.dock.plist | */Library/Preferences/com.apple.finder.plist) - return 0 - ;; - # Bluetooth and WiFi configurations - */ByHost/com.apple.bluetooth.* | */ByHost/com.apple.wifi.*) - return 0 - ;; - # iCloud Drive - protect user's cloud synced data - */Library/Mobile\ Documents* | */Mobile\ Documents*) - return 0 - ;; + */Library/Preferences/com.apple.dock.plist | */Library/Preferences/com.apple.finder.plist) + return 0 + ;; + # Bluetooth and WiFi configurations + */ByHost/com.apple.bluetooth.* | */ByHost/com.apple.wifi.*) + return 0 + ;; + # iCloud Drive - protect user's cloud synced data + */Library/Mobile\ Documents* | */Mobile\ Documents*) + return 0 + ;; esac # 6. Match full path against protected patterns @@ -611,9 +611,9 @@ is_path_whitelisted() { local check_pattern="${pattern%/}" local has_glob="false" case "$check_pattern" in - *\** | *\?* | *\[*) - has_glob="true" - ;; + *\** | *\?* | *\[*) + has_glob="true" + ;; esac # Check for exact match or glob pattern match @@ -716,17 +716,17 @@ find_app_files() { # Safety check: Skip if path ends with a common directory name (indicates empty app_name/bundle_id) # This prevents deletion of entire Library subdirectories when bundle_id is empty case "$expanded_path" in - */Library/Application\ Support | */Library/Application\ Support/ | \ - */Library/Caches | */Library/Caches/ | \ - */Library/Logs | */Library/Logs/ | \ - */Library/Containers | */Library/Containers/ | \ - */Library/WebKit | */Library/WebKit/ | \ - */Library/HTTPStorages | */Library/HTTPStorages/ | \ - */Library/Application\ Scripts | */Library/Application\ Scripts/ | \ - */Library/Autosave\ Information | */Library/Autosave\ Information/ | \ - */Library/Group\ Containers | */Library/Group\ Containers/) - continue - ;; + */Library/Application\ Support | */Library/Application\ Support/ | \ + */Library/Caches | */Library/Caches/ | \ + */Library/Logs | */Library/Logs/ | \ + */Library/Containers | */Library/Containers/ | \ + */Library/WebKit | */Library/WebKit/ | \ + */Library/HTTPStorages | */Library/HTTPStorages/ | \ + */Library/Application\ Scripts | */Library/Application\ Scripts/ | \ + */Library/Autosave\ Information | */Library/Autosave\ Information/ | \ + */Library/Group\ Containers | */Library/Group\ Containers/) + continue + ;; esac files_to_clean+=("$expanded_path") @@ -737,13 +737,13 @@ find_app_files() { [[ -f ~/Library/Preferences/"$bundle_id".plist ]] && files_to_clean+=("$HOME/Library/Preferences/$bundle_id.plist") [[ -d ~/Library/Preferences/ByHost ]] && while IFS= read -r -d '' pref; do files_to_clean+=("$pref") - done < <(command find ~/Library/Preferences/ByHost -maxdepth 1 \( -name "$bundle_id*.plist" \) -print0 2>/dev/null) + done < <(command find ~/Library/Preferences/ByHost -maxdepth 1 \( -name "$bundle_id*.plist" \) -print0 2> /dev/null) # Group Containers (special handling) if [[ -d ~/Library/Group\ Containers ]]; then while IFS= read -r -d '' container; do files_to_clean+=("$container") - done < <(command find ~/Library/Group\ Containers -maxdepth 1 \( -name "*$bundle_id*" \) -print0 2>/dev/null) + done < <(command find ~/Library/Group\ Containers -maxdepth 1 \( -name "*$bundle_id*" \) -print0 2> /dev/null) fi fi @@ -758,7 +758,7 @@ find_app_files() { continue fi files_to_clean+=("$plist") - done < <(command find ~/Library/LaunchAgents -maxdepth 1 -name "*$app_name*.plist" -print0 2>/dev/null) + done < <(command find ~/Library/LaunchAgents -maxdepth 1 -name "*$app_name*.plist" -print0 2> /dev/null) fi # Handle specialized toolchains and development environments @@ -774,7 +774,7 @@ find_app_files() { for d in ~/AndroidStudioProjects ~/Library/Android ~/.android; do [[ -d "$d" ]] && files_to_clean+=("$d") done - [[ -d ~/Library/Application\ Support/Google ]] && while IFS= read -r -d '' d; do files_to_clean+=("$d"); done < <(command find ~/Library/Application\ Support/Google -maxdepth 1 -name "AndroidStudio*" -print0 2>/dev/null) + [[ -d ~/Library/Application\ Support/Google ]] && while IFS= read -r -d '' d; do files_to_clean+=("$d"); done < <(command find ~/Library/Application\ Support/Google -maxdepth 1 -name "AndroidStudio*" -print0 2> /dev/null) fi # 3. Xcode (Apple) @@ -786,7 +786,7 @@ find_app_files() { # 4. JetBrains (IDE settings) if [[ "$bundle_id" =~ jetbrains ]] || [[ "$app_name" =~ IntelliJ|PyCharm|WebStorm|GoLand|RubyMine|PhpStorm|CLion|DataGrip|Rider ]]; then for base in ~/Library/Application\ Support/JetBrains ~/Library/Caches/JetBrains ~/Library/Logs/JetBrains; do - [[ -d "$base" ]] && while IFS= read -r -d '' d; do files_to_clean+=("$d"); done < <(command find "$base" -maxdepth 1 -name "${app_name}*" -print0 2>/dev/null) + [[ -d "$base" ]] && while IFS= read -r -d '' d; do files_to_clean+=("$d"); done < <(command find "$base" -maxdepth 1 -name "${app_name}*" -print0 2> /dev/null) done fi @@ -853,11 +853,11 @@ find_app_system_files() { # Safety check: Skip if path ends with a common directory name (indicates empty app_name/bundle_id) case "$p" in - /Library/Application\ Support | /Library/Application\ Support/ | \ - /Library/Caches | /Library/Caches/ | \ - /Library/Logs | /Library/Logs/) - continue - ;; + /Library/Application\ Support | /Library/Application\ Support/ | \ + /Library/Caches | /Library/Caches/ | \ + /Library/Logs | /Library/Logs/) + continue + ;; esac system_files+=("$p") @@ -868,7 +868,7 @@ find_app_system_files() { for base in /Library/LaunchAgents /Library/LaunchDaemons; do [[ -d "$base" ]] && while IFS= read -r -d '' plist; do system_files+=("$plist") - done < <(command find "$base" -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2>/dev/null) + done < <(command find "$base" -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2> /dev/null) done fi @@ -877,11 +877,11 @@ find_app_system_files() { if [[ -n "$bundle_id" && "$bundle_id" != "unknown" && ${#bundle_id} -gt 3 ]]; then [[ -d /Library/PrivilegedHelperTools ]] && while IFS= read -r -d '' helper; do system_files+=("$helper") - done < <(command find /Library/PrivilegedHelperTools -maxdepth 1 \( -name "$bundle_id*" \) -print0 2>/dev/null) + done < <(command find /Library/PrivilegedHelperTools -maxdepth 1 \( -name "$bundle_id*" \) -print0 2> /dev/null) [[ -d /private/var/db/receipts ]] && while IFS= read -r -d '' receipt; do system_files+=("$receipt") - done < <(command find /private/var/db/receipts -maxdepth 1 \( -name "*$bundle_id*" \) -print0 2>/dev/null) + done < <(command find /private/var/db/receipts -maxdepth 1 \( -name "*$bundle_id*" \) -print0 2> /dev/null) fi local receipt_files="" @@ -926,7 +926,7 @@ find_app_receipt_files() { if [[ -d /private/var/db/receipts ]]; then while IFS= read -r -d '' bom; do bom_files+=("$bom") - done < <(find /private/var/db/receipts -maxdepth 1 -name "${bundle_id}*.bom" -print0 2>/dev/null) + done < <(find /private/var/db/receipts -maxdepth 1 -name "${bundle_id}*.bom" -print0 2> /dev/null) fi # Process bom files if any found @@ -938,7 +938,7 @@ find_app_receipt_files() { # lsbom -f: file paths only # -s: suppress output (convert to text) local bom_content - bom_content=$(lsbom -f -s "$bom_file" 2>/dev/null) + bom_content=$(lsbom -f -s "$bom_file" 2> /dev/null) while IFS= read -r file_path; do # Standardize path (remove leading dot) @@ -965,21 +965,21 @@ find_app_receipt_files() { # Whitelisted prefixes (exclude /Users, /usr, /opt) case "$clean_path" in - /Applications/*) is_safe=true ;; - /Library/Application\ Support/*) is_safe=true ;; - /Library/Caches/*) is_safe=true ;; - /Library/Logs/*) is_safe=true ;; - /Library/Preferences/*) is_safe=true ;; - /Library/LaunchAgents/*) is_safe=true ;; - /Library/LaunchDaemons/*) is_safe=true ;; - /Library/PrivilegedHelperTools/*) is_safe=true ;; - /Library/Extensions/*) is_safe=false ;; - *) is_safe=false ;; + /Applications/*) is_safe=true ;; + /Library/Application\ Support/*) is_safe=true ;; + /Library/Caches/*) is_safe=true ;; + /Library/Logs/*) is_safe=true ;; + /Library/Preferences/*) is_safe=true ;; + /Library/LaunchAgents/*) is_safe=true ;; + /Library/LaunchDaemons/*) is_safe=true ;; + /Library/PrivilegedHelperTools/*) is_safe=true ;; + /Library/Extensions/*) is_safe=false ;; + *) is_safe=false ;; esac # Hard blocks case "$clean_path" in - /System/* | /usr/bin/* | /usr/lib/* | /bin/* | /sbin/* | /private/*) is_safe=false ;; + /System/* | /usr/bin/* | /usr/lib/* | /bin/* | /sbin/* | /private/*) is_safe=false ;; esac if [[ "$is_safe" == "true" && -e "$clean_path" ]]; then @@ -988,7 +988,7 @@ find_app_receipt_files() { continue fi - if declare -f should_protect_path >/dev/null 2>&1; then + if declare -f should_protect_path > /dev/null 2>&1; then if should_protect_path "$clean_path"; then continue fi @@ -997,7 +997,7 @@ find_app_receipt_files() { receipt_files+=("$clean_path") fi - done <<<"$bom_content" + done <<< "$bom_content" done fi if [[ ${#receipt_files[@]} -gt 0 ]]; then @@ -1014,34 +1014,34 @@ force_kill_app() { # Get the executable name from bundle if app_path is provided local exec_name="" if [[ -n "$app_path" && -e "$app_path/Contents/Info.plist" ]]; then - exec_name=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2>/dev/null || echo "") + exec_name=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2> /dev/null || echo "") fi # Use executable name for precise matching, fallback to app name local match_pattern="${exec_name:-$app_name}" # Check if process is running using exact match only - if ! pgrep -x "$match_pattern" >/dev/null 2>&1; then + if ! pgrep -x "$match_pattern" > /dev/null 2>&1; then return 0 fi # Try graceful termination first - pkill -x "$match_pattern" 2>/dev/null || true + pkill -x "$match_pattern" 2> /dev/null || true sleep 2 # Check again after graceful kill - if ! pgrep -x "$match_pattern" >/dev/null 2>&1; then + if ! pgrep -x "$match_pattern" > /dev/null 2>&1; then return 0 fi # Force kill if still running - pkill -9 -x "$match_pattern" 2>/dev/null || true + pkill -9 -x "$match_pattern" 2> /dev/null || true sleep 2 # If still running and sudo is available, try with sudo - if pgrep -x "$match_pattern" >/dev/null 2>&1; then - if sudo -n true 2>/dev/null; then - sudo pkill -9 -x "$match_pattern" 2>/dev/null || true + if pgrep -x "$match_pattern" > /dev/null 2>&1; then + if sudo -n true 2> /dev/null; then + sudo pkill -9 -x "$match_pattern" 2> /dev/null || true sleep 2 fi fi @@ -1049,7 +1049,7 @@ force_kill_app() { # Final check with longer timeout for stubborn processes local retries=3 while [[ $retries -gt 0 ]]; do - if ! pgrep -x "$match_pattern" >/dev/null 2>&1; then + if ! pgrep -x "$match_pattern" > /dev/null 2>&1; then return 0 fi sleep 1 @@ -1057,7 +1057,7 @@ force_kill_app() { done # Still running after all attempts - pgrep -x "$match_pattern" >/dev/null 2>&1 && return 1 || return 0 + pgrep -x "$match_pattern" > /dev/null 2>&1 && return 1 || return 0 } # Note: calculate_total_size() is defined in lib/core/file_ops.sh diff --git a/lib/core/common.sh b/lib/core/common.sh index ed3a225..21a427e 100755 --- a/lib/core/common.sh +++ b/lib/core/common.sh @@ -43,9 +43,9 @@ update_via_homebrew() { echo "Updating Homebrew..." fi - brew update >"$temp_update" 2>&1 & + brew update > "$temp_update" 2>&1 & local update_pid=$! - wait $update_pid 2>/dev/null || true # Continue even if brew update fails + wait $update_pid 2> /dev/null || true # Continue even if brew update fails if [[ -t 1 ]]; then stop_inline_spinner @@ -58,9 +58,9 @@ update_via_homebrew() { echo "Upgrading Mole..." fi - brew upgrade mole >"$temp_upgrade" 2>&1 & + brew upgrade mole > "$temp_upgrade" 2>&1 & local upgrade_pid=$! - wait $upgrade_pid 2>/dev/null || true # Continue even if brew upgrade fails + wait $upgrade_pid 2> /dev/null || true # Continue even if brew upgrade fails local upgrade_output upgrade_output=$(cat "$temp_upgrade") @@ -78,7 +78,7 @@ update_via_homebrew() { if echo "$upgrade_output" | grep -q "already installed"; then local installed_version - installed_version=$(brew list --versions mole 2>/dev/null | awk '{print $2}') + installed_version=$(brew list --versions mole 2> /dev/null | awk '{print $2}') echo "" echo -e "${GREEN}${ICON_SUCCESS}${NC} Already on latest version (${installed_version:-$current_version})" echo "" @@ -89,14 +89,14 @@ update_via_homebrew() { else echo "$upgrade_output" | grep -Ev "^(==>|Updating Homebrew|Warning:)" || true local new_version - new_version=$(brew list --versions mole 2>/dev/null | awk '{print $2}') + new_version=$(brew list --versions mole 2> /dev/null | awk '{print $2}') echo "" echo -e "${GREEN}${ICON_SUCCESS}${NC} Updated to latest version (${new_version:-$current_version})" echo "" fi # Clear update cache (suppress errors if cache doesn't exist or is locked) - rm -f "$HOME/.cache/mole/version_check" "$HOME/.cache/mole/update_message" 2>/dev/null || true + rm -f "$HOME/.cache/mole/version_check" "$HOME/.cache/mole/update_message" 2> /dev/null || true } # Remove applications from Dock @@ -133,16 +133,16 @@ remove_apps_from_dock() { fi if [[ -e "$app_path" ]]; then - if full_path=$(cd "$(dirname "$app_path")" 2>/dev/null && pwd); then + if full_path=$(cd "$(dirname "$app_path")" 2> /dev/null && pwd); then full_path="$full_path/$(basename "$app_path")" else continue fi else case "$app_path" in - ~/*) full_path="$HOME/${app_path#~/}" ;; - /*) full_path="$app_path" ;; - *) continue ;; + ~/*) full_path="$HOME/${app_path#~/}" ;; + /*) full_path="$app_path" ;; + *) continue ;; esac fi @@ -154,11 +154,11 @@ remove_apps_from_dock() { local i=0 while true; do local label - label=$(/usr/libexec/PlistBuddy -c "Print :persistent-apps:$i:tile-data:file-label" "$plist" 2>/dev/null || echo "") + label=$(/usr/libexec/PlistBuddy -c "Print :persistent-apps:$i:tile-data:file-label" "$plist" 2> /dev/null || echo "") [[ -z "$label" ]] && break local url - url=$(/usr/libexec/PlistBuddy -c "Print :persistent-apps:$i:tile-data:file-data:_CFURLString" "$plist" 2>/dev/null || echo "") + url=$(/usr/libexec/PlistBuddy -c "Print :persistent-apps:$i:tile-data:file-data:_CFURLString" "$plist" 2> /dev/null || echo "") [[ -z "$url" ]] && { ((i++)) continue @@ -166,7 +166,7 @@ remove_apps_from_dock() { # Match by URL-encoded path to handle spaces in app names if [[ -n "$encoded_path" && "$url" == *"$encoded_path"* ]]; then - if /usr/libexec/PlistBuddy -c "Delete :persistent-apps:$i" "$plist" 2>/dev/null; then + if /usr/libexec/PlistBuddy -c "Delete :persistent-apps:$i" "$plist" 2> /dev/null; then changed=true # After deletion, current index i now points to the next item continue @@ -178,6 +178,6 @@ remove_apps_from_dock() { if [[ "$changed" == "true" ]]; then # Restart Dock to apply changes from the plist - killall Dock 2>/dev/null || true + killall Dock 2> /dev/null || true fi } diff --git a/lib/core/file_ops.sh b/lib/core/file_ops.sh index cc6ca8e..9044dd6 100644 --- a/lib/core/file_ops.sh +++ b/lib/core/file_ops.sh @@ -42,7 +42,7 @@ validate_path_for_deletion() { # Check symlink target if path is a symbolic link if [[ -L "$path" ]]; then local link_target - link_target=$(readlink "$path" 2>/dev/null) || { + link_target=$(readlink "$path" 2> /dev/null) || { log_error "Cannot read symlink: $path" return 1 } @@ -52,16 +52,16 @@ validate_path_for_deletion() { if [[ "$link_target" != /* ]]; then local link_dir link_dir=$(dirname "$path") - resolved_target=$(cd "$link_dir" 2>/dev/null && cd "$(dirname "$link_target")" 2>/dev/null && pwd)/$(basename "$link_target") || resolved_target="" + resolved_target=$(cd "$link_dir" 2> /dev/null && cd "$(dirname "$link_target")" 2> /dev/null && pwd)/$(basename "$link_target") || resolved_target="" fi # Validate resolved target against protected paths if [[ -n "$resolved_target" ]]; then case "$resolved_target" in - /System/* | /usr/bin/* | /usr/lib/* | /bin/* | /sbin/* | /private/etc/*) - log_error "Symlink points to protected system path: $path -> $resolved_target" - return 1 - ;; + /System/* | /usr/bin/* | /usr/lib/* | /bin/* | /sbin/* | /private/etc/*) + log_error "Symlink points to protected system path: $path -> $resolved_target" + return 1 + ;; esac fi fi @@ -88,47 +88,47 @@ validate_path_for_deletion() { # Allow deletion of coresymbolicationd cache (safe system cache that can be rebuilt) case "$path" in - /System/Library/Caches/com.apple.coresymbolicationd/data | /System/Library/Caches/com.apple.coresymbolicationd/data/*) - return 0 - ;; + /System/Library/Caches/com.apple.coresymbolicationd/data | /System/Library/Caches/com.apple.coresymbolicationd/data/*) + return 0 + ;; esac # Allow known safe paths under /private case "$path" in - /private/tmp | /private/tmp/* | \ - /private/var/tmp | /private/var/tmp/* | \ - /private/var/log | /private/var/log/* | \ - /private/var/folders | /private/var/folders/* | \ - /private/var/db/diagnostics | /private/var/db/diagnostics/* | \ - /private/var/db/DiagnosticPipeline | /private/var/db/DiagnosticPipeline/* | \ - /private/var/db/powerlog | /private/var/db/powerlog/* | \ - /private/var/db/reportmemoryexception | /private/var/db/reportmemoryexception/*) - return 0 - ;; + /private/tmp | /private/tmp/* | \ + /private/var/tmp | /private/var/tmp/* | \ + /private/var/log | /private/var/log/* | \ + /private/var/folders | /private/var/folders/* | \ + /private/var/db/diagnostics | /private/var/db/diagnostics/* | \ + /private/var/db/DiagnosticPipeline | /private/var/db/DiagnosticPipeline/* | \ + /private/var/db/powerlog | /private/var/db/powerlog/* | \ + /private/var/db/reportmemoryexception | /private/var/db/reportmemoryexception/*) + return 0 + ;; esac # Check path isn't critical system directory case "$path" in - / | /bin | /bin/* | /sbin | /sbin/* | /usr | /usr/bin | /usr/bin/* | /usr/sbin | /usr/sbin/* | /usr/lib | /usr/lib/* | /System | /System/* | /Library/Extensions) - log_error "Path validation failed: critical system directory: $path" - return 1 - ;; - /private) - log_error "Path validation failed: critical system directory: $path" - return 1 - ;; - /etc | /etc/* | /private/etc | /private/etc/*) - log_error "Path validation failed: /etc contains critical system files: $path" - return 1 - ;; - /var | /var/db | /var/db/* | /private/var | /private/var/db | /private/var/db/*) - log_error "Path validation failed: /var/db contains system databases: $path" - return 1 - ;; + / | /bin | /bin/* | /sbin | /sbin/* | /usr | /usr/bin | /usr/bin/* | /usr/sbin | /usr/sbin/* | /usr/lib | /usr/lib/* | /System | /System/* | /Library/Extensions) + log_error "Path validation failed: critical system directory: $path" + return 1 + ;; + /private) + log_error "Path validation failed: critical system directory: $path" + return 1 + ;; + /etc | /etc/* | /private/etc | /private/etc/*) + log_error "Path validation failed: /etc contains critical system files: $path" + return 1 + ;; + /var | /var/db | /var/db/* | /private/var | /private/var/db | /private/var/db/*) + log_error "Path validation failed: /var/db contains system databases: $path" + return 1 + ;; esac # Check if path is protected (keychains, system settings, etc) - if declare -f should_protect_path >/dev/null 2>&1; then + if declare -f should_protect_path > /dev/null 2>&1; then if should_protect_path "$path"; then if [[ "${MO_DEBUG:-0}" == "1" ]]; then log_warning "Path validation: protected path skipped: $path" @@ -171,16 +171,16 @@ safe_remove() { if [[ -e "$path" ]]; then local size_kb - size_kb=$(get_path_size_kb "$path" 2>/dev/null || echo "0") + size_kb=$(get_path_size_kb "$path" 2> /dev/null || echo "0") if [[ "$size_kb" -gt 0 ]]; then file_size=$(bytes_to_human "$((size_kb * 1024))") fi if [[ -f "$path" || -d "$path" ]] && ! [[ -L "$path" ]]; then local mod_time - mod_time=$(stat -f%m "$path" 2>/dev/null || echo "0") + mod_time=$(stat -f%m "$path" 2> /dev/null || echo "0") local now - now=$(date +%s 2>/dev/null || echo "0") + now=$(date +%s 2> /dev/null || echo "0") if [[ "$mod_time" -gt 0 && "$now" -gt 0 ]]; then file_age=$(((now - mod_time) / 86400)) fi @@ -248,18 +248,18 @@ safe_sudo_remove() { local file_size="" local file_age="" - if sudo test -e "$path" 2>/dev/null; then + if sudo test -e "$path" 2> /dev/null; then local size_kb - size_kb=$(sudo du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0") + size_kb=$(sudo du -sk "$path" 2> /dev/null | awk '{print $1}' || echo "0") if [[ "$size_kb" -gt 0 ]]; then file_size=$(bytes_to_human "$((size_kb * 1024))") fi - if sudo test -f "$path" 2>/dev/null || sudo test -d "$path" 2>/dev/null; then + if sudo test -f "$path" 2> /dev/null || sudo test -d "$path" 2> /dev/null; then local mod_time - mod_time=$(sudo stat -f%m "$path" 2>/dev/null || echo "0") + mod_time=$(sudo stat -f%m "$path" 2> /dev/null || echo "0") local now - now=$(date +%s 2>/dev/null || echo "0") + now=$(date +%s 2> /dev/null || echo "0") if [[ "$mod_time" -gt 0 && "$now" -gt 0 ]]; then file_age=$(((now - mod_time) / 86400)) fi @@ -276,7 +276,7 @@ safe_sudo_remove() { debug_log "Removing (sudo): $path" # Perform the deletion - if sudo rm -rf "$path" 2>/dev/null; then # SAFE: safe_sudo_remove implementation + if sudo rm -rf "$path" 2> /dev/null; then # SAFE: safe_sudo_remove implementation return 0 else log_error "Failed to remove (sudo): $path" @@ -325,7 +325,7 @@ safe_find_delete() { continue fi safe_remove "$match" true || true - done < <(command find "$base_dir" "${find_args[@]}" -print0 2>/dev/null || true) + done < <(command find "$base_dir" "${find_args[@]}" -print0 2> /dev/null || true) return 0 } @@ -338,12 +338,12 @@ safe_sudo_find_delete() { local type_filter="${4:-f}" # Validate base directory (use sudo for permission-restricted dirs) - if ! sudo test -d "$base_dir" 2>/dev/null; then + if ! sudo test -d "$base_dir" 2> /dev/null; then debug_log "Directory does not exist (skipping): $base_dir" return 0 fi - if sudo test -L "$base_dir" 2>/dev/null; then + if sudo test -L "$base_dir" 2> /dev/null; then log_error "Refusing to search symlinked directory: $base_dir" return 1 fi @@ -367,7 +367,7 @@ safe_sudo_find_delete() { continue fi safe_sudo_remove "$match" || true - done < <(sudo find "$base_dir" "${find_args[@]}" -print0 2>/dev/null || true) + done < <(sudo find "$base_dir" "${find_args[@]}" -print0 2> /dev/null || true) return 0 } @@ -387,7 +387,7 @@ get_path_size_kb() { # Use || echo 0 to ensure failure in du (e.g. permission error) doesn't exit script under set -e # Pipefail would normally cause the pipeline to fail if du fails, but || handle catches it. local size - size=$(command du -sk "$path" 2>/dev/null | awk 'NR==1 {print $1; exit}' || true) + size=$(command du -sk "$path" 2> /dev/null | awk 'NR==1 {print $1; exit}' || true) # Ensure size is a valid number (fix for non-numeric du output) if [[ "$size" =~ ^[0-9]+$ ]]; then @@ -408,7 +408,7 @@ calculate_total_size() { size_kb=$(get_path_size_kb "$file") ((total_kb += size_kb)) fi - done <<<"$files" + done <<< "$files" echo "$total_kb" } diff --git a/lib/uninstall/batch.sh b/lib/uninstall/batch.sh index 9a0951e..b54fe08 100755 --- a/lib/uninstall/batch.sh +++ b/lib/uninstall/batch.sh @@ -22,17 +22,17 @@ has_sensitive_data() { # Use Bash native pattern matching (faster than spawning grep) case "$file" in - */.warp* | */.config/* | */themes/* | */settings/* | */User\ Data/* | \ - */.ssh/* | */.gnupg/* | */Documents/* | */Preferences/*.plist | \ - */Desktop/* | */Downloads/* | */Movies/* | */Music/* | */Pictures/* | \ - */.password* | */.token* | */.auth* | */keychain* | \ - */Passwords/* | */Accounts/* | */Cookies/* | \ - */.aws/* | */.docker/config.json | */.kube/* | \ - */credentials/* | */secrets/*) - return 0 # Found sensitive data - ;; + */.warp* | */.config/* | */themes/* | */settings/* | */User\ Data/* | \ + */.ssh/* | */.gnupg/* | */Documents/* | */Preferences/*.plist | \ + */Desktop/* | */Downloads/* | */Movies/* | */Music/* | */Pictures/* | \ + */.password* | */.token* | */.auth* | */keychain* | \ + */Passwords/* | */Accounts/* | */Cookies/* | \ + */.aws/* | */.docker/config.json | */.kube/* | \ + */credentials/* | */secrets/*) + return 0 # Found sensitive data + ;; esac - done <<<"$files" + done <<< "$files" return 1 # Not found } @@ -44,8 +44,8 @@ decode_file_list() { local decoded # macOS uses -D, GNU uses -d. Always return 0 for set -e safety. - if ! decoded=$(printf '%s' "$encoded" | base64 -D 2>/dev/null); then - if ! decoded=$(printf '%s' "$encoded" | base64 -d 2>/dev/null); then + if ! decoded=$(printf '%s' "$encoded" | base64 -D 2> /dev/null); then + if ! decoded=$(printf '%s' "$encoded" | base64 -d 2> /dev/null); then log_error "Failed to decode file list for $app_name" >&2 echo "" return 0 # Return success with empty string @@ -64,7 +64,7 @@ decode_file_list() { echo "" return 0 # Return success with empty string fi - done <<<"$decoded" + done <<< "$decoded" echo "$decoded" return 0 @@ -80,20 +80,20 @@ stop_launch_services() { if [[ -d ~/Library/LaunchAgents ]]; then while IFS= read -r -d '' plist; do - launchctl unload "$plist" 2>/dev/null || true - done < <(find ~/Library/LaunchAgents -maxdepth 1 -name "${bundle_id}*.plist" -print0 2>/dev/null) + launchctl unload "$plist" 2> /dev/null || true + done < <(find ~/Library/LaunchAgents -maxdepth 1 -name "${bundle_id}*.plist" -print0 2> /dev/null) fi if [[ "$has_system_files" == "true" ]]; then if [[ -d /Library/LaunchAgents ]]; then while IFS= read -r -d '' plist; do - sudo launchctl unload "$plist" 2>/dev/null || true - done < <(find /Library/LaunchAgents -maxdepth 1 -name "${bundle_id}*.plist" -print0 2>/dev/null) + sudo launchctl unload "$plist" 2> /dev/null || true + done < <(find /Library/LaunchAgents -maxdepth 1 -name "${bundle_id}*.plist" -print0 2> /dev/null) fi if [[ -d /Library/LaunchDaemons ]]; then while IFS= read -r -d '' plist; do - sudo launchctl unload "$plist" 2>/dev/null || true - done < <(find /Library/LaunchDaemons -maxdepth 1 -name "${bundle_id}*.plist" -print0 2>/dev/null) + sudo launchctl unload "$plist" 2> /dev/null || true + done < <(find /Library/LaunchDaemons -maxdepth 1 -name "${bundle_id}*.plist" -print0 2> /dev/null) fi fi } @@ -115,7 +115,7 @@ remove_login_item() { local escaped_name="${clean_name//\\/\\\\}" escaped_name="${escaped_name//\"/\\\"}" - osascript <<-EOF >/dev/null 2>&1 || true + osascript <<- EOF > /dev/null 2>&1 || true tell application "System Events" try set itemCount to count of login items @@ -149,9 +149,9 @@ remove_file_list() { if [[ -L "$file" ]]; then if [[ "$use_sudo" == "true" ]]; then - sudo rm "$file" 2>/dev/null && ((++count)) || true + sudo rm "$file" 2> /dev/null && ((++count)) || true else - rm "$file" 2>/dev/null && ((++count)) || true + rm "$file" 2> /dev/null && ((++count)) || true fi else if [[ "$use_sudo" == "true" ]]; then @@ -160,7 +160,7 @@ remove_file_list() { safe_remove "$file" true && ((++count)) || true fi fi - done <<<"$file_list" + done <<< "$file_list" echo "$count" } @@ -184,15 +184,15 @@ batch_uninstall_applications() { if [[ -t 1 ]]; then start_inline_spinner "Scanning files..."; fi for selected_app in "${selected_apps[@]}"; do [[ -z "$selected_app" ]] && continue - IFS='|' read -r _ app_path app_name bundle_id _ _ <<<"$selected_app" + IFS='|' read -r _ app_path app_name bundle_id _ _ <<< "$selected_app" # Check running app by bundle executable if available. local exec_name="" if [[ -e "$app_path/Contents/Info.plist" ]]; then - exec_name=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2>/dev/null || echo "") + exec_name=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2> /dev/null || echo "") fi local check_pattern="${exec_name:-$app_name}" - if pgrep -x "$check_pattern" >/dev/null 2>&1; then + if pgrep -x "$check_pattern" > /dev/null 2>&1; then running_apps+=("$app_name") fi @@ -259,7 +259,7 @@ batch_uninstall_applications() { # Warn if user data is detected. local has_user_data=false for detail in "${app_details[@]}"; do - IFS='|' read -r _ _ _ _ _ _ has_sensitive_data <<<"$detail" + IFS='|' read -r _ _ _ _ _ _ has_sensitive_data <<< "$detail" if [[ "$has_sensitive_data" == "true" ]]; then has_user_data=true break @@ -272,7 +272,7 @@ batch_uninstall_applications() { fi for detail in "${app_details[@]}"; do - IFS='|' read -r app_name app_path bundle_id total_kb encoded_files encoded_system_files has_sensitive_data needs_sudo_flag is_brew_cask cask_name <<<"$detail" + IFS='|' read -r app_name app_path bundle_id total_kb encoded_files encoded_system_files has_sensitive_data needs_sudo_flag is_brew_cask cask_name <<< "$detail" local app_size_display=$(bytes_to_human "$((total_kb * 1024))") local brew_tag="" @@ -295,7 +295,7 @@ batch_uninstall_applications() { fi ((file_count++)) fi - done <<<"$related_files" + done <<< "$related_files" # Show system files (limit to 5). local sys_file_count=0 @@ -306,7 +306,7 @@ batch_uninstall_applications() { fi ((sys_file_count++)) fi - done <<<"$system_files" + done <<< "$system_files" local total_hidden=$((file_count > max_files ? file_count - max_files : 0)) ((total_hidden += sys_file_count > max_files ? sys_file_count - max_files : 0)) @@ -332,24 +332,24 @@ batch_uninstall_applications() { IFS= read -r -s -n1 key || key="" drain_pending_input # Clean up any escape sequence remnants case "$key" in - $'\e' | q | Q) - echo "" - echo "" - return 0 - ;; - "" | $'\n' | $'\r' | y | Y) - echo "" # Move to next line - ;; - *) - echo "" - echo "" - return 0 - ;; + $'\e' | q | Q) + echo "" + echo "" + return 0 + ;; + "" | $'\n' | $'\r' | y | Y) + echo "" # Move to next line + ;; + *) + echo "" + echo "" + return 0 + ;; esac # Request sudo if needed. if [[ ${#sudo_apps[@]} -gt 0 ]]; then - if ! sudo -n true 2>/dev/null; then + if ! sudo -n true 2> /dev/null; then if ! request_sudo_access "Admin required for system apps: ${sudo_apps[*]}"; then echo "" log_error "Admin access denied" @@ -359,12 +359,12 @@ batch_uninstall_applications() { # Keep sudo alive during uninstall. parent_pid=$$ (while true; do - if ! kill -0 "$parent_pid" 2>/dev/null; then + if ! kill -0 "$parent_pid" 2> /dev/null; then exit 0 fi sudo -n true sleep 60 - done 2>/dev/null) & + done 2> /dev/null) & sudo_keepalive_pid=$! fi @@ -376,7 +376,7 @@ batch_uninstall_applications() { local current_index=0 for detail in "${app_details[@]}"; do ((current_index++)) - IFS='|' read -r app_name app_path bundle_id total_kb encoded_files encoded_system_files has_sensitive_data needs_sudo is_brew_cask cask_name <<<"$detail" + IFS='|' read -r app_name app_path bundle_id total_kb encoded_files encoded_system_files has_sensitive_data needs_sudo is_brew_cask cask_name <<< "$detail" local related_files=$(decode_file_list "$encoded_files" "$app_name") local system_files=$(decode_file_list "$encoded_system_files" "$app_name") local reason="" @@ -439,24 +439,24 @@ batch_uninstall_applications() { # Remove related files if app removal succeeded. if [[ -z "$reason" ]]; then - remove_file_list "$related_files" "false" >/dev/null + remove_file_list "$related_files" "false" > /dev/null # If brew successfully uninstalled the cask, avoid deleting # system-level files Mole discovered. Brew manages its own # receipts/symlinks and we don't want to fight it. if [[ "$used_brew_successfully" != "true" ]]; then - remove_file_list "$system_files" "true" >/dev/null + remove_file_list "$system_files" "true" > /dev/null fi # Clean up macOS defaults (preference domains). if [[ -n "$bundle_id" && "$bundle_id" != "unknown" ]]; then - if defaults read "$bundle_id" &>/dev/null; then - defaults delete "$bundle_id" 2>/dev/null || true + if defaults read "$bundle_id" &> /dev/null; then + defaults delete "$bundle_id" 2> /dev/null || true fi # ByHost preferences (machine-specific). if [[ -d ~/Library/Preferences/ByHost ]]; then - find ~/Library/Preferences/ByHost -maxdepth 1 -name "${bundle_id}.*.plist" -delete 2>/dev/null || true + find ~/Library/Preferences/ByHost -maxdepth 1 -name "${bundle_id}.*.plist" -delete 2> /dev/null || true fi fi @@ -554,11 +554,11 @@ batch_uninstall_applications() { if [[ $failed_count -eq 1 ]]; then local first_reason=${failed_items[0]#*:} case "$first_reason" in - still*running*) reason_summary="is still running" ;; - remove*failed*) reason_summary="could not be removed" ;; - permission*denied*) reason_summary="permission denied" ;; - owned*by*) reason_summary="$first_reason (try with sudo)" ;; - *) reason_summary="$first_reason" ;; + still*running*) reason_summary="is still running" ;; + remove*failed*) reason_summary="could not be removed" ;; + permission*denied*) reason_summary="permission denied" ;; + owned*by*) reason_summary="$first_reason (try with sudo)" ;; + *) reason_summary="$first_reason" ;; esac fi summary_details+=("Failed: ${RED}${failed_list}${NC} ${reason_summary}") @@ -586,7 +586,7 @@ batch_uninstall_applications() { fi local autoremove_output removed_count - autoremove_output=$(HOMEBREW_NO_ENV_HINTS=1 brew autoremove 2>/dev/null) || true + autoremove_output=$(HOMEBREW_NO_ENV_HINTS=1 brew autoremove 2> /dev/null) || true removed_count=$(printf '%s\n' "$autoremove_output" | grep -c "^Uninstalling" || true) removed_count=${removed_count:-0} @@ -604,7 +604,7 @@ batch_uninstall_applications() { if [[ $success_count -gt 0 ]]; then local -a removed_paths=() for detail in "${app_details[@]}"; do - IFS='|' read -r app_name app_path _ _ _ _ <<<"$detail" + IFS='|' read -r app_name app_path _ _ _ _ <<< "$detail" for success_name in "${success_items[@]}"; do if [[ "$success_name" == "$app_name" ]]; then removed_paths+=("$app_path") @@ -613,21 +613,21 @@ batch_uninstall_applications() { done done if [[ ${#removed_paths[@]} -gt 0 ]]; then - remove_apps_from_dock "${removed_paths[@]}" 2>/dev/null || true + remove_apps_from_dock "${removed_paths[@]}" 2> /dev/null || true fi fi # Clean up sudo keepalive if it was started. if [[ -n "${sudo_keepalive_pid:-}" ]]; then - kill "$sudo_keepalive_pid" 2>/dev/null || true - wait "$sudo_keepalive_pid" 2>/dev/null || true + kill "$sudo_keepalive_pid" 2> /dev/null || true + wait "$sudo_keepalive_pid" 2> /dev/null || true sudo_keepalive_pid="" fi # Invalidate cache if any apps were successfully uninstalled. if [[ $success_count -gt 0 ]]; then local cache_file="$HOME/.cache/mole/app_scan_cache" - rm -f "$cache_file" 2>/dev/null || true + rm -f "$cache_file" 2> /dev/null || true fi ((total_size_cleaned += total_size_freed)) diff --git a/lib/uninstall/brew.sh b/lib/uninstall/brew.sh index 2906e16..91f9627 100644 --- a/lib/uninstall/brew.sh +++ b/lib/uninstall/brew.sh @@ -18,13 +18,13 @@ resolve_path() { [[ -e "$p" ]] || return 1 # macOS 12.3+ and Linux have realpath - if realpath "$p" 2>/dev/null; then + if realpath "$p" 2> /dev/null; then return 0 fi # Fallback: use cd -P to resolve directory, then append basename local dir base - dir=$(cd -P "$(dirname "$p")" 2>/dev/null && pwd) || return 1 + dir=$(cd -P "$(dirname "$p")" 2> /dev/null && pwd) || return 1 base=$(basename "$p") echo "$dir/$base" } @@ -32,7 +32,7 @@ resolve_path() { # Check if Homebrew is installed and accessible # Returns: 0 if brew is available, 1 otherwise is_homebrew_available() { - command -v brew >/dev/null 2>&1 + command -v brew > /dev/null 2>&1 } # Extract cask token from a Caskroom path @@ -44,8 +44,8 @@ _extract_cask_token_from_path() { # Check if path is inside Caskroom case "$path" in - /opt/homebrew/Caskroom/* | /usr/local/Caskroom/*) ;; - *) return 1 ;; + /opt/homebrew/Caskroom/* | /usr/local/Caskroom/*) ;; + *) return 1 ;; esac # Extract token from path: /opt/homebrew/Caskroom///... @@ -87,9 +87,9 @@ _detect_cask_via_caskroom_search() { [[ -d "$room" ]] || continue while IFS= read -r match; do [[ -n "$match" ]] || continue - token=$(_extract_cask_token_from_path "$match" 2>/dev/null) || continue + token=$(_extract_cask_token_from_path "$match" 2> /dev/null) || continue [[ -n "$token" ]] && tokens+=("$token") - done < <(find "$room" -maxdepth 3 -name "$app_bundle_name" 2>/dev/null) + done < <(find "$room" -maxdepth 3 -name "$app_bundle_name" 2> /dev/null) done # Need at least one token @@ -101,7 +101,7 @@ _detect_cask_via_caskroom_search() { # Only succeed if exactly one unique token found and it's installed if ((${#uniq[@]} == 1)) && [[ -n "${uniq[0]}" ]]; then - HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2>/dev/null | grep -qxF "${uniq[0]}" || return 1 + HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2> /dev/null | grep -qxF "${uniq[0]}" || return 1 echo "${uniq[0]}" return 0 fi @@ -115,7 +115,7 @@ _detect_cask_via_symlink_check() { [[ -L "$app_path" ]] || return 1 local target - target=$(readlink "$app_path" 2>/dev/null) || return 1 + target=$(readlink "$app_path" 2> /dev/null) || return 1 _extract_cask_token_from_path "$target" } @@ -127,10 +127,10 @@ _detect_cask_via_brew_list() { app_name_lower=$(echo "${app_bundle_name%.app}" | LC_ALL=C tr '[:upper:]' '[:lower:]') local cask_name - cask_name=$(HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2>/dev/null | grep -Fix "$app_name_lower") || return 1 + cask_name=$(HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2> /dev/null | grep -Fix "$app_name_lower") || return 1 # Verify this cask actually owns this app path - HOMEBREW_NO_ENV_HINTS=1 brew info --cask "$cask_name" 2>/dev/null | grep -qF "$app_path" || return 1 + HOMEBREW_NO_ENV_HINTS=1 brew info --cask "$cask_name" 2> /dev/null | grep -qF "$app_path" || return 1 echo "$cask_name" } @@ -175,7 +175,7 @@ brew_uninstall_cask() { # Ensure we have sudo access if needed, to prevent brew from hanging on password prompt if [[ "${NONINTERACTIVE:-}" != "1" && -t 0 && -t 1 ]]; then - if ! sudo -n true 2>/dev/null; then + if ! sudo -n true 2> /dev/null; then sudo -v fi fi @@ -194,7 +194,7 @@ brew_uninstall_cask() { # Verify removal local cask_gone=true app_gone=true - HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2>/dev/null | grep -qxF "$cask_name" && cask_gone=false + HOMEBREW_NO_ENV_HINTS=1 brew list --cask 2> /dev/null | grep -qxF "$cask_name" && cask_gone=false [[ -n "$app_path" && -e "$app_path" ]] && app_gone=false # Success: uninstall worked and both are gone, or already uninstalled