From cf821cdc4b5a17f3346c0023a4218ffabad57495 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Sun, 12 Oct 2025 20:49:10 +0800 Subject: [PATCH] Code support format detection --- .editorconfig | 25 ++ .github/workflows/tests.yml | 19 +- CONTRIBUTING.md | 54 +++ bin/analyze.sh | 342 +++++++++-------- bin/clean.sh | 220 ++++++----- bin/touchid.sh | 34 +- bin/uninstall.sh | 124 +++--- install.sh | 80 ++-- lib/batch_uninstall.sh | 34 +- lib/common.sh | 744 ++++++++++++++++++------------------ lib/paginated_menu.sh | 24 +- lib/whitelist_manager.sh | 5 +- mole | 85 ++-- scripts/format.sh | 60 +++ scripts/install-hooks.sh | 44 +++ scripts/pre-commit.sh | 67 ++++ tests/analyze.bats | 40 +- tests/clean.bats | 66 ++-- tests/cli.bats | 86 ++--- tests/common.bats | 143 +++---- tests/run.sh | 52 +-- tests/uninstall.bats | 82 ++-- tests/update_remove.bats | 58 +-- tests/whitelist.bats | 130 +++---- tests/z_shellcheck.bats | 16 +- 25 files changed, 1482 insertions(+), 1152 deletions(-) create mode 100644 .editorconfig create mode 100644 CONTRIBUTING.md create mode 100755 scripts/format.sh create mode 100755 scripts/install-hooks.sh create mode 100755 scripts/pre-commit.sh diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f7bf3d4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,25 @@ +# EditorConfig for Mole project +# https://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{sh,bash}] +indent_style = space +indent_size = 4 +# shfmt will use these settings automatically + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c2bde3..a8a620f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: Mole Tests +name: Tests on: push: @@ -13,10 +13,17 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Install bats-core - run: | - brew update - brew install bats-core + - name: Install tools + run: brew install bats-core shfmt shellcheck - - name: Run test suite + - name: Check formatting + run: ./scripts/format.sh --check + + - name: Run shellcheck + run: | + find . -type f \( -name "*.sh" -o -name "mole" \) \ + ! -path "./.git/*" \ + -exec shellcheck -S warning {} + + + - name: Run tests run: tests/run.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d96b7ff --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,54 @@ +# Contributing to Mole + +## Setup + +```bash +# Install tools +brew install shfmt shellcheck bats-core + +# Install git hooks (optional) +./scripts/install-hooks.sh +``` + +## Development + +```bash +# Format code +./scripts/format.sh + +# Run tests +./tests/run.sh + +# Check quality +shellcheck -S warning mole bin/*.sh lib/*.sh +``` + +## Git Hooks + +Pre-commit hook will auto-format your code. Install with: +```bash +./scripts/install-hooks.sh +``` + +Skip if needed: `git commit --no-verify` + +## Code Style + +- Bash 3.2+ compatible +- 4 spaces indent +- Use `set -euo pipefail` +- Quote all variables +- BSD commands not GNU + +Config: `.editorconfig` and `.shellcheckrc` + +## Pull Requests + +1. Fork and create branch +2. Make changes +3. Format: `./scripts/format.sh` +4. Test: `./tests/run.sh` +5. Commit and push +6. Open PR + +CI will check formatting, lint, and run tests. diff --git a/bin/analyze.sh b/bin/analyze.sh index cae26a4..298c6b4 100755 --- a/bin/analyze.sh +++ b/bin/analyze.sh @@ -19,9 +19,9 @@ source "$LIB_DIR/common.sh" # Constants readonly CACHE_DIR="${HOME}/.config/mole/cache" readonly TEMP_PREFIX="/tmp/mole_analyze_$$" -readonly MIN_LARGE_FILE_SIZE="1000000000" # 1GB -readonly MIN_MEDIUM_FILE_SIZE="100000000" # 100MB -readonly MIN_SMALL_FILE_SIZE="10000000" # 10MB +readonly MIN_LARGE_FILE_SIZE="1000000000" # 1GB +readonly MIN_MEDIUM_FILE_SIZE="100000000" # 100MB +readonly MIN_SMALL_FILE_SIZE="10000000" # 10MB # Emoji badges for list displays only readonly BADGE_DIR="🍞" @@ -42,16 +42,16 @@ declare CURRENT_DEPTH=1 # UI State declare CURSOR_POS=0 -declare SORT_MODE="size" # size, name, time -declare VIEW_MODE="overview" # overview, detail, files +declare SORT_MODE="size" # size, name, time +declare VIEW_MODE="overview" # overview, detail, files # Cleanup on exit cleanup() { show_cursor # Cleanup temp files using glob pattern (analyze uses many temp files) - rm -f "$TEMP_PREFIX"* 2>/dev/null || true - if [[ -n "$SCAN_PID" ]] && kill -0 "$SCAN_PID" 2>/dev/null; then - kill "$SCAN_PID" 2>/dev/null || true + rm -f "$TEMP_PREFIX"* 2> /dev/null || true + if [[ -n "$SCAN_PID" ]] && kill -0 "$SCAN_PID" 2> /dev/null; then + kill "$SCAN_PID" 2> /dev/null || true fi } @@ -66,7 +66,7 @@ scan_large_files() { local target_path="$1" local output_file="$2" - if ! command -v mdfind &>/dev/null; then + if ! command -v mdfind &> /dev/null; then return 1 fi @@ -75,10 +75,10 @@ scan_large_files() { while IFS= read -r file; do if [[ -f "$file" ]]; then local size - size=$(stat -f%z "$file" 2>/dev/null || echo "0") + size=$(stat -f%z "$file" 2> /dev/null || echo "0") echo "$size|$file" fi - done < <(mdfind -onlyin "$target_path" "kMDItemFSSize > $MIN_LARGE_FILE_SIZE" 2>/dev/null) | \ + done < <(mdfind -onlyin "$target_path" "kMDItemFSSize > $MIN_LARGE_FILE_SIZE" 2> /dev/null) | sort -t'|' -k1 -rn > "$output_file" } @@ -87,7 +87,7 @@ scan_medium_files() { local target_path="$1" local output_file="$2" - if ! command -v mdfind &>/dev/null; then + if ! command -v mdfind &> /dev/null; then return 1 fi @@ -95,11 +95,11 @@ scan_medium_files() { while IFS= read -r file; do if [[ -f "$file" ]]; then local size - size=$(stat -f%z "$file" 2>/dev/null || echo "0") + size=$(stat -f%z "$file" 2> /dev/null || echo "0") echo "$size|$file" fi done < <(mdfind -onlyin "$target_path" \ - "kMDItemFSSize > $MIN_MEDIUM_FILE_SIZE && kMDItemFSSize < $MIN_LARGE_FILE_SIZE" 2>/dev/null) | \ + "kMDItemFSSize > $MIN_MEDIUM_FILE_SIZE && kMDItemFSSize < $MIN_LARGE_FILE_SIZE" 2> /dev/null) | sort -t'|' -k1 -rn > "$output_file" } @@ -110,18 +110,18 @@ scan_directories() { local depth="${3:-1}" # Check if we can use parallel processing - if command -v xargs &>/dev/null && [[ $depth -eq 1 ]]; then + if command -v xargs &> /dev/null && [[ $depth -eq 1 ]]; then # Fast parallel scan for depth 1 - find "$target_path" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null | \ - xargs -0 -P 4 -I {} du -sk {} 2>/dev/null | \ - sort -rn | \ + find "$target_path" -mindepth 1 -maxdepth 1 -type d -print0 2> /dev/null | + xargs -0 -P 4 -I {} du -sk {} 2> /dev/null | + sort -rn | while IFS=$'\t' read -r size path; do echo "$((size * 1024))|$path" done > "$output_file" else # Standard du scan - du -d "$depth" -k "$target_path" 2>/dev/null | \ - sort -rn | \ + du -d "$depth" -k "$target_path" 2> /dev/null | + sort -rn | while IFS=$'\t' read -r size path; do # Skip if path is the target itself at depth > 0 if [[ "$path" != "$target_path" ]]; then @@ -161,21 +161,21 @@ aggregate_by_directory() { get_cache_file() { local target_path="$1" local path_hash - path_hash=$(echo "$target_path" | md5 2>/dev/null || echo "$target_path" | shasum | cut -d' ' -f1) + path_hash=$(echo "$target_path" | md5 2> /dev/null || echo "$target_path" | shasum | cut -d' ' -f1) echo "$CACHE_DIR/scan_${path_hash}.cache" } # Check if cache is valid (less than 1 hour old) is_cache_valid() { local cache_file="$1" - local max_age="${2:-3600}" # Default 1 hour + local max_age="${2:-3600}" # Default 1 hour if [[ ! -f "$cache_file" ]]; then return 1 fi local cache_age - cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0))) + cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2> /dev/null || echo 0))) if [[ $cache_age -lt $max_age ]]; then return 0 fi @@ -192,7 +192,7 @@ save_to_cache() { local temp_agg="$TEMP_PREFIX.agg" # Create cache directory - mkdir -p "$(dirname "$cache_file")" 2>/dev/null || return 1 + mkdir -p "$(dirname "$cache_file")" 2> /dev/null || return 1 # Bundle all scan results into cache file { @@ -204,7 +204,7 @@ save_to_cache() { [[ -f "$temp_dirs" ]] && cat "$temp_dirs" echo "### AGG ###" [[ -f "$temp_agg" ]] && cat "$temp_agg" - } > "$cache_file" 2>/dev/null + } > "$cache_file" 2> /dev/null } # Load scan results from cache @@ -283,7 +283,7 @@ perform_scan() { ) local msg_idx=0 - while kill -0 "$SCAN_PID" 2>/dev/null; do + while kill -0 "$SCAN_PID" 2> /dev/null; do # Show different messages based on elapsed time local current_msg="" if [[ $elapsed -lt 5 ]]; then @@ -299,12 +299,12 @@ perform_scan() { printf "\r${BLUE}%s${NC} %s" \ "${spinner_chars:$i:1}" "$current_msg" - i=$(( (i + 1) % 10 )) + i=$(((i + 1) % 10)) ((elapsed++)) sleep 0.1 done - wait "$SCAN_PID" 2>/dev/null || true - printf "\r%80s\r" "" # Clear spinner line + wait "$SCAN_PID" 2> /dev/null || true + printf "\r%80s\r" "" # Clear spinner line show_cursor # Aggregate results @@ -508,7 +508,7 @@ display_directories_compact() { # Simple bar (10 chars) local bar_width=10 - local percentage_int=${percentage%.*} # Remove decimal part + local percentage_int=${percentage%.*} # Remove decimal part local filled filled=$((percentage_int * bar_width / 100)) [[ $filled -gt $bar_width ]] && filled=$bar_width @@ -622,8 +622,8 @@ display_cleanup_suggestions_compact() { if [[ "$CURRENT_PATH" == "$HOME/Library/Caches"* ]] || [[ "$CURRENT_PATH" == "$HOME/Library"* ]]; then if [[ -d "$HOME/Library/Caches" ]]; then local cache_size - cache_size=$(du -sk "$HOME/Library/Caches" 2>/dev/null | cut -f1) - if [[ $cache_size -gt 1048576 ]]; then # > 1GB + cache_size=$(du -sk "$HOME/Library/Caches" 2> /dev/null | cut -f1) + if [[ $cache_size -gt 1048576 ]]; then # > 1GB local human human=$(bytes_to_human $((cache_size * 1024))) top_suggestion="Clear app caches ($human)" @@ -637,7 +637,7 @@ display_cleanup_suggestions_compact() { # Check Downloads folder (only if analyzing Downloads) if [[ "$CURRENT_PATH" == "$HOME/Downloads"* ]]; then local old_files - old_files=$(find "$CURRENT_PATH" -type f -mtime +90 2>/dev/null | wc -l | tr -d ' ') + old_files=$(find "$CURRENT_PATH" -type f -mtime +90 2> /dev/null | wc -l | tr -d ' ') if [[ $old_files -gt 0 ]]; then [[ -z "$top_suggestion" ]] && top_suggestion="$old_files files older than 90 days found" [[ -z "$action_command" ]] && action_command="manually review old files" @@ -646,13 +646,13 @@ display_cleanup_suggestions_compact() { fi # Check for large disk images in current path - if command -v mdfind &>/dev/null; then + if command -v mdfind &> /dev/null; then local dmg_count=$(mdfind -onlyin "$CURRENT_PATH" \ - "kMDItemFSSize > 500000000 && kMDItemDisplayName == '*.dmg'" 2>/dev/null | wc -l | tr -d ' ') + "kMDItemFSSize > 500000000 && kMDItemDisplayName == '*.dmg'" 2> /dev/null | wc -l | tr -d ' ') if [[ $dmg_count -gt 0 ]]; then local dmg_size=$(mdfind -onlyin "$CURRENT_PATH" \ - "kMDItemFSSize > 500000000 && kMDItemDisplayName == '*.dmg'" 2>/dev/null | \ - xargs stat -f%z 2>/dev/null | awk '{sum+=$1} END {print sum}') + "kMDItemFSSize > 500000000 && kMDItemDisplayName == '*.dmg'" 2> /dev/null | + xargs stat -f%z 2> /dev/null | awk '{sum+=$1} END {print sum}') local dmg_human dmg_human=$(bytes_to_human "$dmg_size") [[ -z "$top_suggestion" ]] && top_suggestion="$dmg_count DMG files ($dmg_human) can be removed" @@ -665,7 +665,7 @@ display_cleanup_suggestions_compact() { # Check Xcode (only if in developer paths) if [[ "$CURRENT_PATH" == "$HOME/Library/Developer"* ]] && [[ -d "$HOME/Library/Developer/Xcode/DerivedData" ]]; then local xcode_size - xcode_size=$(du -sk "$HOME/Library/Developer/Xcode/DerivedData" 2>/dev/null | cut -f1) + xcode_size=$(du -sk "$HOME/Library/Developer/Xcode/DerivedData" 2> /dev/null | cut -f1) if [[ $xcode_size -gt 10485760 ]]; then local xcode_human xcode_human=$(bytes_to_human $((xcode_size * 1024))) @@ -677,9 +677,9 @@ display_cleanup_suggestions_compact() { fi # Check for duplicates in current path - if command -v mdfind &>/dev/null; then - local dup_count=$(mdfind -onlyin "$CURRENT_PATH" "kMDItemFSSize > 10000000" 2>/dev/null | \ - xargs -I {} stat -f "%z" {} 2>/dev/null | sort | uniq -d | wc -l | tr -d ' ') + if command -v mdfind &> /dev/null; then + local dup_count=$(mdfind -onlyin "$CURRENT_PATH" "kMDItemFSSize > 10000000" 2> /dev/null | + xargs -I {} stat -f "%z" {} 2> /dev/null | sort | uniq -d | wc -l | tr -d ' ') if [[ $dup_count -gt 5 ]]; then [[ -z "$top_suggestion" ]] && top_suggestion="$dup_count potential duplicate files detected" ((suggestions_count++)) @@ -720,8 +720,8 @@ display_cleanup_suggestions() { # Check common cache locations if [[ -d "$HOME/Library/Caches" ]]; then local cache_size - cache_size=$(du -sk "$HOME/Library/Caches" 2>/dev/null | cut -f1) - if [[ $cache_size -gt 1048576 ]]; then # > 1GB + cache_size=$(du -sk "$HOME/Library/Caches" 2> /dev/null | cut -f1) + if [[ $cache_size -gt 1048576 ]]; then # > 1GB local human human=$(bytes_to_human $((cache_size * 1024))) suggestions+=(" Clear application caches: $human") @@ -731,16 +731,16 @@ display_cleanup_suggestions() { # Check Downloads folder if [[ -d "$HOME/Downloads" ]]; then local old_files - old_files=$(find "$HOME/Downloads" -type f -mtime +90 2>/dev/null | wc -l | tr -d ' ') + old_files=$(find "$HOME/Downloads" -type f -mtime +90 2> /dev/null | wc -l | tr -d ' ') if [[ $old_files -gt 0 ]]; then suggestions+=(" Clean old downloads: $old_files files older than 90 days") fi fi # Check for large disk images - if command -v mdfind &>/dev/null; then + if command -v mdfind &> /dev/null; then local dmg_count=$(mdfind -onlyin "$HOME" \ - "kMDItemFSSize > 500000000 && kMDItemDisplayName == '*.dmg'" 2>/dev/null | wc -l | tr -d ' ') + "kMDItemFSSize > 500000000 && kMDItemDisplayName == '*.dmg'" 2> /dev/null | wc -l | tr -d ' ') if [[ $dmg_count -gt 0 ]]; then suggestions+=(" Remove disk images: $dmg_count DMG files >500MB") fi @@ -749,8 +749,8 @@ display_cleanup_suggestions() { # Check Xcode derived data if [[ -d "$HOME/Library/Developer/Xcode/DerivedData" ]]; then local xcode_size - xcode_size=$(du -sk "$HOME/Library/Developer/Xcode/DerivedData" 2>/dev/null | cut -f1) - if [[ $xcode_size -gt 10485760 ]]; then # > 10GB + xcode_size=$(du -sk "$HOME/Library/Developer/Xcode/DerivedData" 2> /dev/null | cut -f1) + if [[ $xcode_size -gt 10485760 ]]; then # > 10GB local human human=$(bytes_to_human $((xcode_size * 1024))) suggestions+=(" Clear Xcode cache: $human") @@ -760,8 +760,8 @@ display_cleanup_suggestions() { # Check iOS device backups if [[ -d "$HOME/Library/Application Support/MobileSync/Backup" ]]; then local backup_size - backup_size=$(du -sk "$HOME/Library/Application Support/MobileSync/Backup" 2>/dev/null | cut -f1) - if [[ $backup_size -gt 5242880 ]]; then # > 5GB + backup_size=$(du -sk "$HOME/Library/Application Support/MobileSync/Backup" 2> /dev/null | cut -f1) + if [[ $backup_size -gt 5242880 ]]; then # > 5GB local human human=$(bytes_to_human $((backup_size * 1024))) suggestions+=(" 📱 Review iOS backups: $human") @@ -769,13 +769,13 @@ display_cleanup_suggestions() { fi # Check for duplicate files (by size, quick heuristic) - if command -v mdfind &>/dev/null; then + if command -v mdfind &> /dev/null; then local temp_dup="$TEMP_PREFIX.dup_check" - mdfind -onlyin "$CURRENT_PATH" "kMDItemFSSize > 10000000" 2>/dev/null | \ - xargs -I {} stat -f "%z" {} 2>/dev/null | \ - sort | uniq -d | wc -l | tr -d ' ' > "$temp_dup" 2>/dev/null || echo "0" > "$temp_dup" + mdfind -onlyin "$CURRENT_PATH" "kMDItemFSSize > 10000000" 2> /dev/null | + xargs -I {} stat -f "%z" {} 2> /dev/null | + sort | uniq -d | wc -l | tr -d ' ' > "$temp_dup" 2> /dev/null || echo "0" > "$temp_dup" local dup_count - dup_count=$(cat "$temp_dup" 2>/dev/null || echo "0") + dup_count=$(cat "$temp_dup" 2> /dev/null || echo "0") if [[ $dup_count -gt 5 ]]; then suggestions+=(" ♻️ Possible duplicates: $dup_count size matches in large files (>10MB)") fi @@ -804,14 +804,14 @@ display_disk_summary() { local total_dirs_count=0 if [[ -f "$temp_large" ]]; then - total_large_count=$(wc -l < "$temp_large" 2>/dev/null | tr -d ' ') + total_large_count=$(wc -l < "$temp_large" 2> /dev/null | tr -d ' ') while IFS='|' read -r size path; do ((total_large_size += size)) done < "$temp_large" fi if [[ -f "$temp_dirs" ]]; then - total_dirs_count=$(wc -l < "$temp_dirs" 2>/dev/null | tr -d ' ') + total_dirs_count=$(wc -l < "$temp_dirs" 2> /dev/null | tr -d ' ') while IFS='|' read -r size path; do ((total_dirs_size += size)) done < "$temp_dirs" @@ -841,20 +841,24 @@ get_file_info() { local type="File" case "$ext" in - dmg|iso|pkg|zip|tar|gz|rar|7z) - badge="$BADGE_BUNDLE" ; type="Bundle" + dmg | iso | pkg | zip | tar | gz | rar | 7z) + badge="$BADGE_BUNDLE" + type="Bundle" ;; - mov|mp4|avi|mkv|webm|jpg|jpeg|png|gif|heic) - badge="$BADGE_MEDIA" ; type="Media" + mov | mp4 | avi | mkv | webm | jpg | jpeg | png | gif | heic) + badge="$BADGE_MEDIA" + type="Media" ;; - pdf|key|ppt|pptx) + pdf | key | ppt | pptx) type="Document" ;; log) - badge="$BADGE_LOG" ; type="Log" + badge="$BADGE_LOG" + type="Log" ;; app) - badge="$BADGE_APP" ; type="App" + badge="$BADGE_APP" + type="App" ;; esac @@ -870,7 +874,7 @@ get_file_age() { fi local mtime - mtime=$(stat -f%m "$path" 2>/dev/null || echo "0") + mtime=$(stat -f%m "$path" 2> /dev/null || echo "0") local now now=$(date +%s) local diff @@ -936,8 +940,8 @@ display_large_files_table() { # Color based on file type local color="" case "$ext" in - dmg|iso|pkg) color="${RED}" ;; - mov|mp4|avi|mkv|webm|zip|tar|gz|rar|7z) color="${YELLOW}" ;; + dmg | iso | pkg) color="${RED}" ;; + mov | mp4 | avi | mkv | webm | zip | tar | gz | rar | 7z) color="${YELLOW}" ;; log) color="${GRAY}" ;; *) color="${NC}" ;; esac @@ -1104,7 +1108,7 @@ display_recent_large_files() { log_header "Recent Large Files (Last 30 Days)" echo "" - if ! command -v mdfind &>/dev/null; then + if ! command -v mdfind &> /dev/null; then echo " ${YELLOW}Note: mdfind not available${NC}" echo "" return @@ -1114,13 +1118,13 @@ display_recent_large_files() { # Find files created in last 30 days, larger than 100MB mdfind -onlyin "$CURRENT_PATH" \ - "kMDItemFSSize > 100000000 && kMDItemContentCreationDate >= \$time.today(-30)" 2>/dev/null | \ + "kMDItemFSSize > 100000000 && kMDItemContentCreationDate >= \$time.today(-30)" 2> /dev/null | while IFS= read -r file; do if [[ -f "$file" ]]; then local size - size=$(stat -f%z "$file" 2>/dev/null || echo "0") + size=$(stat -f%z "$file" 2> /dev/null || echo "0") local mtime - mtime=$(stat -f%m "$file" 2>/dev/null || echo "0") + mtime=$(stat -f%m "$file" 2> /dev/null || echo "0") echo "$size|$mtime|$file" fi done | sort -t'|' -k1 -rn | head -10 > "$temp_recent" @@ -1140,7 +1144,7 @@ display_recent_large_files() { local dirname dirname=$(dirname "$path" | sed "s|^$HOME|~|") local days_ago - days_ago=$(( ($(date +%s) - mtime) / 86400 )) + days_ago=$((($(date +%s) - mtime) / 86400)) local info info=$(get_file_info "$path") @@ -1162,10 +1166,10 @@ get_subdirectories() { local target="$1" local temp_file="$2" - find "$target" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | \ + find "$target" -mindepth 1 -maxdepth 1 -type d 2> /dev/null | while IFS= read -r dir; do local size - size=$(du -sk "$dir" 2>/dev/null | cut -f1) + size=$(du -sk "$dir" 2> /dev/null | cut -f1) echo "$((size * 1024))|$dir" done | sort -t'|' -k1 -rn > "$temp_file" } @@ -1298,7 +1302,7 @@ display_file_types() { log_header "File Types Analysis" echo "" - if ! command -v mdfind &>/dev/null; then + if ! command -v mdfind &> /dev/null; then echo " ${YELLOW}Note: mdfind not available, limited analysis${NC}" return fi @@ -1336,7 +1340,7 @@ display_file_types() { esac local files - files=$(mdfind -onlyin "$CURRENT_PATH" "$query" 2>/dev/null) + files=$(mdfind -onlyin "$CURRENT_PATH" "$query" 2> /dev/null) local count count=$(echo "$files" | grep -c . || echo "0") local total_size=0 @@ -1345,7 +1349,7 @@ display_file_types() { while IFS= read -r file; do if [[ -f "$file" ]]; then local fsize - fsize=$(stat -f%z "$file" 2>/dev/null || echo "0") + fsize=$(stat -f%z "$file" 2> /dev/null || echo "0") ((total_size += fsize)) fi done <<< "$files" @@ -1364,7 +1368,7 @@ display_file_types() { read_single_key() { local key="" # Read single character without waiting for Enter - if read -rsn1 key 2>/dev/null; then + if read -rsn1 key 2> /dev/null; then echo "$key" else echo "q" @@ -1396,13 +1400,13 @@ scan_directory_contents_fast() { fi # Ultra-fast file scanning - batch stat for maximum speed - find "$dir_path" -mindepth 1 -maxdepth 1 -type f -print0 2>/dev/null | \ - xargs -0 -n 20 -P "$num_jobs" stat -f "%z|file|%N" 2>/dev/null > "$temp_files" & + find "$dir_path" -mindepth 1 -maxdepth 1 -type f -print0 2> /dev/null | + xargs -0 -n 20 -P "$num_jobs" stat -f "%z|file|%N" 2> /dev/null > "$temp_files" & local file_pid=$! # Smart directory scanning with aggressive optimization # Strategy: Fast estimation first, accurate on-demand - find "$dir_path" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null | \ + find "$dir_path" -mindepth 1 -maxdepth 1 -type d -print0 2> /dev/null | xargs -0 -n 1 -P "$num_jobs" sh -c ' dir="$1" size="" @@ -1436,7 +1440,7 @@ scan_directory_contents_fast() { [[ -z "$size" ]] || [[ "$size" -eq 0 ]] && size=1 fi echo "$((size * 1024))|dir|$dir" - ' _ > "$temp_dirs" 2>/dev/null & + ' _ > "$temp_dirs" 2> /dev/null & local dir_pid=$! # Show progress while waiting @@ -1448,22 +1452,22 @@ scan_directory_contents_fast() { local spinner_chars spinner_chars="$(mo_spinner_chars)" local chars_len=${#spinner_chars} - for ((idx=0; idx/dev/null || kill -0 "$file_pid" 2>/dev/null ); do + while (kill -0 "$dir_pid" 2> /dev/null || kill -0 "$file_pid" 2> /dev/null); do printf "\r ${BLUE}Scanning${NC} ${spinner[$((i % spin_len))]} (%ds)" "$elapsed" >&2 ((i++)) - sleep 0.1 # Faster animation (100ms per frame) + sleep 0.1 # Faster animation (100ms per frame) ((tick++)) # Update elapsed seconds every 10 ticks (1 second) @@ -1473,10 +1477,10 @@ scan_directory_contents_fast() { # Force kill if taking too long (30 seconds for fast response) if [[ $elapsed -ge $max_wait ]]; then - kill -9 "$dir_pid" 2>/dev/null || true - kill -9 "$file_pid" 2>/dev/null || true - wait "$dir_pid" 2>/dev/null || true - wait "$file_pid" 2>/dev/null || true + kill -9 "$dir_pid" 2> /dev/null || true + kill -9 "$file_pid" 2> /dev/null || true + wait "$dir_pid" 2> /dev/null || true + wait "$file_pid" 2> /dev/null || true printf "\r ${YELLOW}Large directory - showing estimated sizes${NC}\n" >&2 sleep 0.3 break @@ -1488,8 +1492,8 @@ scan_directory_contents_fast() { fi # Wait for completion (non-blocking if already killed) - wait "$file_pid" 2>/dev/null || true - wait "$dir_pid" 2>/dev/null || true + wait "$file_pid" 2> /dev/null || true + wait "$dir_pid" 2> /dev/null || true # Small delay only if scan was very fast (let user see the spinner briefly) if [[ "$show_progress" == "true" ]] && [[ ${elapsed:-0} -lt 1 ]]; then @@ -1498,19 +1502,19 @@ scan_directory_contents_fast() { # Combine and sort - only keep top items # Ensure we handle empty files gracefully - > "$output_file" + true > "$output_file" if [[ -f "$temp_dirs" ]] || [[ -f "$temp_files" ]]; then - cat "$temp_dirs" "$temp_files" 2>/dev/null | sort -t'|' -k1 -rn | head -"$max_items" > "$output_file" || true + cat "$temp_dirs" "$temp_files" 2> /dev/null | sort -t'|' -k1 -rn | head -"$max_items" > "$output_file" || true fi # Cleanup - rm -f "$temp_dirs" "$temp_files" 2>/dev/null + rm -f "$temp_dirs" "$temp_files" 2> /dev/null } # Calculate directory sizes and update (now only used for deep refresh) calculate_dir_sizes() { local items_file="$1" - local max_items="${2:-15}" # Only recalculate first 15 by default + local max_items="${2:-15}" # Only recalculate first 15 by default local temp_file="${items_file}.calc" # Since we now scan with actual sizes, this function is mainly for refresh @@ -1519,9 +1523,9 @@ calculate_dir_sizes() { # Only update if source file still exists (might have been deleted if user quit) if [[ -f "$items_file" ]]; then - mv "$temp_file" "$items_file" 2>/dev/null || true + mv "$temp_file" "$items_file" 2> /dev/null || true else - rm -f "$temp_file" 2>/dev/null || true + rm -f "$temp_file" 2> /dev/null || true fi } @@ -1531,7 +1535,7 @@ combine_initial_scan_results() { local temp_large="$TEMP_PREFIX.large" local temp_dirs="$TEMP_PREFIX.dirs" - > "$output_file" + true > "$output_file" # Add directories if [[ -f "$temp_dirs" ]]; then @@ -1572,7 +1576,7 @@ show_volumes_overview() { # External volumes (if any) if [[ -d "/Volumes" ]]; then local vol_priority=500 - find /Volumes -mindepth 1 -maxdepth 1 -type d 2>/dev/null | while IFS= read -r vol; do + find /Volumes -mindepth 1 -maxdepth 1 -type d 2> /dev/null | while IFS= read -r vol; do local vol_name vol_name=$(basename "$vol") echo "$((vol_priority))|$vol|Volume: $vol_name" @@ -1582,17 +1586,17 @@ show_volumes_overview() { } | sort -t'|' -k1 -rn > "$temp_volumes" # Setup alternate screen and hide cursor (keep hidden throughout) - tput smcup 2>/dev/null || true - printf "\033[?25l" >&2 # Hide cursor + tput smcup 2> /dev/null || true + printf "\033[?25l" >&2 # Hide cursor cleanup_volumes() { - printf "\033[?25h" >&2 # Show cursor - tput rmcup 2>/dev/null || true + printf "\033[?25h" >&2 # Show cursor + tput rmcup 2> /dev/null || true } trap cleanup_volumes EXIT INT TERM # Force cursor hidden at the start - stty -echo 2>/dev/null || true + stty -echo 2> /dev/null || true local cursor=0 local total_items @@ -1603,10 +1607,10 @@ show_volumes_overview() { printf "\033[?25l" >&2 # Drain burst input (trackpad scroll -> many arrows) - type drain_pending_input >/dev/null 2>&1 && drain_pending_input + type drain_pending_input > /dev/null 2>&1 && drain_pending_input # Build output buffer to reduce flicker local output="" - output+="\033[?25l" # Hide cursor + output+="\033[?25l" # Hide cursor output+="\033[H\033[J" output+=$'\n' output+="\033[0;35mSelect a location to explore\033[0m"$'\n' @@ -1633,7 +1637,7 @@ show_volumes_overview() { # Read key (suppress any escape sequences that might leak) local key - key=$(read_key 2>/dev/null || echo "OTHER") + key=$(read_key 2> /dev/null || echo "OTHER") case "$key" in "UP") @@ -1642,7 +1646,7 @@ show_volumes_overview() { "DOWN") ((cursor < total_items - 1)) && ((cursor++)) ;; - "ENTER"|"RIGHT") + "ENTER" | "RIGHT") # Get selected path and enter it local selected_path="" idx=0 @@ -1679,7 +1683,7 @@ show_volumes_overview() { # In volumes view, LEFT does nothing (already at top level) # User must press q/ESC to quit ;; - "QUIT"|"q") + "QUIT" | "q") # Quit the volumes view break ;; @@ -1693,13 +1697,13 @@ show_volumes_overview() { # Interactive drill-down mode interactive_drill_down() { local start_path="$1" - local initial_items="${2:-}" # Pre-scanned items for first level + local initial_items="${2:-}" # Pre-scanned items for first level local current_path="$start_path" local path_stack=() local cursor=0 - local scroll_offset=0 # New: for scrolling + local scroll_offset=0 # New: for scrolling local need_scan=true - local wait_for_calc=false # Don't wait on first load, let user press 'r' + local wait_for_calc=false # Don't wait on first load, let user press 'r' local temp_items="$TEMP_PREFIX.items" local status_message="" @@ -1711,33 +1715,33 @@ interactive_drill_down() { # Directory cache: store scan results for each visited directory # Use temp files because bash 3.2 doesn't have associative arrays local cache_dir="$TEMP_PREFIX.cache.$$" - mkdir -p "$cache_dir" 2>/dev/null || true + mkdir -p "$cache_dir" 2> /dev/null || true # Note: We're already in alternate screen from show_volumes_overview # Just hide cursor, don't re-enter alternate screen - printf "\033[?25l" # Hide cursor + printf "\033[?25l" # Hide cursor # Save terminal settings and disable echo local old_tty_settings="" if [[ -t 0 ]]; then - old_tty_settings=$(stty -g 2>/dev/null || echo "") - stty -echo 2>/dev/null || true + old_tty_settings=$(stty -g 2> /dev/null || echo "") + stty -echo 2> /dev/null || true fi # Cleanup on exit (but don't exit alternate screen - may return to menu) cleanup_drill_down() { # Restore terminal settings if [[ -n "${old_tty_settings:-}" ]]; then - stty "$old_tty_settings" 2>/dev/null || true + stty "$old_tty_settings" 2> /dev/null || true fi - printf "\033[?25h" # Show cursor + printf "\033[?25h" # Show cursor # Don't call tput rmcup - we may be returning to volumes menu - [[ -d "${cache_dir:-}" ]] && rm -rf "$cache_dir" 2>/dev/null || true # Clean up cache + [[ -d "${cache_dir:-}" ]] && rm -rf "$cache_dir" 2> /dev/null || true # Clean up cache } trap cleanup_drill_down EXIT INT TERM # Drain any input that accumulated before entering interactive mode - type drain_pending_input >/dev/null 2>&1 && drain_pending_input + type drain_pending_input > /dev/null 2>&1 && drain_pending_input while true; do # Ensure cursor is always hidden during navigation @@ -1747,7 +1751,7 @@ interactive_drill_down() { if [[ "$need_scan" == "true" ]]; then # Generate cache key (use md5 hash of path) local cache_key - cache_key=$(echo "$current_path" | md5 2>/dev/null || echo "$current_path" | shasum | cut -d' ' -f1) + cache_key=$(echo "$current_path" | md5 2> /dev/null || echo "$current_path" | shasum | cut -d' ' -f1) local cache_file="$cache_dir/$cache_key" # Check if we have cached results for this directory @@ -1760,12 +1764,12 @@ interactive_drill_down() { # Use || true to prevent exit on scan failure scan_directory_contents_fast "$current_path" "$temp_items" 50 true || { # Scan failed - create empty result file - > "$temp_items" + true > "$temp_items" } # Save to cache for next time (only if not empty) if [[ -s "$temp_items" ]]; then - cp "$temp_items" "$cache_file" 2>/dev/null || true + cp "$temp_items" "$cache_file" 2> /dev/null || true fi fi @@ -1787,7 +1791,7 @@ interactive_drill_down() { scroll_offset=0 # Drain any input accumulated during scanning - type drain_pending_input >/dev/null 2>&1 && drain_pending_input + type drain_pending_input > /dev/null 2>&1 && drain_pending_input # Check if empty or scan failed if [[ $total_items -eq 0 ]]; then @@ -1800,7 +1804,7 @@ interactive_drill_down() { echo " ${GRAY}Path: $current_path${NC}" >&2 echo "" >&2 echo " ${GRAY}Press any key to go back...${NC}" >&2 - read_key >/dev/null 2>&1 + read_key > /dev/null 2>&1 else # Directory exists but scan returned nothing (timeout or empty) printf "\033[H\033[J" >&2 @@ -1811,7 +1815,7 @@ interactive_drill_down() { echo " ${GRAY}Press ${NC}${GREEN}R${NC}${GRAY} to retry, any other key to go back${NC}" >&2 local retry_key - retry_key=$(read_key 2>/dev/null || echo "OTHER") + retry_key=$(read_key 2> /dev/null || echo "OTHER") if [[ "$retry_key" == "RETRY" ]]; then # Retry scan @@ -1842,13 +1846,13 @@ interactive_drill_down() { # Build output buffer once for smooth rendering local output="" - output+="\033[?25l" # Hide cursor - output+="\033[H\033[J" # Clear screen + output+="\033[?25l" # Hide cursor + output+="\033[H\033[J" # Clear screen output+=$'\n' output+="\033[0;35mDisk space explorer > $(echo "$current_path" | sed "s|^$HOME|~|")\033[0m"$'\n' output+=$'\n' - local max_show=15 # Show 15 items per page + local max_show=15 # Show 15 items per page local page_start=$scroll_offset local page_end page_end=$((scroll_offset + max_show)) @@ -1886,8 +1890,10 @@ interactive_drill_down() { local badge="$BADGE_FILE" color="${NC}" if [[ "$type" == "dir" ]]; then badge="$BADGE_DIR" color="${BLUE}" - if [[ $size -gt 10737418240 ]]; then color="${RED}" - elif [[ $size -gt 1073741824 ]]; then color="${YELLOW}" + if [[ $size -gt 10737418240 ]]; then + color="${RED}" + elif [[ $size -gt 1073741824 ]]; then + color="${YELLOW}" fi else local ext="${name##*.}" @@ -1895,10 +1901,10 @@ interactive_drill_down() { info=$(get_file_info "$path") badge="${info%|*}" case "$ext" in - dmg|iso|pkg|zip|tar|gz|rar|7z) + dmg | iso | pkg | zip | tar | gz | rar | 7z) color="${YELLOW}" ;; - mov|mp4|avi|mkv|webm|jpg|jpeg|png|gif|heic) + mov | mp4 | avi | mkv | webm | jpg | jpeg | png | gif | heic) color="${YELLOW}" ;; log) @@ -1945,7 +1951,7 @@ interactive_drill_down() { # Read key directly without draining (to preserve all user input) local key - key=$(read_key 2>/dev/null || echo "OTHER") + key=$(read_key 2> /dev/null || echo "OTHER") # Debug: uncomment to see what keys are being received # printf "\rDEBUG: Received key=[%s] " "$key" >&2 @@ -1974,7 +1980,7 @@ interactive_drill_down() { fi fi ;; - "ENTER"|"RIGHT") + "ENTER" | "RIGHT") # Enter selected item - directory or file if [[ $cursor -lt ${#items[@]} ]]; then local selected="${items[$cursor]}" @@ -1998,7 +2004,7 @@ interactive_drill_down() { # For text-like files, use less or fallback to open case "$file_ext" in - txt|log|md|json|xml|yaml|yml|conf|cfg|ini|sh|bash|zsh|py|js|ts|go|rs|c|cpp|h|java|rb|php|html|css|sql) + txt | log | md | json | xml | yaml | yml | conf | cfg | ini | sh | bash | zsh | py | js | ts | go | rs | c | cpp | h | java | rb | php | html | css | sql) # Clear screen and show loading message printf "\033[H\033[J" echo "" @@ -2006,21 +2012,21 @@ interactive_drill_down() { echo "" # Try less first (best for text viewing) - if command -v less &>/dev/null; then + if command -v less &> /dev/null; then # Exit alternate screen only for less - printf "\033[?25h" # Show cursor - tput rmcup 2>/dev/null || true + printf "\033[?25h" # Show cursor + tput rmcup 2> /dev/null || true - less -F "$selected_path" 2>/dev/null && open_success=true + less -F "$selected_path" 2> /dev/null && open_success=true # Return to alternate screen - tput smcup 2>/dev/null || true - printf "\033[?25l" # Hide cursor + tput smcup 2> /dev/null || true + printf "\033[?25l" # Hide cursor else # Fallback to system open if less is not available echo " ${GRAY}Launching default application...${NC}" - if command -v open &>/dev/null; then - open "$selected_path" 2>/dev/null && open_success=true + if command -v open &> /dev/null; then + open "$selected_path" 2> /dev/null && open_success=true if [[ "$open_success" == "true" ]]; then echo "" echo " ${GREEN}${ICON_SUCCESS}${NC} File opened in external app" @@ -2038,8 +2044,8 @@ interactive_drill_down() { echo "" echo " ${GRAY}Launching default application...${NC}" - if command -v open &>/dev/null; then - open "$selected_path" 2>/dev/null && open_success=true + if command -v open &> /dev/null; then + open "$selected_path" 2> /dev/null && open_success=true # Show brief success message if [[ "$open_success" == "true" ]]; then @@ -2059,7 +2065,7 @@ interactive_drill_down() { echo "" echo " ${GRAY}File: $selected_path${NC}" echo " ${GRAY}Press any key to return...${NC}" - read -n 1 -s 2>/dev/null + read -n 1 -s 2> /dev/null fi fi fi @@ -2081,16 +2087,16 @@ interactive_drill_down() { # Already at start path - return to volumes menu # Don't show cursor or exit screen - menu will handle it if [[ -n "${old_tty_settings:-}" ]]; then - stty "$old_tty_settings" 2>/dev/null || true + stty "$old_tty_settings" 2> /dev/null || true fi - [[ -d "${cache_dir:-}" ]] && rm -rf "$cache_dir" 2>/dev/null || true + [[ -d "${cache_dir:-}" ]] && rm -rf "$cache_dir" 2> /dev/null || true trap - EXIT INT TERM - return 1 # Return to menu + return 1 # Return to menu fi ;; "OPEN") - if command -v open >/dev/null 2>&1; then - if open "$current_path" >/dev/null 2>&1; then + if command -v open > /dev/null 2>&1; then + if open "$current_path" > /dev/null 2>&1; then status_message="${GREEN}${ICON_SUCCESS}${NC} Finder opened: ${GRAY}$current_path${NC}" else status_message="${YELLOW}Warning:${NC} Could not open ${GRAY}$current_path${NC}" @@ -2155,7 +2161,7 @@ interactive_drill_down() { # Read confirmation local confirm - confirm=$(read_key 2>/dev/null || echo "QUIT") + confirm=$(read_key 2> /dev/null || echo "QUIT") if [[ "$confirm" == "ENTER" ]]; then # Request sudo if needed before deletion @@ -2180,11 +2186,11 @@ interactive_drill_down() { # Try to delete with sudo if needed local delete_success=false if [[ "$needs_sudo" == "true" ]]; then - if sudo rm -rf "$selected_path" 2>/dev/null; then + if sudo rm -rf "$selected_path" 2> /dev/null; then delete_success=true fi else - if rm -rf "$selected_path" 2>/dev/null; then + if rm -rf "$selected_path" 2> /dev/null; then delete_success=true fi fi @@ -2195,9 +2201,9 @@ interactive_drill_down() { # Clear cache to force rescan local cache_key - cache_key=$(echo "$current_path" | md5 2>/dev/null || echo "$current_path" | shasum | cut -d' ' -f1) + cache_key=$(echo "$current_path" | md5 2> /dev/null || echo "$current_path" | shasum | cut -d' ' -f1) local cache_file="$cache_dir/$cache_key" - rm -f "$cache_file" 2>/dev/null || true + rm -f "$cache_file" 2> /dev/null || true # Refresh the view need_scan=true @@ -2215,16 +2221,16 @@ interactive_drill_down() { echo " ${ICON_LIST} System protection (SIP) prevents deletion" echo "" echo " ${GRAY}Press any key to continue...${NC}" - read_key >/dev/null 2>&1 + read_key > /dev/null 2>&1 fi fi fi ;; - "QUIT"|"q") + "QUIT" | "q") # Quit the explorer cleanup_drill_down trap - EXIT INT TERM - return 0 # Return true to indicate normal exit + return 0 # Return true to indicate normal exit ;; *) # Unknown key - ignore it @@ -2233,7 +2239,7 @@ interactive_drill_down() { done # Cleanup is handled by trap - return 0 # Normal exit if loop ends + return 0 # Normal exit if loop ends } # Main interactive loop @@ -2242,7 +2248,7 @@ interactive_mode() { VIEW_MODE="overview" while true; do - type drain_pending_input >/dev/null 2>&1 && drain_pending_input + type drain_pending_input > /dev/null 2>&1 && drain_pending_input display_interactive_menu local key @@ -2291,10 +2297,10 @@ interactive_mode() { VIEW_MODE="overview" fi ;; - "f"|"F") + "f" | "F") VIEW_MODE="files" ;; - "t"|"T") + "t" | "T") VIEW_MODE="types" ;; "ENTER") @@ -2402,7 +2408,7 @@ main() { # Parse arguments - only support --help while [[ $# -gt 0 ]]; do case "$1" in - -h|--help) + -h | --help) echo "Usage: mole analyze" echo "" echo "Interactive disk space explorer - Navigate folders sorted by size" @@ -2446,7 +2452,7 @@ main() { CURRENT_PATH="$target_path" # Create cache directory - mkdir -p "$CACHE_DIR" 2>/dev/null || true + mkdir -p "$CACHE_DIR" 2> /dev/null || true # Start with volumes overview to let user choose location show_volumes_overview diff --git a/bin/clean.sh b/bin/clean.sh index bbaa2ad..aa19711 100755 --- a/bin/clean.sh +++ b/bin/clean.sh @@ -18,11 +18,11 @@ DRY_RUN=false IS_M_SERIES=$([ "$(uname -m)" = "arm64" ] && echo "true" || echo "false") # Constants -readonly MAX_PARALLEL_JOBS=15 # Maximum parallel background jobs -readonly TEMP_FILE_AGE_DAYS=7 # Age threshold for temp file cleanup -readonly ORPHAN_AGE_DAYS=60 # Age threshold for orphaned data -readonly SIZE_1GB_KB=1048576 # 1GB in kilobytes -readonly SIZE_1MB_KB=1024 # 1MB in kilobytes +readonly MAX_PARALLEL_JOBS=15 # Maximum parallel background jobs +readonly TEMP_FILE_AGE_DAYS=7 # Age threshold for temp file cleanup +readonly ORPHAN_AGE_DAYS=60 # Age threshold for orphaned data +readonly SIZE_1GB_KB=1048576 # 1GB in kilobytes +readonly SIZE_1MB_KB=1024 # 1MB in kilobytes # Default whitelist patterns (preselected, user can disable) declare -a DEFAULT_WHITELIST_PATTERNS=( "$HOME/Library/Caches/ms-playwright*" @@ -52,7 +52,7 @@ if [[ -f "$HOME/.config/mole/whitelist" ]]; then # Prevent absolute path to critical system directories case "$line" in - /System/*|/bin/*|/sbin/*|/usr/bin/*|/usr/sbin/*) + /System/* | /bin/* | /sbin/* | /usr/bin/* | /usr/sbin/*) WHITELIST_WARNINGS+=("System path: $line") continue ;; @@ -104,14 +104,14 @@ cleanup() { # Stop all spinners and clear the line if [[ -n "$SPINNER_PID" ]]; then - kill "$SPINNER_PID" 2>/dev/null || true - wait "$SPINNER_PID" 2>/dev/null || true + kill "$SPINNER_PID" 2> /dev/null || true + wait "$SPINNER_PID" 2> /dev/null || true SPINNER_PID="" fi if [[ -n "$INLINE_SPINNER_PID" ]]; then - kill "$INLINE_SPINNER_PID" 2>/dev/null || true - wait "$INLINE_SPINNER_PID" 2>/dev/null || true + kill "$INLINE_SPINNER_PID" 2> /dev/null || true + wait "$INLINE_SPINNER_PID" 2> /dev/null || true INLINE_SPINNER_PID="" fi @@ -122,8 +122,8 @@ cleanup() { # Stop sudo keepalive 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 @@ -176,8 +176,8 @@ stop_spinner() { fi if [[ -n "$SPINNER_PID" ]]; then - kill "$SPINNER_PID" 2>/dev/null - wait "$SPINNER_PID" 2>/dev/null + kill "$SPINNER_PID" 2> /dev/null + wait "$SPINNER_PID" 2> /dev/null SPINNER_PID="" printf "\r ${GREEN}${ICON_SUCCESS}${NC} %s\n" "$result_message" else @@ -229,7 +229,7 @@ safe_clean() { if [[ ${#WHITELIST_PATTERNS[@]} -gt 0 ]]; then for w in "${WHITELIST_PATTERNS[@]}"; do # Match both exact path and glob pattern - if [[ "$path" == "$w" ]] || [[ "$path" == $w ]]; then + if [[ "$path" == "$w" ]] || [[ "$path" == "$w" ]]; then skip=true ((skipped_count++)) break @@ -239,7 +239,7 @@ safe_clean() { [[ "$skip" == "true" ]] && continue [[ -e "$path" ]] && existing_paths+=("$path") done - + # Update global whitelist skip counter if [[ $skipped_count -gt 0 ]]; then ((whitelist_skipped_count += skipped_count)) @@ -253,31 +253,34 @@ safe_clean() { # Show progress indicator for potentially slow operations if [[ ${#existing_paths[@]} -gt 3 ]]; then if [[ -t 1 ]]; then MOLE_SPINNER_PREFIX=" " start_inline_spinner "Checking items with whitelist safety..."; fi - local temp_dir=$(create_temp_dir) + local temp_dir + temp_dir=$(create_temp_dir) # Parallel processing (bash 3.2 compatible) local -a pids=() local idx=0 for path in "${existing_paths[@]}"; do ( - local size=$(du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0") - local count=$(find "$path" -type f 2>/dev/null | wc -l | tr -d ' ') + local size + size=$(du -sk "$path" 2> /dev/null | awk '{print $1}' || echo "0") + local count + count=$(find "$path" -type f 2> /dev/null | wc -l | tr -d ' ') # Use index + PID for unique filename local tmp_file="$temp_dir/result_${idx}.$$" echo "$size $count" > "$tmp_file" - mv "$tmp_file" "$temp_dir/result_${idx}" 2>/dev/null || true + mv "$tmp_file" "$temp_dir/result_${idx}" 2> /dev/null || true ) & pids+=($!) ((idx++)) - if (( ${#pids[@]} >= MAX_PARALLEL_JOBS )); then - wait "${pids[0]}" 2>/dev/null || true + if ((${#pids[@]} >= MAX_PARALLEL_JOBS)); then + wait "${pids[0]}" 2> /dev/null || true pids=("${pids[@]:1}") fi done for pid in "${pids[@]}"; do - wait "$pid" 2>/dev/null || true + wait "$pid" 2> /dev/null || true done # Read results using same index @@ -285,10 +288,10 @@ safe_clean() { for path in "${existing_paths[@]}"; do local result_file="$temp_dir/result_${idx}" if [[ -f "$result_file" ]]; then - read -r size count < "$result_file" 2>/dev/null || true + read -r size count < "$result_file" 2> /dev/null || true if [[ "$count" -gt 0 && "$size" -gt 0 ]]; then if [[ "$DRY_RUN" != "true" ]]; then - rm -rf "$path" 2>/dev/null || true + rm -rf "$path" 2> /dev/null || true fi ((total_size_bytes += size)) ((total_count += count)) @@ -304,12 +307,14 @@ safe_clean() { if [[ -t 1 ]]; then MOLE_SPINNER_PREFIX=" " start_inline_spinner "Checking items with whitelist safety..."; fi for path in "${existing_paths[@]}"; do - local size_bytes=$(du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0") - local count=$(find "$path" -type f 2>/dev/null | wc -l | tr -d ' ') + local size_bytes + size_bytes=$(du -sk "$path" 2> /dev/null | awk '{print $1}' || echo "0") + local count + count=$(find "$path" -type f 2> /dev/null | wc -l | tr -d ' ') if [[ "$count" -gt 0 && "$size_bytes" -gt 0 ]]; then if [[ "$DRY_RUN" != "true" ]]; then - rm -rf "$path" 2>/dev/null || true + rm -rf "$path" 2> /dev/null || true fi ((total_size_bytes += size_bytes)) ((total_count += count)) @@ -319,7 +324,10 @@ safe_clean() { fi # Clear progress / stop spinner before showing result - if [[ -t 1 ]]; then stop_inline_spinner; echo -ne "\r\033[K"; fi + if [[ -t 1 ]]; then + stop_inline_spinner + echo -ne "\r\033[K" + fi if [[ $removed_any -eq 1 ]]; then # Convert KB to bytes for bytes_to_human() @@ -335,8 +343,8 @@ safe_clean() { else echo -e " ${GREEN}${ICON_SUCCESS}${NC} $label ${GREEN}($size_human)${NC}" fi - ((files_cleaned+=total_count)) - ((total_size_cleaned+=total_size_bytes)) + ((files_cleaned += total_count)) + ((total_size_cleaned += total_size_bytes)) ((total_items++)) note_activity fi @@ -349,7 +357,7 @@ start_cleanup() { clear printf '\n' echo -e "${PURPLE}Clean Your Mac${NC}" - + if [[ "$DRY_RUN" != "true" && -t 0 ]]; then echo "" echo -e "${YELLOW}Tip:${NC} Safety first—run 'mo clean --dry-run'. Important Macs should stop." @@ -384,7 +392,7 @@ start_cleanup() { # Enter = yes, do system cleanup if [[ -z "$choice" ]] || [[ "$choice" == $'\n' ]]; then - printf "\r\033[K" # Clear the prompt line + printf "\r\033[K" # Clear the prompt line if request_sudo_access "System cleanup requires admin access"; then SYSTEM_CLEAN=true echo -e "${GREEN}${ICON_SUCCESS}${NC} Admin access granted" @@ -393,7 +401,7 @@ start_cleanup() { ( local retry_count=0 while true; do - if ! sudo -n true 2>/dev/null; then + if ! sudo -n true 2> /dev/null; then ((retry_count++)) if [[ $retry_count -ge 3 ]]; then exit 1 @@ -403,9 +411,9 @@ start_cleanup() { fi retry_count=0 sleep 30 - kill -0 "$$" 2>/dev/null || exit + kill -0 "$$" 2> /dev/null || exit done - ) 2>/dev/null & + ) 2> /dev/null & SUDO_KEEPALIVE_PID=$! else SYSTEM_CLEAN=false @@ -430,7 +438,7 @@ start_cleanup() { perform_cleanup() { echo -e "${BLUE}${ICON_ADMIN}${NC} $(detect_architecture) | Free space: $(get_free_space)" - + # Show whitelist info if patterns are active local active_count=${#WHITELIST_PATTERNS[@]} if [[ $active_count -gt 2 ]]; then @@ -453,25 +461,25 @@ perform_cleanup() { start_section "Deep system-level cleanup" # Clean system caches more safely - sudo find /Library/Caches -name "*.cache" -delete 2>/dev/null || true - sudo find /Library/Caches -name "*.tmp" -delete 2>/dev/null || true - sudo find /Library/Caches -type f -name "*.log" -delete 2>/dev/null || true + sudo find /Library/Caches -name "*.cache" -delete 2> /dev/null || true + sudo find /Library/Caches -name "*.tmp" -delete 2> /dev/null || true + sudo find /Library/Caches -type f -name "*.log" -delete 2> /dev/null || true # Clean old temp files only (avoid breaking running processes) local tmp_cleaned=0 - local tmp_count=$(sudo find /tmp -type f -mtime +${TEMP_FILE_AGE_DAYS} 2>/dev/null | wc -l | tr -d ' ') + local tmp_count=$(sudo find /tmp -type f -mtime +${TEMP_FILE_AGE_DAYS} 2> /dev/null | wc -l | tr -d ' ') if [[ "$tmp_count" -gt 0 ]]; then - sudo find /tmp -type f -mtime +${TEMP_FILE_AGE_DAYS} -delete 2>/dev/null || true + sudo find /tmp -type f -mtime +${TEMP_FILE_AGE_DAYS} -delete 2> /dev/null || true tmp_cleaned=1 fi - local var_tmp_count=$(sudo find /var/tmp -type f -mtime +${TEMP_FILE_AGE_DAYS} 2>/dev/null | wc -l | tr -d ' ') + local var_tmp_count=$(sudo find /var/tmp -type f -mtime +${TEMP_FILE_AGE_DAYS} 2> /dev/null | wc -l | tr -d ' ') if [[ "$var_tmp_count" -gt 0 ]]; then - sudo find /var/tmp -type f -mtime +${TEMP_FILE_AGE_DAYS} -delete 2>/dev/null || true + sudo find /var/tmp -type f -mtime +${TEMP_FILE_AGE_DAYS} -delete 2> /dev/null || true tmp_cleaned=1 fi [[ $tmp_cleaned -eq 1 ]] && log_success "Old system temp files (${TEMP_FILE_AGE_DAYS}+ days)" - sudo rm -rf /Library/Updates/* 2>/dev/null || true + sudo rm -rf /Library/Updates/* 2> /dev/null || true log_success "System library caches and updates" end_section @@ -497,15 +505,15 @@ perform_cleanup() { [[ -d "$volume" && -d "$volume/.Trashes" && -w "$volume" ]] || continue # Skip network volumes - local fs_type=$(df -T "$volume" 2>/dev/null | tail -1 | awk '{print $2}') + local fs_type=$(df -T "$volume" 2> /dev/null | tail -1 | awk '{print $2}') case "$fs_type" in - nfs|smbfs|afpfs|cifs|webdav) continue ;; + nfs | smbfs | afpfs | cifs | webdav) continue ;; esac # Verify volume is mounted if mount | grep -q "on $volume "; then if [[ "$DRY_RUN" != "true" ]]; then - find "$volume/.Trashes" -mindepth 1 -maxdepth 1 -exec rm -rf {} \; 2>/dev/null || true + find "$volume/.Trashes" -mindepth 1 -maxdepth 1 -exec rm -rf {} \; 2> /dev/null || true fi fi done @@ -526,7 +534,6 @@ perform_cleanup() { safe_clean ~/Downloads/*.part "Incomplete downloads (partial)" end_section - # ===== 3. macOS System Caches ===== start_section "macOS system caches" safe_clean ~/Library/Saved\ Application\ State/* "Saved application states" @@ -542,7 +549,6 @@ perform_cleanup() { safe_clean ~/Library/Application\ Support/CloudDocs/session/db/* "iCloud session cache" end_section - # ===== 4. Sandboxed App Caches ===== start_section "Sandboxed app caches" # Clean specific high-usage apps first for better user feedback @@ -553,7 +559,6 @@ perform_cleanup() { safe_clean ~/Library/Containers/*/Data/Library/Caches/* "Sandboxed app caches" end_section - # ===== 5. Browsers ===== start_section "Browser cleanup" # Safari (cache only, NOT local storage or databases to preserve login states) @@ -577,7 +582,6 @@ perform_cleanup() { safe_clean ~/Library/Application\ Support/Firefox/Profiles/*/cache2/* "Firefox profile cache" end_section - # ===== 6. Cloud Storage ===== start_section "Cloud storage caches" # Only cache files, not sync state or login credentials @@ -590,7 +594,6 @@ perform_cleanup() { safe_clean ~/Library/Caches/com.microsoft.OneDrive "OneDrive cache" end_section - # ===== 7. Office Applications ===== start_section "Office applications" safe_clean ~/Library/Caches/com.microsoft.Word "Microsoft Word cache" @@ -603,11 +606,10 @@ perform_cleanup() { safe_clean ~/Library/Caches/com.apple.mail/* "Apple Mail cache" end_section - # ===== 8. Developer tools ===== start_section "Developer tools" # Node.js ecosystem - if command -v npm >/dev/null 2>&1; then + if command -v npm > /dev/null 2>&1; then if [[ "$DRY_RUN" != "true" ]]; then clean_tool_cache "npm cache" npm cache clean --force else @@ -622,7 +624,7 @@ perform_cleanup() { safe_clean ~/.bun/install/cache/* "Bun cache" # Python ecosystem - if command -v pip3 >/dev/null 2>&1; then + if command -v pip3 > /dev/null 2>&1; then if [[ "$DRY_RUN" != "true" ]]; then clean_tool_cache "pip cache" pip3 cache purge else @@ -636,7 +638,7 @@ perform_cleanup() { safe_clean ~/.pyenv/cache/* "pyenv cache" # Go ecosystem - if command -v go >/dev/null 2>&1; then + if command -v go > /dev/null 2>&1; then if [[ "$DRY_RUN" != "true" ]]; then clean_tool_cache "Go cache" bash -c 'go clean -modcache >/dev/null 2>&1 || true; go clean -cache >/dev/null 2>&1 || true' else @@ -652,7 +654,7 @@ perform_cleanup() { safe_clean ~/.cargo/registry/cache/* "Rust cargo cache" # Docker (only clean build cache, preserve images and volumes) - if command -v docker >/dev/null 2>&1; then + if command -v docker > /dev/null 2>&1; then if [[ "$DRY_RUN" != "true" ]]; then clean_tool_cache "Docker build cache" docker builder prune -af else @@ -674,9 +676,10 @@ perform_cleanup() { safe_clean ~/Library/Caches/Homebrew/* "Homebrew cache" safe_clean /opt/homebrew/var/homebrew/locks/* "Homebrew lock files (M series)" safe_clean /usr/local/var/homebrew/locks/* "Homebrew lock files (Intel)" - if command -v brew >/dev/null 2>&1; then + if command -v brew > /dev/null 2>&1; then if [[ "$DRY_RUN" != "true" ]]; then - clean_tool_cache "Homebrew cleanup" brew cleanup + # Use -s (scrub cache) for faster cleanup, --prune=all removes old versions + MOLE_CMD_TIMEOUT=300 clean_tool_cache "Homebrew cleanup" brew cleanup -s --prune=all else echo -e " ${YELLOW}→${NC} Homebrew (would cleanup)" fi @@ -818,7 +821,6 @@ perform_cleanup() { end_section - # ===== 10. Applications ===== start_section "Applications" @@ -983,7 +985,6 @@ perform_cleanup() { end_section - # ===== 11. Virtualization Tools ===== start_section "Virtualization tools" safe_clean ~/Library/Caches/com.vmware.fusion "VMware Fusion cache" @@ -992,7 +993,6 @@ perform_cleanup() { safe_clean ~/.vagrant.d/tmp/* "Vagrant temporary files" end_section - # ===== 12. Application Support logs cleanup ===== start_section "Application Support logs" @@ -1003,7 +1003,7 @@ perform_cleanup() { # Skip system and protected apps case "$app_name" in - com.apple.*|Adobe*|1Password|Claude) + com.apple.* | Adobe* | 1Password | Claude) continue ;; esac @@ -1022,7 +1022,6 @@ perform_cleanup() { end_section - # ===== 13. Orphaned app data cleanup ===== # Deep cleanup of leftover files from uninstalled apps # @@ -1074,32 +1073,32 @@ perform_cleanup() { if [[ -d "$search_path" ]]; then while IFS= read -r app; do [[ -f "$app/Contents/Info.plist" ]] || continue - bundle_id=$(defaults read "$app/Contents/Info.plist" CFBundleIdentifier 2>/dev/null || echo "") + bundle_id=$(defaults read "$app/Contents/Info.plist" CFBundleIdentifier 2> /dev/null || echo "") [[ -n "$bundle_id" ]] && echo "$bundle_id" >> "$installed_bundles" - done < <(find "$search_path" -maxdepth 3 -type d -name "*.app" 2>/dev/null || true) + done < <(find "$search_path" -maxdepth 3 -type d -name "*.app" 2> /dev/null || true) fi done # Use Spotlight as fallback to catch apps in unusual locations # This significantly reduces false positives - if command -v mdfind >/dev/null 2>&1; then + if command -v mdfind > /dev/null 2>&1; then while IFS= read -r app; do [[ -f "$app/Contents/Info.plist" ]] || continue - bundle_id=$(defaults read "$app/Contents/Info.plist" CFBundleIdentifier 2>/dev/null || echo "") + bundle_id=$(defaults read "$app/Contents/Info.plist" CFBundleIdentifier 2> /dev/null || echo "") [[ -n "$bundle_id" ]] && echo "$bundle_id" >> "$installed_bundles" - done < <(mdfind "kMDItemKind == 'Application'" 2>/dev/null | grep "\.app$" || true) + done < <(mdfind "kMDItemKind == 'Application'" 2> /dev/null | grep "\.app$" || true) fi # Get running applications (if an app is running, it's definitely not orphaned) - local running_apps=$(osascript -e 'tell application "System Events" to get bundle identifier of every application process' 2>/dev/null || echo "") + local running_apps=$(osascript -e 'tell application "System Events" to get bundle identifier of every application process' 2> /dev/null || echo "") echo "$running_apps" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | grep -v '^$' > "$running_bundles" # Check LaunchAgents and LaunchDaemons (if app has launch items, it likely exists) find ~/Library/LaunchAgents /Library/LaunchAgents /Library/LaunchDaemons \ - -name "*.plist" -type f 2>/dev/null | while IFS= read -r plist; do + -name "*.plist" -type f 2> /dev/null | while IFS= read -r plist; do bundle_id=$(basename "$plist" .plist) echo "$bundle_id" >> "$launch_agents" - done 2>/dev/null || true + done 2> /dev/null || true # Combine and deduplicate all bundle IDs sort -u "$installed_bundles" "$running_bundles" "$launch_agents" > "${installed_bundles}.final" @@ -1117,7 +1116,7 @@ perform_cleanup() { # Returns 0 (true) only if we are VERY CONFIDENT the app is uninstalled is_orphaned() { local bundle_id="$1" - local directory_path="$2" # The actual directory we're considering deleting + local directory_path="$2" # The actual directory we're considering deleting # SAFETY CHECK 1: Skip system-critical and protected apps (MOST IMPORTANT) if should_protect_data "$bundle_id"; then @@ -1125,13 +1124,13 @@ perform_cleanup() { fi # SAFETY CHECK 2: Check if app bundle exists in our comprehensive scan - if grep -q "^$bundle_id$" "$installed_bundles" 2>/dev/null; then + if grep -q "^$bundle_id$" "$installed_bundles" 2> /dev/null; then return 1 fi # SAFETY CHECK 3: Extra check for system bundles (belt and suspenders) case "$bundle_id" in - com.apple.*|loginwindow|dock|systempreferences|finder|safari) + com.apple.* | loginwindow | dock | systempreferences | finder | safari) return 1 ;; esac @@ -1139,7 +1138,7 @@ perform_cleanup() { # SAFETY CHECK 4: If it's a very common/important prefix, be extra careful # For major vendors, we NEVER auto-clean (too risky) case "$bundle_id" in - com.adobe.*|com.microsoft.*|com.google.*|org.mozilla.*|com.jetbrains.*|com.docker.*) + com.adobe.* | com.microsoft.* | com.google.* | org.mozilla.* | com.jetbrains.* | com.docker.*) return 1 ;; esac @@ -1149,9 +1148,9 @@ perform_cleanup() { # This protects against apps in unusual locations we didn't scan if [[ -e "$directory_path" ]]; then # Get last access time (days ago) - local last_access_epoch=$(stat -f%a "$directory_path" 2>/dev/null || echo "0") + local last_access_epoch=$(stat -f%a "$directory_path" 2> /dev/null || echo "0") local current_epoch=$(date +%s) - local days_since_access=$(( (current_epoch - last_access_epoch) / 86400 )) + local days_since_access=$(((current_epoch - last_access_epoch) / 86400)) # If accessed in the last 60 days, DO NOT DELETE # This means app is likely still installed somewhere @@ -1167,12 +1166,12 @@ perform_cleanup() { # Clean orphaned caches MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning orphaned caches..." local cache_found=0 - if ls ~/Library/Caches/com.* >/dev/null 2>&1; then + if ls ~/Library/Caches/com.* > /dev/null 2>&1; then for cache_dir in ~/Library/Caches/com.* ~/Library/Caches/org.* ~/Library/Caches/net.* ~/Library/Caches/io.*; do [[ -d "$cache_dir" ]] || continue local bundle_id=$(basename "$cache_dir") if is_orphaned "$bundle_id" "$cache_dir"; then - local size_kb=$(du -sk "$cache_dir" 2>/dev/null | awk '{print $1}' || echo "0") + local size_kb=$(du -sk "$cache_dir" 2> /dev/null | awk '{print $1}' || echo "0") if [[ "$size_kb" -gt 0 ]]; then safe_clean "$cache_dir" "Orphaned cache: $bundle_id" ((cache_found++)) @@ -1187,12 +1186,12 @@ perform_cleanup() { # Clean orphaned logs MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning orphaned logs..." local logs_found=0 - if ls ~/Library/Logs/com.* >/dev/null 2>&1; then + if ls ~/Library/Logs/com.* > /dev/null 2>&1; then for log_dir in ~/Library/Logs/com.* ~/Library/Logs/org.* ~/Library/Logs/net.* ~/Library/Logs/io.*; do [[ -d "$log_dir" ]] || continue local bundle_id=$(basename "$log_dir") if is_orphaned "$bundle_id" "$log_dir"; then - local size_kb=$(du -sk "$log_dir" 2>/dev/null | awk '{print $1}' || echo "0") + local size_kb=$(du -sk "$log_dir" 2> /dev/null | awk '{print $1}' || echo "0") if [[ "$size_kb" -gt 0 ]]; then safe_clean "$log_dir" "Orphaned logs: $bundle_id" ((logs_found++)) @@ -1207,12 +1206,12 @@ perform_cleanup() { # Clean orphaned saved states MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning orphaned saved states..." local states_found=0 - if ls ~/Library/Saved\ Application\ State/*.savedState >/dev/null 2>&1; then + if ls ~/Library/Saved\ Application\ State/*.savedState > /dev/null 2>&1; then for state_dir in ~/Library/Saved\ Application\ State/*.savedState; do [[ -d "$state_dir" ]] || continue local bundle_id=$(basename "$state_dir" .savedState) if is_orphaned "$bundle_id" "$state_dir"; then - local size_kb=$(du -sk "$state_dir" 2>/dev/null | awk '{print $1}' || echo "0") + local size_kb=$(du -sk "$state_dir" 2> /dev/null | awk '{print $1}' || echo "0") if [[ "$size_kb" -gt 0 ]]; then safe_clean "$state_dir" "Orphaned state: $bundle_id" ((states_found++)) @@ -1231,13 +1230,13 @@ perform_cleanup() { # To avoid deleting data from installed apps, we skip container cleanup. MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning orphaned containers..." local containers_found=0 - if ls ~/Library/Containers/com.* >/dev/null 2>&1; then + if ls ~/Library/Containers/com.* > /dev/null 2>&1; then # Count potential orphaned containers but don't delete them for container_dir in ~/Library/Containers/com.* ~/Library/Containers/org.* ~/Library/Containers/net.* ~/Library/Containers/io.*; do [[ -d "$container_dir" ]] || continue local bundle_id=$(basename "$container_dir") if is_orphaned "$bundle_id" "$container_dir"; then - local size_kb=$(du -sk "$container_dir" 2>/dev/null | awk '{print $1}' || echo "0") + local size_kb=$(du -sk "$container_dir" 2> /dev/null | awk '{print $1}' || echo "0") if [[ "$size_kb" -gt 0 ]]; then # DISABLED: safe_clean "$container_dir" "Orphaned container: $bundle_id" ((containers_found++)) @@ -1252,12 +1251,12 @@ perform_cleanup() { # Clean orphaned WebKit data MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning orphaned WebKit data..." local webkit_found=0 - if ls ~/Library/WebKit/com.* >/dev/null 2>&1; then + if ls ~/Library/WebKit/com.* > /dev/null 2>&1; then for webkit_dir in ~/Library/WebKit/com.* ~/Library/WebKit/org.* ~/Library/WebKit/net.* ~/Library/WebKit/io.*; do [[ -d "$webkit_dir" ]] || continue local bundle_id=$(basename "$webkit_dir") if is_orphaned "$bundle_id" "$webkit_dir"; then - local size_kb=$(du -sk "$webkit_dir" 2>/dev/null | awk '{print $1}' || echo "0") + local size_kb=$(du -sk "$webkit_dir" 2> /dev/null | awk '{print $1}' || echo "0") if [[ "$size_kb" -gt 0 ]]; then safe_clean "$webkit_dir" "Orphaned WebKit: $bundle_id" ((webkit_found++)) @@ -1272,12 +1271,12 @@ perform_cleanup() { # Clean orphaned HTTP storages MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning orphaned HTTP storages..." local http_found=0 - if ls ~/Library/HTTPStorages/com.* >/dev/null 2>&1; then + if ls ~/Library/HTTPStorages/com.* > /dev/null 2>&1; then for http_dir in ~/Library/HTTPStorages/com.* ~/Library/HTTPStorages/org.* ~/Library/HTTPStorages/net.* ~/Library/HTTPStorages/io.*; do [[ -d "$http_dir" ]] || continue local bundle_id=$(basename "$http_dir") if is_orphaned "$bundle_id" "$http_dir"; then - local size_kb=$(du -sk "$http_dir" 2>/dev/null | awk '{print $1}' || echo "0") + local size_kb=$(du -sk "$http_dir" 2> /dev/null | awk '{print $1}' || echo "0") if [[ "$size_kb" -gt 0 ]]; then safe_clean "$http_dir" "Orphaned HTTP storage: $bundle_id" ((http_found++)) @@ -1292,12 +1291,12 @@ perform_cleanup() { # Clean orphaned cookies MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning orphaned cookies..." local cookies_found=0 - if ls ~/Library/Cookies/*.binarycookies >/dev/null 2>&1; then + if ls ~/Library/Cookies/*.binarycookies > /dev/null 2>&1; then for cookie_file in ~/Library/Cookies/*.binarycookies; do [[ -f "$cookie_file" ]] || continue local bundle_id=$(basename "$cookie_file" .binarycookies) if is_orphaned "$bundle_id" "$cookie_file"; then - local size_kb=$(du -sk "$cookie_file" 2>/dev/null | awk '{print $1}' || echo "0") + local size_kb=$(du -sk "$cookie_file" 2> /dev/null | awk '{print $1}' || echo "0") if [[ "$size_kb" -gt 0 ]]; then safe_clean "$cookie_file" "Orphaned cookies: $bundle_id" ((cookies_found++)) @@ -1340,9 +1339,9 @@ perform_cleanup() { start_section "iOS device backups" backup_dir="$HOME/Library/Application Support/MobileSync/Backup" if [[ -d "$backup_dir" ]] && find "$backup_dir" -mindepth 1 -maxdepth 1 | read -r _; then - backup_kb=$(du -sk "$backup_dir" 2>/dev/null | awk '{print $1}') + backup_kb=$(du -sk "$backup_dir" 2> /dev/null | awk '{print $1}') if [[ -n "${backup_kb:-}" && "$backup_kb" -gt 102400 ]]; then - backup_human=$(du -sh "$backup_dir" 2>/dev/null | awk '{print $1}') + backup_human=$(du -sh "$backup_dir" 2> /dev/null | awk '{print $1}') note_activity echo -e " Found ${GREEN}${backup_human}${NC} iOS backups" echo -e " You can delete them manually: ${backup_dir}" @@ -1361,9 +1360,9 @@ perform_cleanup() { # Skip system volume and network volumes [[ "$volume" == "/Volumes/MacintoshHD" || "$volume" == "/" ]] && continue - local fs_type=$(df -T "$volume" 2>/dev/null | tail -1 | awk '{print $2}') + local fs_type=$(df -T "$volume" 2> /dev/null | tail -1 | awk '{print $2}') case "$fs_type" in - nfs|smbfs|afpfs|cifs|webdav) continue ;; + nfs | smbfs | afpfs | cifs | webdav) continue ;; esac # Look for HFS+ style backups (Backups.backupdb) @@ -1374,19 +1373,19 @@ perform_cleanup() { while IFS= read -r inprogress_file; do [[ -d "$inprogress_file" ]] || continue - local size_kb=$(du -sk "$inprogress_file" 2>/dev/null | awk '{print $1}' || echo "0") + local size_kb=$(du -sk "$inprogress_file" 2> /dev/null | awk '{print $1}' || echo "0") if [[ "$size_kb" -gt 0 ]]; then local backup_name=$(basename "$inprogress_file") if [[ "$DRY_RUN" != "true" ]]; then # Use tmutil to safely delete the failed backup - if command -v tmutil >/dev/null 2>&1; then - if tmutil delete "$inprogress_file" 2>/dev/null; then + if command -v tmutil > /dev/null 2>&1; then + if tmutil delete "$inprogress_file" 2> /dev/null; then local size_human=$(bytes_to_human "$((size_kb * 1024))") echo -e " ${GREEN}${ICON_SUCCESS}${NC} Failed backup: $backup_name ${GREEN}($size_human)${NC}" ((tm_cleaned++)) ((files_cleaned++)) - ((total_size_cleaned+=size_kb)) + ((total_size_cleaned += size_kb)) ((total_items++)) note_activity else @@ -1402,7 +1401,7 @@ perform_cleanup() { note_activity fi fi - done < <(find "$backupdb_dir" -type d \( -name "*.inProgress" -o -name "*.inprogress" \) 2>/dev/null || true) + done < <(find "$backupdb_dir" -type d \( -name "*.inProgress" -o -name "*.inprogress" \) 2> /dev/null || true) fi # Look for APFS style backups (.backupbundle or .sparsebundle) @@ -1414,25 +1413,25 @@ perform_cleanup() { # Check if bundle is already mounted by looking at hdiutil info local bundle_name=$(basename "$bundle") - local mounted_path=$(hdiutil info 2>/dev/null | grep -A 5 "image-path.*$bundle_name" | grep "/Volumes/" | awk '{print $1}' | head -1 || echo "") + local mounted_path=$(hdiutil info 2> /dev/null | grep -A 5 "image-path.*$bundle_name" | grep "/Volumes/" | awk '{print $1}' | head -1 || echo "") if [[ -n "$mounted_path" && -d "$mounted_path" ]]; then # Bundle is already mounted, safe to check while IFS= read -r inprogress_file; do [[ -d "$inprogress_file" ]] || continue - local size_kb=$(du -sk "$inprogress_file" 2>/dev/null | awk '{print $1}' || echo "0") + local size_kb=$(du -sk "$inprogress_file" 2> /dev/null | awk '{print $1}' || echo "0") if [[ "$size_kb" -gt 0 ]]; then local backup_name=$(basename "$inprogress_file") if [[ "$DRY_RUN" != "true" ]]; then - if command -v tmutil >/dev/null 2>&1; then - if tmutil delete "$inprogress_file" 2>/dev/null; then + if command -v tmutil > /dev/null 2>&1; then + if tmutil delete "$inprogress_file" 2> /dev/null; then local size_human=$(bytes_to_human "$((size_kb * 1024))") echo -e " ${GREEN}${ICON_SUCCESS}${NC} Failed APFS backup in $bundle_name: $backup_name ${GREEN}($size_human)${NC}" ((tm_cleaned++)) ((files_cleaned++)) - ((total_size_cleaned+=size_kb)) + ((total_size_cleaned += size_kb)) ((total_items++)) note_activity else @@ -1446,7 +1445,7 @@ perform_cleanup() { note_activity fi fi - done < <(find "$mounted_path" -type d \( -name "*.inProgress" -o -name "*.inprogress" \) 2>/dev/null || true) + done < <(find "$mounted_path" -type d \( -name "*.inProgress" -o -name "*.inprogress" \) 2> /dev/null || true) fi done done @@ -1525,12 +1524,11 @@ perform_cleanup() { printf '\n' } - main() { # Parse args (only dry-run and help for minimal impact) for arg in "$@"; do case "$arg" in - "--dry-run"|"-n") + "--dry-run" | "-n") DRY_RUN=true ;; "--whitelist") @@ -1538,7 +1536,7 @@ main() { manage_whitelist exit 0 ;; - "--help"|"-h") + "--help" | "-h") echo "Mole - Deeper system cleanup" echo "Usage: clean.sh [options]" echo "" diff --git a/bin/touchid.sh b/bin/touchid.sh index 9195199..a882460 100755 --- a/bin/touchid.sh +++ b/bin/touchid.sh @@ -20,14 +20,14 @@ is_touchid_configured() { if [[ ! -f "$PAM_SUDO_FILE" ]]; then return 1 fi - grep -q "pam_tid.so" "$PAM_SUDO_FILE" 2>/dev/null + grep -q "pam_tid.so" "$PAM_SUDO_FILE" 2> /dev/null } # Check if system supports Touch ID supports_touchid() { # Check if bioutil exists and has Touch ID capability - if command -v bioutil &>/dev/null; then - bioutil -r 2>/dev/null | grep -q "Touch ID" && return 0 + if command -v bioutil &> /dev/null; then + bioutil -r 2> /dev/null | grep -q "Touch ID" && return 0 fi # Fallback: check if running on Apple Silicon or modern Intel Mac @@ -39,7 +39,7 @@ supports_touchid() { # For Intel Macs, check if it's 2018 or later (approximation) local model_year - model_year=$(system_profiler SPHardwareDataType 2>/dev/null | grep "Model Identifier" | grep -o "[0-9]\{4\}" | head -1) + model_year=$(system_profiler SPHardwareDataType 2> /dev/null | grep "Model Identifier" | grep -o "[0-9]\{4\}" | head -1) if [[ -n "$model_year" ]] && [[ "$model_year" -ge 2018 ]]; then return 0 fi @@ -76,7 +76,7 @@ enable_touchid() { fi # Create backup and apply changes - if ! sudo cp "$PAM_SUDO_FILE" "${PAM_SUDO_FILE}.mole-backup" 2>/dev/null; then + if ! sudo cp "$PAM_SUDO_FILE" "${PAM_SUDO_FILE}.mole-backup" 2> /dev/null; then log_error "Failed to create backup" return 1 fi @@ -97,12 +97,12 @@ enable_touchid() { ' "$PAM_SUDO_FILE" > "$temp_file" # Apply the changes - if sudo mv "$temp_file" "$PAM_SUDO_FILE" 2>/dev/null; then + if sudo mv "$temp_file" "$PAM_SUDO_FILE" 2> /dev/null; then echo -e "${GREEN}${ICON_SUCCESS} Touch ID enabled${NC} ${GRAY}- try: sudo ls${NC}" echo "" return 0 else - rm -f "$temp_file" 2>/dev/null || true + rm -f "$temp_file" 2> /dev/null || true log_error "Failed to enable Touch ID" return 1 fi @@ -116,7 +116,7 @@ disable_touchid() { fi # Create backup and remove configuration - if ! sudo cp "$PAM_SUDO_FILE" "${PAM_SUDO_FILE}.mole-backup" 2>/dev/null; then + if ! sudo cp "$PAM_SUDO_FILE" "${PAM_SUDO_FILE}.mole-backup" 2> /dev/null; then log_error "Failed to create backup" return 1 fi @@ -126,12 +126,12 @@ disable_touchid() { temp_file=$(mktemp) grep -v "pam_tid.so" "$PAM_SUDO_FILE" > "$temp_file" - if sudo mv "$temp_file" "$PAM_SUDO_FILE" 2>/dev/null; then + if sudo mv "$temp_file" "$PAM_SUDO_FILE" 2> /dev/null; then echo -e "${GREEN}${ICON_SUCCESS} Touch ID disabled${NC}" echo "" return 0 else - rm -f "$temp_file" 2>/dev/null || true + rm -f "$temp_file" 2> /dev/null || true log_error "Failed to disable Touch ID" return 1 fi @@ -174,11 +174,11 @@ show_menu() { echo "" case "$key" in - $'\e') # ESC + $'\e') # ESC return 0 ;; - ""|$'\n'|$'\r') # Enter - printf "\r\033[K" # Clear the prompt line + "" | $'\n' | $'\r') # Enter + printf "\r\033[K" # Clear the prompt line disable_touchid ;; *) @@ -191,11 +191,11 @@ show_menu() { IFS= read -r -s -n1 key || key="" case "$key" in - $'\e') # ESC + $'\e') # ESC return 0 ;; - ""|$'\n'|$'\r') # Enter - printf "\r\033[K" # Clear the prompt line + "" | $'\n' | $'\r') # Enter + printf "\r\033[K" # Clear the prompt line enable_touchid ;; *) @@ -220,7 +220,7 @@ main() { status) show_status ;; - help|--help|-h) + help | --help | -h) show_help ;; "") diff --git a/bin/uninstall.sh b/bin/uninstall.sh index 90da6ac..942f2d1 100755 --- a/bin/uninstall.sh +++ b/bin/uninstall.sh @@ -56,10 +56,9 @@ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then fi # Initialize global variables -selected_apps=() # Global array for app selection +selected_apps=() # Global array for app selection declare -a apps_data=() declare -a selection_state=() -current_line=0 total_items=0 files_cleaned=0 total_size_cleaned=0 @@ -68,16 +67,16 @@ total_size_cleaned=0 get_app_last_used() { local app_path="$1" local last_used - last_used=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null) + last_used=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2> /dev/null) if [[ "$last_used" == "(null)" || -z "$last_used" ]]; then echo "Never" else local last_used_epoch - last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$last_used" "+%s" 2>/dev/null) + last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$last_used" "+%s" 2> /dev/null) local current_epoch current_epoch=$(date "+%s") - local days_ago=$(( (current_epoch - last_used_epoch) / 86400 )) + local days_ago=$(((current_epoch - last_used_epoch) / 86400)) if [[ $days_ago -eq 0 ]]; then echo "Today" @@ -86,10 +85,10 @@ get_app_last_used() { elif [[ $days_ago -lt 30 ]]; then echo "${days_ago} days ago" elif [[ $days_ago -lt 365 ]]; then - local months_ago=$(( days_ago / 30 )) + local months_ago=$((days_ago / 30)) echo "${months_ago} month(s) ago" else - local years_ago=$(( days_ago / 365 )) + local years_ago=$((days_ago / 365)) echo "${years_ago} year(s) ago" fi fi @@ -101,22 +100,24 @@ scan_applications() { local cache_dir="$HOME/.cache/mole" local cache_file="$cache_dir/app_scan_cache" local cache_meta="$cache_dir/app_scan_meta" - local cache_ttl=3600 # 1 hour cache validity + local cache_ttl=3600 # 1 hour cache validity - mkdir -p "$cache_dir" 2>/dev/null + mkdir -p "$cache_dir" 2> /dev/null # Quick count of current apps (system + user directories) local current_app_count current_app_count=$( - (find /Applications -name "*.app" -maxdepth 1 2>/dev/null; - find ~/Applications -name "*.app" -maxdepth 1 2>/dev/null) | wc -l | tr -d ' ' + ( + find /Applications -name "*.app" -maxdepth 1 2> /dev/null + find ~/Applications -name "*.app" -maxdepth 1 2> /dev/null + ) | wc -l | tr -d ' ' ) # Check if cache is valid unless explicitly disabled if [[ -f "$cache_file" && -f "$cache_meta" ]]; then - local cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0))) + local cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2> /dev/null || echo 0))) local cached_app_count - cached_app_count=$(cat "$cache_meta" 2>/dev/null || echo "0") + cached_app_count=$(cat "$cache_meta" 2> /dev/null || echo "0") # Cache is valid if: age < TTL AND app count matches if [[ $cache_age -lt $cache_ttl && "$cached_app_count" == "$current_app_count" ]]; then @@ -149,26 +150,26 @@ scan_applications() { local bundle_id="unknown" local display_name="$app_name" if [[ -f "$app_path/Contents/Info.plist" ]]; then - bundle_id=$(defaults read "$app_path/Contents/Info.plist" CFBundleIdentifier 2>/dev/null || echo "unknown") + bundle_id=$(defaults read "$app_path/Contents/Info.plist" CFBundleIdentifier 2> /dev/null || echo "unknown") # Try to get English name from bundle info local bundle_executable - bundle_executable=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2>/dev/null) + bundle_executable=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2> /dev/null) # Smart display name selection - prefer descriptive names over generic ones local candidates=() # Get all potential names local bundle_display_name - bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2>/dev/null) + bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2> /dev/null) local bundle_name - bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2>/dev/null) + bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2> /dev/null) # Check if executable name is generic/technical (should be avoided) local is_generic_executable=false if [[ -n "$bundle_executable" ]]; then case "$bundle_executable" in - "pake"|"Electron"|"electron"|"nwjs"|"node"|"helper"|"main"|"app"|"binary") + "pake" | "Electron" | "electron" | "nwjs" | "node" | "helper" | "main" | "app" | "binary") is_generic_executable=true ;; esac @@ -219,19 +220,19 @@ scan_applications() { app_data_tuples+=("${app_path}|${app_name}|${bundle_id}|${display_name}") done < <( # Scan both system and user application directories - find /Applications -name "*.app" -maxdepth 1 -print0 2>/dev/null - find ~/Applications -name "*.app" -maxdepth 1 -print0 2>/dev/null + find /Applications -name "*.app" -maxdepth 1 -print0 2> /dev/null + find ~/Applications -name "*.app" -maxdepth 1 -print0 2> /dev/null ) # Second pass: process each app with parallel size calculation local app_count=0 local total_apps=${#app_data_tuples[@]} - local max_parallel=10 # Process 10 apps in parallel + local max_parallel=10 # Process 10 apps in parallel local pids=() local inline_loading=false if [[ "${MOLE_INLINE_LOADING:-}" == "1" || "${MOLE_INLINE_LOADING:-}" == "true" ]]; then inline_loading=true - printf "\033[H" >&2 # Position cursor at top of screen + printf "\033[H" >&2 # Position cursor at top of screen fi # Process app metadata extraction function @@ -245,7 +246,7 @@ scan_applications() { # Parallel size calculation local app_size="N/A" if [[ -d "$app_path" ]]; then - app_size=$(du -sh "$app_path" 2>/dev/null | cut -f1 || echo "N/A") + app_size=$(du -sh "$app_path" 2> /dev/null | cut -f1 || echo "N/A") fi # Get real last used date from macOS metadata @@ -254,13 +255,13 @@ scan_applications() { if [[ -d "$app_path" ]]; then local metadata_date - metadata_date=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2>/dev/null) + metadata_date=$(mdls -name kMDItemLastUsedDate -raw "$app_path" 2> /dev/null) if [[ "$metadata_date" != "(null)" && -n "$metadata_date" ]]; then - last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$metadata_date" "+%s" 2>/dev/null || echo "0") + last_used_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$metadata_date" "+%s" 2> /dev/null || echo "0") if [[ $last_used_epoch -gt 0 ]]; then - local days_ago=$(( (current_epoch - last_used_epoch) / 86400 )) + local days_ago=$(((current_epoch - last_used_epoch) / 86400)) if [[ $days_ago -eq 0 ]]; then last_used="Today" @@ -269,21 +270,21 @@ scan_applications() { elif [[ $days_ago -lt 7 ]]; then last_used="${days_ago} days ago" elif [[ $days_ago -lt 30 ]]; then - local weeks_ago=$(( days_ago / 7 )) + local weeks_ago=$((days_ago / 7)) [[ $weeks_ago -eq 1 ]] && last_used="1 week ago" || last_used="${weeks_ago} weeks ago" elif [[ $days_ago -lt 365 ]]; then - local months_ago=$(( days_ago / 30 )) + local months_ago=$((days_ago / 30)) [[ $months_ago -eq 1 ]] && last_used="1 month ago" || last_used="${months_ago} months ago" else - local years_ago=$(( days_ago / 365 )) + local years_ago=$((days_ago / 365)) [[ $years_ago -eq 1 ]] && last_used="1 year ago" || last_used="${years_ago} years ago" fi fi else # Fallback to file modification time - last_used_epoch=$(stat -f%m "$app_path" 2>/dev/null || echo "0") + last_used_epoch=$(stat -f%m "$app_path" 2> /dev/null || echo "0") if [[ $last_used_epoch -gt 0 ]]; then - local days_ago=$(( (current_epoch - last_used_epoch) / 86400 )) + local days_ago=$(((current_epoch - last_used_epoch) / 86400)) if [[ $days_ago -lt 30 ]]; then last_used="Recent" elif [[ $days_ago -lt 365 ]]; then @@ -319,15 +320,15 @@ scan_applications() { ((spinner_idx++)) # Wait if we've hit max parallel limit - if (( ${#pids[@]} >= max_parallel )); then - wait "${pids[0]}" 2>/dev/null - pids=("${pids[@]:1}") # Remove first pid + if ((${#pids[@]} >= max_parallel)); then + wait "${pids[0]}" 2> /dev/null + pids=("${pids[@]:1}") # Remove first pid fi done # Wait for remaining background processes for pid in "${pids[@]}"; do - wait "$pid" 2>/dev/null + wait "$pid" 2> /dev/null done # Check if we found any applications @@ -347,12 +348,15 @@ scan_applications() { fi # Sort by last used (oldest first) and cache the result - sort -t'|' -k1,1n "$temp_file" > "${temp_file}.sorted" || { rm -f "$temp_file"; return 1; } + sort -t'|' -k1,1n "$temp_file" > "${temp_file}.sorted" || { + rm -f "$temp_file" + return 1 + } rm -f "$temp_file" # Update cache with app count metadata - cp "${temp_file}.sorted" "$cache_file" 2>/dev/null || true - echo "$current_app_count" > "$cache_meta" 2>/dev/null || true + cp "${temp_file}.sorted" "$cache_file" 2> /dev/null || true + echo "$current_app_count" > "$cache_meta" 2> /dev/null || true # Verify sorted file exists before returning if [[ -f "${temp_file}.sorted" ]]; then @@ -415,12 +419,12 @@ uninstall_applications() { echo "" # Check if app is running (use app path for precise matching) - if pgrep -f "$app_path" >/dev/null 2>&1; then + if pgrep -f "$app_path" > /dev/null 2>&1; then echo -e "${YELLOW}${ICON_ERROR} $app_name is currently running${NC}" read -p " Force quit $app_name? (y/N): " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then - pkill -f "$app_path" 2>/dev/null || true + pkill -f "$app_path" 2> /dev/null || true sleep 2 else echo -e " ${BLUE}${ICON_EMPTY}${NC} Skipped $app_name" @@ -438,7 +442,7 @@ uninstall_applications() { # Calculate total size local app_size_kb - app_size_kb=$(du -sk "$app_path" 2>/dev/null | awk '{print $1}' || echo "0") + app_size_kb=$(du -sk "$app_path" 2> /dev/null | awk '{print $1}' || echo "0") local related_size_kb related_size_kb=$(calculate_total_size "$related_files") local system_size_kb @@ -461,12 +465,13 @@ uninstall_applications() { done <<< "$system_files" fi - if [[ $total_kb -gt 1048576 ]]; then # > 1GB - local size_display=$(echo "$total_kb" | awk '{printf "%.2fGB", $1/1024/1024}') - elif [[ $total_kb -gt 1024 ]]; then # > 1MB - local size_display=$(echo "$total_kb" | awk '{printf "%.1fMB", $1/1024}') + local size_display + if [[ $total_kb -gt 1048576 ]]; then # > 1GB + size_display=$(echo "$total_kb" | awk '{printf "%.2fGB", $1/1024/1024}') + elif [[ $total_kb -gt 1024 ]]; then # > 1MB + size_display=$(echo "$total_kb" | awk '{printf "%.1fMB", $1/1024}') else - local size_display="${total_kb}KB" + size_display="${total_kb}KB" fi echo -e " ${BLUE}Total size: $size_display${NC}" @@ -477,7 +482,7 @@ uninstall_applications() { if [[ $REPLY =~ ^[Yy]$ ]]; then # Remove the application - if rm -rf "$app_path" 2>/dev/null; then + if rm -rf "$app_path" 2> /dev/null; then echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed application" else echo -e " ${RED}${ICON_ERROR}${NC} Failed to remove $app_path" @@ -487,7 +492,7 @@ uninstall_applications() { # Remove user-level related files while IFS= read -r file; do if [[ -n "$file" && -e "$file" ]]; then - if rm -rf "$file" 2>/dev/null; then + if rm -rf "$file" 2> /dev/null; then echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed $(echo "$file" | sed "s|$HOME|~|" | xargs basename)" fi fi @@ -498,7 +503,7 @@ uninstall_applications() { echo -e " ${BLUE}${ICON_SOLID}${NC} Admin access required for system files" while IFS= read -r file; do if [[ -n "$file" && -e "$file" ]]; then - if sudo rm -rf "$file" 2>/dev/null; then + if sudo rm -rf "$file" 2> /dev/null; then echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed $(basename "$file")" else echo -e " ${YELLOW}${ICON_ERROR}${NC} Failed to remove: $file" @@ -521,12 +526,13 @@ uninstall_applications() { echo -e "${PURPLE}${ICON_ARROW} Uninstallation Summary${NC}" if [[ $total_size_freed -gt 0 ]]; then - if [[ $total_size_freed -gt 1048576 ]]; then # > 1GB - local freed_display=$(echo "$total_size_freed" | awk '{printf "%.2fGB", $1/1024/1024}') - elif [[ $total_size_freed -gt 1024 ]]; then # > 1MB - local freed_display=$(echo "$total_size_freed" | awk '{printf "%.1fMB", $1/1024}') + local freed_display + if [[ $total_size_freed -gt 1048576 ]]; then # > 1GB + freed_display=$(echo "$total_size_freed" | awk '{printf "%.2fGB", $1/1024/1024}') + elif [[ $total_size_freed -gt 1024 ]]; then # > 1MB + freed_display=$(echo "$total_size_freed" | awk '{printf "%.1fMB", $1/1024}') else - local freed_display="${total_size_freed}KB" + freed_display="${total_size_freed}KB" fi echo -e " ${GREEN}${ICON_SUCCESS}${NC} Freed $freed_display of disk space" @@ -544,8 +550,8 @@ cleanup() { unset MOLE_ALT_SCREEN_ACTIVE fi 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 show_cursor @@ -634,7 +640,9 @@ main() { clear local selection_count=${#selected_apps[@]} if [[ $selection_count -eq 0 ]]; then - echo "No apps selected"; rm -f "$apps_file"; return 0 + echo "No apps selected" + rm -f "$apps_file" + return 0 fi # Show selected apps, max 3 per line echo -e "${BLUE}${ICON_CONFIRM}${NC} Selected ${selection_count} app(s):" @@ -644,7 +652,7 @@ main() { IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app" local display_item="${app_name}(${size})" - if (( idx % 3 == 0 )); then + if ((idx % 3 == 0)); then # Start new line [[ -n "$line" ]] && echo " $line" line="$display_item" diff --git a/install.sh b/install.sh index 2c72484..1775be5 100755 --- a/install.sh +++ b/install.sh @@ -13,14 +13,28 @@ NC='\033[0m' # Simple spinner _SPINNER_PID="" start_line_spinner() { - local msg="$1"; [[ ! -t 1 ]] && { echo -e "${BLUE}|${NC} $msg"; return; } - local chars="${MO_SPINNER_CHARS:-|/-\\}"; [[ -z "$chars" ]] && chars='|/-\\' + local msg="$1" + [[ ! -t 1 ]] && { + echo -e "${BLUE}|${NC} $msg" + return + } + local chars="${MO_SPINNER_CHARS:-|/-\\}" + [[ -z "$chars" ]] && chars='|/-\\' local i=0 - ( while true; do c="${chars:$((i % ${#chars})):1}"; printf "\r${BLUE}%s${NC} %s" "$c" "$msg"; ((i++)); sleep 0.12; done ) & + (while true; do + c="${chars:$((i % ${#chars})):1}" + printf "\r${BLUE}%s${NC} %s" "$c" "$msg" + ((i++)) + sleep 0.12 + done) & _SPINNER_PID=$! } -stop_line_spinner() { if [[ -n "$_SPINNER_PID" ]]; then kill "$_SPINNER_PID" 2>/dev/null || true; wait "$_SPINNER_PID" 2>/dev/null || true; _SPINNER_PID=""; printf "\r\033[K"; fi; } - +stop_line_spinner() { if [[ -n "$_SPINNER_PID" ]]; then + kill "$_SPINNER_PID" 2> /dev/null || true + wait "$_SPINNER_PID" 2> /dev/null || true + _SPINNER_PID="" + printf "\r\033[K" +fi; } # Verbosity (0 = quiet, 1 = verbose) VERBOSE=1 @@ -105,7 +119,7 @@ resolve_source_dir() { trap "rm -rf '$tmp'" EXIT start_line_spinner "Fetching Mole source..." - if command -v curl >/dev/null 2>&1; then + if command -v curl > /dev/null 2>&1; then if curl -fsSL -o "$tmp/mole.tar.gz" "https://github.com/tw93/mole/archive/refs/heads/main.tar.gz"; then stop_line_spinner tar -xzf "$tmp/mole.tar.gz" -C "$tmp" @@ -119,8 +133,8 @@ resolve_source_dir() { stop_line_spinner start_line_spinner "Cloning Mole source..." - if command -v git >/dev/null 2>&1; then - if git clone --depth=1 https://github.com/tw93/mole.git "$tmp/mole" >/dev/null 2>&1; then + if command -v git > /dev/null 2>&1; then + if git clone --depth=1 https://github.com/tw93/mole.git "$tmp/mole" > /dev/null 2>&1; then stop_line_spinner SOURCE_DIR="$tmp/mole" return 0 @@ -142,7 +156,7 @@ get_source_version() { get_installed_version() { local binary="$INSTALL_DIR/mole" if [[ -x "$binary" ]]; then - "$binary" --version 2>/dev/null | awk 'NF {print $NF; exit}' + "$binary" --version 2> /dev/null | awk 'NF {print $NF; exit}' fi } @@ -166,11 +180,11 @@ parse_args() { uninstall_mole exit 0 ;; - --verbose|-v) + --verbose | -v) VERBOSE=1 shift 1 ;; - --help|-h) + --help | -h) show_help exit 0 ;; @@ -192,7 +206,7 @@ check_requirements() { fi # Check if already installed via Homebrew - if command -v brew >/dev/null 2>&1 && brew list mole >/dev/null 2>&1; then + if command -v brew > /dev/null 2>&1 && brew list mole > /dev/null 2>&1; then if [[ "$ACTION" == "update" ]]; then return 0 fi @@ -330,7 +344,7 @@ verify_installation() { if [[ -x "$INSTALL_DIR/mole" ]] && [[ -f "$CONFIG_DIR/lib/common.sh" ]]; then # Test if mole command works - if "$INSTALL_DIR/mole" --help >/dev/null 2>&1; then + if "$INSTALL_DIR/mole" --help > /dev/null 2>&1; then return 0 else log_warning "Mole command installed but may not be working properly" @@ -369,7 +383,7 @@ print_usage_summary() { fi echo "" - + local message="Mole ${action} successfully" if [[ "$action" == "updated" && -n "$previous_version" && -n "$new_version" && "$previous_version" != "$new_version" ]]; then @@ -433,15 +447,15 @@ uninstall_mole() { # Additional safety: never delete system critical paths (check first) case "$CONFIG_DIR" in - /|/usr|/usr/local|/usr/local/bin|/usr/local/lib|/usr/local/share|\ - /Library|/System|/bin|/sbin|/etc|/var|/opt|"$HOME"|"$HOME/Library"|\ - /usr/local/lib/*|/usr/local/share/*|/Library/*|/System/*) + / | /usr | /usr/local | /usr/local/bin | /usr/local/lib | /usr/local/share | \ + /Library | /System | /bin | /sbin | /etc | /var | /opt | "$HOME" | "$HOME/Library" | \ + /usr/local/lib/* | /usr/local/share/* | /Library/* | /System/*) is_safe=0 ;; *) # Safe patterns: must be in user's home and end with 'mole' if [[ "$CONFIG_DIR" == "$HOME/.config/mole" ]] || - [[ "$CONFIG_DIR" == "$HOME"/.*/mole ]]; then + [[ "$CONFIG_DIR" == "$HOME"/.*/mole ]]; then is_safe=1 fi ;; @@ -457,7 +471,9 @@ uninstall_mole() { echo " $CONFIG_DIR" else echo "" - read -p "Remove configuration directory $CONFIG_DIR? (y/N): " -n 1 -r; echo ""; if [[ $REPLY =~ ^[Yy]$ ]]; then + read -p "Remove configuration directory $CONFIG_DIR? (y/N): " -n 1 -r + echo "" + if [[ $REPLY =~ ^[Yy]$ ]]; then rm -rf "$CONFIG_DIR" log_success "Removed configuration" else @@ -495,9 +511,9 @@ perform_install() { perform_update() { check_requirements - if command -v brew >/dev/null 2>&1 && brew list mole >/dev/null 2>&1; then + if command -v brew > /dev/null 2>&1 && brew list mole > /dev/null 2>&1; then # Try to use shared function if available (when running from installed Mole) - resolve_source_dir 2>/dev/null || true + resolve_source_dir 2> /dev/null || true if [[ -f "$SOURCE_DIR/lib/common.sh" ]]; then # shellcheck disable=SC1090,SC1091 source "$SOURCE_DIR/lib/common.sh" @@ -527,7 +543,7 @@ perform_update() { if echo "$upgrade_output" | grep -q "already installed"; then local current_version - current_version=$(brew list --versions mole 2>/dev/null | awk '{print $2}') + current_version=$(brew list --versions mole 2> /dev/null | awk '{print $2}') echo -e "${GREEN}✓${NC} Already on latest version (${current_version:-$VERSION})" elif echo "$upgrade_output" | grep -q "Error:"; then log_error "Homebrew upgrade failed" @@ -536,7 +552,7 @@ perform_update() { 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 -e "${GREEN}✓${NC} Updated to latest version (${new_version:-$VERSION})" fi @@ -571,9 +587,21 @@ perform_update() { # Update with minimal output (suppress info/success, show errors only) local old_verbose=$VERBOSE VERBOSE=0 - create_directories || { VERBOSE=$old_verbose; log_error "Failed to create directories"; exit 1; } - install_files || { VERBOSE=$old_verbose; log_error "Failed to install files"; exit 1; } - verify_installation || { VERBOSE=$old_verbose; log_error "Failed to verify installation"; exit 1; } + create_directories || { + VERBOSE=$old_verbose + log_error "Failed to create directories" + exit 1 + } + install_files || { + VERBOSE=$old_verbose + log_error "Failed to install files" + exit 1 + } + verify_installation || { + VERBOSE=$old_verbose + log_error "Failed to verify installation" + exit 1 + } setup_path VERBOSE=$old_verbose diff --git a/lib/batch_uninstall.sh b/lib/batch_uninstall.sh index 31398e6..c23a774 100755 --- a/lib/batch_uninstall.sh +++ b/lib/batch_uninstall.sh @@ -33,17 +33,17 @@ batch_uninstall_applications() { IFS='|' read -r epoch app_path app_name bundle_id size last_used <<< "$selected_app" # Check if app is running (use app path for precise matching) - if pgrep -f "$app_path" >/dev/null 2>&1; then + if pgrep -f "$app_path" > /dev/null 2>&1; then running_apps+=("$app_name") fi # Check if app requires sudo to delete - if [[ ! -w "$(dirname "$app_path")" ]] || [[ "$(stat -f%Su "$app_path" 2>/dev/null)" == "root" ]]; then + if [[ ! -w "$(dirname "$app_path")" ]] || [[ "$(stat -f%Su "$app_path" 2> /dev/null)" == "root" ]]; then sudo_apps+=("$app_name") fi # Calculate size for summary - local app_size_kb=$(du -sk "$app_path" 2>/dev/null | awk '{print $1}' || echo "0") + local app_size_kb=$(du -sk "$app_path" 2> /dev/null | awk '{print $1}' || echo "0") local related_files=$(find_app_files "$bundle_id" "$app_name") local related_size_kb=$(calculate_total_size "$related_files") local total_kb=$((app_size_kb + related_size_kb)) @@ -104,13 +104,13 @@ batch_uninstall_applications() { IFS= read -r -s -n1 key || key="" case "$key" in - $'\e'|q|Q) + $'\e' | q | Q) echo "" echo "" return 0 ;; - ""|$'\n'|$'\r'|y|Y) - printf "\r\033[K" # Clear the prompt line + "" | $'\n' | $'\r' | y | Y) + printf "\r\033[K" # Clear the prompt line ;; *) echo "" @@ -122,14 +122,18 @@ batch_uninstall_applications() { # User confirmed, now request sudo access if needed if [[ ${#sudo_apps[@]} -gt 0 ]]; then # Check if sudo is already cached - 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" return 1 fi fi - (while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null) & + (while true; do + sudo -n true + sleep 60 + kill -0 "$$" || exit + done 2> /dev/null) & sudo_keepalive_pid=$! fi @@ -148,22 +152,22 @@ batch_uninstall_applications() { local related_files=$(printf '%s' "$encoded_files" | base64 -d) local reason="" local needs_sudo=false - [[ ! -w "$(dirname "$app_path")" || "$(stat -f%Su "$app_path" 2>/dev/null)" == "root" ]] && needs_sudo=true + [[ ! -w "$(dirname "$app_path")" || "$(stat -f%Su "$app_path" 2> /dev/null)" == "root" ]] && needs_sudo=true if ! force_kill_app "$app_name" "$app_path"; then reason="still running" fi if [[ -z "$reason" ]]; then if [[ "$needs_sudo" == true ]]; then - sudo rm -rf "$app_path" 2>/dev/null || reason="remove failed" + sudo rm -rf "$app_path" 2> /dev/null || reason="remove failed" else - rm -rf "$app_path" 2>/dev/null || reason="remove failed" + rm -rf "$app_path" 2> /dev/null || reason="remove failed" fi fi if [[ -z "$reason" ]]; then local files_removed=0 while IFS= read -r file; do [[ -n "$file" && -e "$file" ]] || continue - rm -rf "$file" 2>/dev/null && ((files_removed++)) || true + rm -rf "$file" 2> /dev/null && ((files_removed++)) || true done <<< "$related_files" ((total_size_freed += total_kb)) ((success_count++)) @@ -202,7 +206,7 @@ batch_uninstall_applications() { for app_name in "${success_items[@]}"; do local display_item="${GREEN}${app_name}${NC}" - if (( idx % 3 == 0 )); then + if ((idx % 3 == 0)); then # Start new line if [[ -n "$current_line" ]]; then summary_details+=("$current_line") @@ -267,8 +271,8 @@ batch_uninstall_applications() { # 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 diff --git a/lib/common.sh b/lib/common.sh index 340a4da..93cc376 100755 --- a/lib/common.sh +++ b/lib/common.sh @@ -21,19 +21,19 @@ readonly GRAY="${ESC}[0;90m" readonly NC="${ESC}[0m" # Icon definitions (shared across modules) -readonly ICON_CONFIRM="◎" # Confirm operation / spinner text -readonly ICON_ADMIN="⚙" # Gear indicator for admin/settings/system info -readonly ICON_SUCCESS="✓" # Success mark -readonly ICON_ERROR="☻" # Error / warning mark -readonly ICON_EMPTY="○" # Hollow circle (empty state / unchecked) -readonly ICON_SOLID="●" # Solid circle (selected / system marker) -readonly ICON_LIST="•" # Basic list bullet -readonly ICON_ARROW="➤" # Pointer / prompt indicator -readonly ICON_WARNING="☻" # Warning marker (shares glyph with error) -readonly ICON_NAV_UP="↑" # Navigation up -readonly ICON_NAV_DOWN="↓" # Navigation down -readonly ICON_NAV_LEFT="←" # Navigation left -readonly ICON_NAV_RIGHT="→" # Navigation right +readonly ICON_CONFIRM="◎" # Confirm operation / spinner text +readonly ICON_ADMIN="⚙" # Gear indicator for admin/settings/system info +readonly ICON_SUCCESS="✓" # Success mark +readonly ICON_ERROR="☻" # Error / warning mark +readonly ICON_EMPTY="○" # Hollow circle (empty state / unchecked) +readonly ICON_SOLID="●" # Solid circle (selected / system marker) +readonly ICON_LIST="•" # Basic list bullet +readonly ICON_ARROW="➤" # Pointer / prompt indicator +readonly ICON_WARNING="☻" # Warning marker (shares glyph with error) +readonly ICON_NAV_UP="↑" # Navigation up +readonly ICON_NAV_DOWN="↓" # Navigation down +readonly ICON_NAV_LEFT="←" # Navigation left +readonly ICON_NAV_RIGHT="→" # Navigation right # Spinner character helpers (ASCII by default, overridable via env) mo_spinner_chars() { @@ -44,17 +44,17 @@ mo_spinner_chars() { # Logging configuration readonly LOG_FILE="${HOME}/.config/mole/mole.log" -readonly LOG_MAX_SIZE_DEFAULT=1048576 # 1MB +readonly LOG_MAX_SIZE_DEFAULT=1048576 # 1MB # Ensure log directory exists -mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true +mkdir -p "$(dirname "$LOG_FILE")" 2> /dev/null || true # Log file maintenance (must be defined before logging functions) rotate_log() { local max_size="${MOLE_MAX_LOG_SIZE:-$LOG_MAX_SIZE_DEFAULT}" - if [[ -f "$LOG_FILE" ]] && [[ $(stat -f%z "$LOG_FILE" 2>/dev/null || echo 0) -gt "$max_size" ]]; then - mv "$LOG_FILE" "${LOG_FILE}.old" 2>/dev/null || true - touch "$LOG_FILE" 2>/dev/null || true + if [[ -f "$LOG_FILE" ]] && [[ $(stat -f%z "$LOG_FILE" 2> /dev/null || echo 0) -gt "$max_size" ]]; then + mv "$LOG_FILE" "${LOG_FILE}.old" 2> /dev/null || true + touch "$LOG_FILE" 2> /dev/null || true fi } @@ -62,31 +62,31 @@ rotate_log() { log_info() { rotate_log echo -e "${BLUE}$1${NC}" - echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: $1" >> "$LOG_FILE" 2>/dev/null || true + echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: $1" >> "$LOG_FILE" 2> /dev/null || true } log_success() { rotate_log echo -e " ${GREEN}${ICON_SUCCESS}${NC} $1" - echo "[$(date '+%Y-%m-%d %H:%M:%S')] SUCCESS: $1" >> "$LOG_FILE" 2>/dev/null || true + echo "[$(date '+%Y-%m-%d %H:%M:%S')] SUCCESS: $1" >> "$LOG_FILE" 2> /dev/null || true } log_warning() { rotate_log echo -e "${YELLOW}$1${NC}" - echo "[$(date '+%Y-%m-%d %H:%M:%S')] WARNING: $1" >> "$LOG_FILE" 2>/dev/null || true + echo "[$(date '+%Y-%m-%d %H:%M:%S')] WARNING: $1" >> "$LOG_FILE" 2> /dev/null || true } log_error() { rotate_log echo -e "${RED}${ICON_ERROR}${NC} $1" >&2 - echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >> "$LOG_FILE" 2>/dev/null || true + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >> "$LOG_FILE" 2> /dev/null || true } log_header() { rotate_log echo -e "\n${PURPLE}${ICON_ARROW} $1${NC}" - echo "[$(date '+%Y-%m-%d %H:%M:%S')] SECTION: $1" >> "$LOG_FILE" 2>/dev/null || true + echo "[$(date '+%Y-%m-%d %H:%M:%S')] SECTION: $1" >> "$LOG_FILE" 2> /dev/null || true } # Icon output helpers @@ -192,24 +192,24 @@ read_key() { fi case "$key" in - $'\n'|$'\r') echo "ENTER" ;; + $'\n' | $'\r') echo "ENTER" ;; ' ') echo "SPACE" ;; - 'q'|'Q') echo "QUIT" ;; - 'a'|'A') echo "ALL" ;; - 'n'|'N') echo "NONE" ;; - 'd'|'D') echo "DELETE" ;; - 'r'|'R') echo "RETRY" ;; + 'q' | 'Q') echo "QUIT" ;; + 'a' | 'A') echo "ALL" ;; + 'n' | 'N') echo "NONE" ;; + 'd' | 'D') echo "DELETE" ;; + 'r' | 'R') echo "RETRY" ;; '?') echo "HELP" ;; - 'o'|'O') echo "OPEN" ;; - $'\x03') echo "QUIT" ;; # Ctrl+C - $'\x7f'|$'\x08') echo "DELETE" ;; # Delete key (labeled "delete" on Mac, actually backspace) + 'o' | 'O') echo "OPEN" ;; + $'\x03') echo "QUIT" ;; # Ctrl+C + $'\x7f' | $'\x08') echo "DELETE" ;; # Delete key (labeled "delete" on Mac, actually backspace) $'\x1b') # ESC sequence - could be arrow key, delete key, or ESC alone # Read the next two bytes within 1s - if IFS= read -r -s -n 1 -t 1 rest 2>/dev/null; then + if IFS= read -r -s -n 1 -t 1 rest 2> /dev/null; then if [[ "$rest" == "[" ]]; then # Got ESC [, read next character - if IFS= read -r -s -n 1 -t 1 rest2 2>/dev/null; then + if IFS= read -r -s -n 1 -t 1 rest2 2> /dev/null; then case "$rest2" in "A") echo "UP" ;; "B") echo "DOWN" ;; @@ -217,7 +217,7 @@ read_key() { "D") echo "LEFT" ;; "3") # Delete key (Fn+Delete): ESC [ 3 ~ - IFS= read -r -s -n 1 -t 1 rest3 2>/dev/null + IFS= read -r -s -n 1 -t 1 rest3 2> /dev/null if [[ "$rest3" == "~" ]]; then echo "DELETE" else @@ -226,21 +226,21 @@ read_key() { ;; "5") # Page Up key: ESC [ 5 ~ - IFS= read -r -s -n 1 -t 1 rest3 2>/dev/null + IFS= read -r -s -n 1 -t 1 rest3 2> /dev/null [[ "$rest3" == "~" ]] && echo "OTHER" || echo "OTHER" ;; "6") # Page Down key: ESC [ 6 ~ - IFS= read -r -s -n 1 -t 1 rest3 2>/dev/null + IFS= read -r -s -n 1 -t 1 rest3 2> /dev/null [[ "$rest3" == "~" ]] && echo "OTHER" || echo "OTHER" ;; *) echo "OTHER" ;; esac else - echo "QUIT" # ESC [ timeout + echo "QUIT" # ESC [ timeout fi else - echo "QUIT" # ESC + something else + echo "QUIT" # ESC + something else fi else # ESC pressed alone - treat as quit @@ -257,7 +257,7 @@ drain_pending_input() { local drained=0 # Single pass with reasonable timeout # Touchpad scrolling can generate bursts of arrow keys - while IFS= read -r -s -n 1 -t 0.001 dummy 2>/dev/null; do + while IFS= read -r -s -n 1 -t 0.001 dummy 2> /dev/null; do ((drained++)) # Safety limit to prevent infinite loop [[ $drained -gt 500 ]] && break @@ -293,7 +293,7 @@ get_human_size() { echo "N/A" return 1 fi - du -sh "$path" 2>/dev/null | cut -f1 || echo "N/A" + du -sh "$path" 2> /dev/null | cut -f1 || echo "N/A" } # Convert bytes to human readable format @@ -304,11 +304,11 @@ bytes_to_human() { return 1 fi - if ((bytes >= 1073741824)); then # >= 1GB + if ((bytes >= 1073741824)); then # >= 1GB local divisor=1073741824 local whole=$((bytes / divisor)) local remainder=$((bytes % divisor)) - local frac=$(( (remainder * 100 + divisor / 2) / divisor )) # Two decimals, rounded + local frac=$(((remainder * 100 + divisor / 2) / divisor)) # Two decimals, rounded if ((frac >= 100)); then frac=0 ((whole++)) @@ -317,11 +317,11 @@ bytes_to_human() { return 0 fi - if ((bytes >= 1048576)); then # >= 1MB + if ((bytes >= 1048576)); then # >= 1MB local divisor=1048576 local whole=$((bytes / divisor)) local remainder=$((bytes % divisor)) - local frac=$(( (remainder * 10 + divisor / 2) / divisor )) # One decimal, rounded + local frac=$(((remainder * 10 + divisor / 2) / divisor)) # One decimal, rounded if ((frac >= 10)); then frac=0 ((whole++)) @@ -330,8 +330,8 @@ bytes_to_human() { return 0 fi - if ((bytes >= 1024)); then # >= 1KB - local rounded_kb=$(((bytes + 512) / 1024)) # Nearest integer KB + if ((bytes >= 1024)); then # >= 1KB + local rounded_kb=$(((bytes + 512) / 1024)) # Nearest integer KB printf "%dKB\n" "$rounded_kb" return 0 fi @@ -346,13 +346,12 @@ get_directory_size_bytes() { echo "0" return 1 fi - du -sk "$path" 2>/dev/null | cut -f1 | awk '{print $1 * 1024}' || echo "0" + du -sk "$path" 2> /dev/null | cut -f1 | awk '{print $1 * 1024}' || echo "0" } - # Permission checks check_sudo() { - if ! sudo -n true 2>/dev/null; then + if ! sudo -n true 2> /dev/null; then return 1 fi return 0 @@ -361,7 +360,7 @@ check_sudo() { # Check if Touch ID is configured for sudo check_touchid_support() { if [[ -f /etc/pam.d/sudo ]]; then - grep -q "pam_tid.so" /etc/pam.d/sudo 2>/dev/null + grep -q "pam_tid.so" /etc/pam.d/sudo 2> /dev/null return $? fi return 1 @@ -374,14 +373,14 @@ request_sudo_access() { local force_password="${2:-false}" # Check if already has sudo access - if sudo -n true 2>/dev/null; then + if sudo -n true 2> /dev/null; then return 0 fi # If Touch ID is supported and not forced to use password if [[ "$force_password" != "true" ]] && check_touchid_support; then echo -e "${PURPLE}${ICON_ARROW}${NC} ${prompt_msg} ${GRAY}(Touch ID or password)${NC}" - if sudo -v 2>/dev/null; then + if sudo -v 2> /dev/null; then return 0 else return 1 @@ -392,7 +391,7 @@ request_sudo_access() { echo -ne "${PURPLE}${ICON_ARROW}${NC} Password: " read -s password echo "" - if [[ -n "$password" ]] && echo "$password" | sudo -S true 2>/dev/null; then + if [[ -n "$password" ]] && echo "$password" | sudo -S true 2> /dev/null; then return 0 else return 1 @@ -405,7 +404,7 @@ request_sudo() { echo -n "Please enter your password: " read -s password echo - if echo "$password" | sudo -S true 2>/dev/null; then + if echo "$password" | sudo -S true 2> /dev/null; then return 0 else log_error "Invalid password or cancelled" @@ -442,7 +441,7 @@ update_via_homebrew() { if echo "$upgrade_output" | grep -q "already installed"; then # Get current version local current_version - current_version=$(brew list --versions mole 2>/dev/null | awk '{print $2}') + current_version=$(brew list --versions mole 2> /dev/null | awk '{print $2}') echo -e "${GREEN}${ICON_SUCCESS}${NC} Already on latest version (${current_version:-$version})" elif echo "$upgrade_output" | grep -q "Error:"; then log_error "Homebrew upgrade failed" @@ -453,7 +452,7 @@ update_via_homebrew() { echo "$upgrade_output" | grep -Ev "^(==>|Updating Homebrew|Warning:)" || true # Get new version 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 -e "${GREEN}${ICON_SUCCESS}${NC} Updated to latest version (${new_version:-$version})" fi @@ -467,10 +466,6 @@ load_config() { MOLE_MAX_LOG_SIZE="${MOLE_MAX_LOG_SIZE:-1048576}" } - - - - # Initialize configuration on sourcing load_config @@ -510,7 +505,7 @@ start_spinner() { # Start an inline spinner (rotating character) start_inline_spinner() { - stop_inline_spinner 2>/dev/null || true + stop_inline_spinner 2> /dev/null || true local message="$1" if [[ -t 1 ]]; then @@ -522,14 +517,14 @@ start_inline_spinner() { local i=0 while true; do local c="${chars:$((i % ${#chars})):1}" - printf "\r${MOLE_SPINNER_PREFIX:-}${BLUE}%s${NC} %s" "$c" "$message" 2>/dev/null || exit 0 + printf "\r${MOLE_SPINNER_PREFIX:-}${BLUE}%s${NC} %s" "$c" "$message" 2> /dev/null || exit 0 ((i++)) # macOS supports decimal sleep, this is the primary target - sleep 0.1 2>/dev/null || sleep 1 2>/dev/null || exit 0 + sleep 0.1 2> /dev/null || sleep 1 2> /dev/null || exit 0 done ) & INLINE_SPINNER_PID=$! - disown 2>/dev/null || true + disown 2> /dev/null || true else echo -n " ${BLUE}|${NC} $message" fi @@ -538,8 +533,8 @@ start_inline_spinner() { # Stop inline spinner stop_inline_spinner() { if [[ -n "$INLINE_SPINNER_PID" ]]; then - kill "$INLINE_SPINNER_PID" 2>/dev/null || true - wait "$INLINE_SPINNER_PID" 2>/dev/null || true + kill "$INLINE_SPINNER_PID" 2> /dev/null || true + wait "$INLINE_SPINNER_PID" 2> /dev/null || true INLINE_SPINNER_PID="" [[ -t 1 ]] && printf "\r\033[K" fi @@ -552,8 +547,8 @@ stop_spinner() { stop_inline_spinner if [[ -n "$SPINNER_PID" ]]; then - kill "$SPINNER_PID" 2>/dev/null || true - wait "$SPINNER_PID" 2>/dev/null || true + kill "$SPINNER_PID" 2> /dev/null || true + wait "$SPINNER_PID" 2> /dev/null || true SPINNER_PID="" fi @@ -570,7 +565,6 @@ stop_spinner() { # User Interaction - Confirmation Dialogs # ============================================================================ - # ============================================================================ # Temporary File Management # ============================================================================ @@ -613,13 +607,13 @@ cleanup_temp_files() { local file if [[ ${#MOLE_TEMP_FILES[@]} -gt 0 ]]; then for file in "${MOLE_TEMP_FILES[@]}"; do - [[ -f "$file" ]] && rm -f "$file" 2>/dev/null || true + [[ -f "$file" ]] && rm -f "$file" 2> /dev/null || true done fi if [[ ${#MOLE_TEMP_DIRS[@]} -gt 0 ]]; then for file in "${MOLE_TEMP_DIRS[@]}"; do - [[ -d "$file" ]] && rm -rf "$file" 2>/dev/null || true + [[ -d "$file" ]] && rm -rf "$file" 2> /dev/null || true done fi @@ -657,16 +651,16 @@ parallel_execute() { pids+=($!) # Wait for a slot if we've hit max parallel jobs - if (( ${#pids[@]} >= max_jobs )); then - wait "${pids[0]}" 2>/dev/null || true + if ((${#pids[@]} >= max_jobs)); then + wait "${pids[0]}" 2> /dev/null || true pids=("${pids[@]:1}") fi done -# Wait for remaining background jobs - if (( ${#pids[@]} > 0 )); then + # Wait for remaining background jobs + if ((${#pids[@]} > 0)); then for pid in "${pids[@]}"; do - wait "$pid" 2>/dev/null || true + wait "$pid" 2> /dev/null || true done fi } @@ -677,17 +671,18 @@ parallel_execute() { # Usage: with_spinner "Message" cmd arg... # Set MOLE_SPINNER_PREFIX=" " for indented spinner (e.g., in clean context) with_spinner() { - local msg="$1"; shift || true - local timeout="${MOLE_CMD_TIMEOUT:-180}" # Default 3min timeout + local msg="$1" + shift || true + local timeout="${MOLE_CMD_TIMEOUT:-180}" # Default 3min timeout if [[ -t 1 ]]; then start_inline_spinner "$msg" fi # Run command with timeout protection - if command -v timeout >/dev/null 2>&1; then + if command -v timeout > /dev/null 2>&1; then # GNU timeout available - timeout "$timeout" "$@" >/dev/null 2>&1 || { + timeout "$timeout" "$@" > /dev/null 2>&1 || { local exit_code=$? if [[ -t 1 ]]; then stop_inline_spinner; fi # Exit code 124 means timeout @@ -696,13 +691,13 @@ with_spinner() { } else # Fallback: run in background with manual timeout - "$@" >/dev/null 2>&1 & + "$@" > /dev/null 2>&1 & local cmd_pid=$! local elapsed=0 - while kill -0 $cmd_pid 2>/dev/null; do + while kill -0 $cmd_pid 2> /dev/null; do if [[ $elapsed -ge $timeout ]]; then - kill -TERM $cmd_pid 2>/dev/null || true - wait $cmd_pid 2>/dev/null || true + kill -TERM $cmd_pid 2> /dev/null || true + wait $cmd_pid 2> /dev/null || true if [[ -t 1 ]]; then stop_inline_spinner; fi echo -e " ${YELLOW}${ICON_WARNING}${NC} $msg timed out (skipped)" >&2 return 124 @@ -710,7 +705,7 @@ with_spinner() { sleep 1 ((elapsed++)) done - wait $cmd_pid 2>/dev/null || { + wait $cmd_pid 2> /dev/null || { local exit_code=$? if [[ -t 1 ]]; then stop_inline_spinner; fi return $exit_code @@ -727,7 +722,8 @@ with_spinner() { # ============================================================================ # clean_tool_cache "Label" command... clean_tool_cache() { - local label="$1"; shift || true + local label="$1" + shift || true if [[ "$DRY_RUN" == "true" ]]; then echo -e " ${YELLOW}→${NC} $label (would clean)" return 0 @@ -741,7 +737,7 @@ clean_tool_cache() { echo -e " ${YELLOW}${ICON_WARNING}${NC} $label failed (skipped)" >&2 fi fi - return 0 # Always return success to continue cleanup + return 0 # Always return success to continue cleanup } # ============================================================================ @@ -760,12 +756,12 @@ prompt_action() { IFS= read -r -s -n1 key || key="" case "$key" in - $'\e') # ESC + $'\e') # ESC echo "" return 1 ;; - ""|$'\n'|$'\r') # Enter - printf "\r\033[K" # Clear the prompt line + "" | $'\n' | $'\r') # Enter + printf "\r\033[K" # Clear the prompt line return 0 ;; *) @@ -782,21 +778,30 @@ confirm_prompt() { echo -n "$message (Enter=OK / ESC q=Cancel): " IFS= read -r -s -n1 _key || _key="" case "$_key" in - $'\e'|q|Q) echo ""; return 1 ;; - ""|$'\n'|$'\r'|y|Y) echo ""; return 0 ;; - *) echo ""; return 1 ;; + $'\e' | q | Q) + echo "" + return 1 + ;; + "" | $'\n' | $'\r' | y | Y) + echo "" + return 0 + ;; + *) + echo "" + return 1 + ;; esac } - # Get optimal parallel job count based on CPU cores # ========================================================================= # Size helpers # ========================================================================= -bytes_to_human_kb() { bytes_to_human "$(( ${1:-0} * 1024 ))"; } +bytes_to_human_kb() { bytes_to_human "$((${1:-0} * 1024))"; } print_space_stat() { - local freed_kb="$1"; shift || true + local freed_kb="$1" + shift || true local current_free current_free=$(get_free_space) local human @@ -808,10 +813,20 @@ print_space_stat() { # mktemp unification wrappers (register access) # ========================================================================= register_temp_file() { MOLE_TEMP_FILES+=("$1"); } -register_temp_dir() { MOLE_TEMP_DIRS+=("$1"); } +register_temp_dir() { MOLE_TEMP_DIRS+=("$1"); } -mktemp_file() { local f; f=$(mktemp) || return 1; register_temp_file "$f"; echo "$f"; } -mktemp_dir() { local d; d=$(mktemp -d) || return 1; register_temp_dir "$d"; echo "$d"; } +mktemp_file() { + local f + f=$(mktemp) || return 1 + register_temp_file "$f" + echo "$f" +} +mktemp_dir() { + local d + d=$(mktemp -d) || return 1 + register_temp_dir "$d" + echo "$d" +} # ========================================================================= # Uninstall helper abstractions @@ -828,15 +843,15 @@ force_kill_app() { match_pattern="$app_path" fi - if pgrep -f "$match_pattern" >/dev/null 2>&1; then - pkill -f "$match_pattern" 2>/dev/null || true + if pgrep -f "$match_pattern" > /dev/null 2>&1; then + pkill -f "$match_pattern" 2> /dev/null || true sleep 1 fi - if pgrep -f "$match_pattern" >/dev/null 2>&1; then - pkill -9 -f "$match_pattern" 2>/dev/null || true + if pgrep -f "$match_pattern" > /dev/null 2>&1; then + pkill -9 -f "$match_pattern" 2> /dev/null || true sleep 1 fi - pgrep -f "$match_pattern" >/dev/null 2>&1 && return 1 || return 0 + pgrep -f "$match_pattern" > /dev/null 2>&1 && return 1 || return 0 } # Remove application icons from the Dock (best effort) @@ -848,7 +863,7 @@ remove_apps_from_dock() { local plist="$HOME/Library/Preferences/com.apple.dock.plist" [[ -f "$plist" ]] || return 0 - if ! command -v python3 >/dev/null 2>&1; then + if ! command -v python3 > /dev/null 2>&1; then return 0 fi @@ -856,7 +871,7 @@ remove_apps_from_dock() { # Exit status 2 means entries were removed. local target_count=$# - python3 - "$@" <<'PY' + python3 - "$@" << 'PY' import os import plistlib import subprocess @@ -955,7 +970,8 @@ map_uninstall_reason() { batch_safe_clean() { # Usage: batch_safe_clean "Label" path1 path2 ... - local label="$1"; shift || true + local label="$1" + shift || true local -a paths=("$@") if [[ ${#paths[@]} -eq 0 ]]; then return 0; fi safe_clean "${paths[@]}" "$label" @@ -965,9 +981,9 @@ batch_safe_clean() { get_optimal_parallel_jobs() { local operation_type="${1:-default}" local cpu_cores - cpu_cores=$(sysctl -n hw.ncpu 2>/dev/null || echo 4) + cpu_cores=$(sysctl -n hw.ncpu 2> /dev/null || echo 4) case "$operation_type" in - scan|io) + scan | io) echo $((cpu_cores * 2)) ;; compute) @@ -989,7 +1005,7 @@ start_sudo_keepalive() { ( local retry_count=0 while true; do - if ! sudo -n true 2>/dev/null; then + if ! sudo -n true 2> /dev/null; then ((retry_count++)) if [[ $retry_count -ge 3 ]]; then exit 1 @@ -999,9 +1015,9 @@ start_sudo_keepalive() { fi retry_count=0 sleep 30 - kill -0 "$$" 2>/dev/null || exit + kill -0 "$$" 2> /dev/null || exit done - ) 2>/dev/null & + ) 2> /dev/null & echo $! } @@ -1010,8 +1026,8 @@ start_sudo_keepalive() { stop_sudo_keepalive() { local pid="${1:-}" if [[ -n "$pid" ]]; then - kill "$pid" 2>/dev/null || true - wait "$pid" 2>/dev/null || true + kill "$pid" 2> /dev/null || true + wait "$pid" 2> /dev/null || true fi } @@ -1052,7 +1068,7 @@ note_activity() { # System critical components that should NEVER be uninstalled readonly SYSTEM_CRITICAL_BUNDLES=( - "com.apple.*" # System essentials + "com.apple.*" # System essentials "loginwindow" "dock" "systempreferences" @@ -1099,289 +1115,288 @@ readonly DATA_PROTECTED_BUNDLES=( # ============================================================================ # System Utilities & Cleanup Tools # ============================================================================ - "com.nektony.*" # App Cleaner & Uninstaller - "com.macpaw.*" # CleanMyMac, CleanMaster - "com.freemacsoft.AppCleaner" # AppCleaner - "com.omnigroup.omnidisksweeper" # OmniDiskSweeper - "com.daisydiskapp.*" # DaisyDisk - "com.tunabellysoftware.*" # Disk Utility apps - "com.grandperspectiv.*" # GrandPerspective - "com.binaryfruit.*" # FusionCast + "com.nektony.*" # App Cleaner & Uninstaller + "com.macpaw.*" # CleanMyMac, CleanMaster + "com.freemacsoft.AppCleaner" # AppCleaner + "com.omnigroup.omnidisksweeper" # OmniDiskSweeper + "com.daisydiskapp.*" # DaisyDisk + "com.tunabellysoftware.*" # Disk Utility apps + "com.grandperspectiv.*" # GrandPerspective + "com.binaryfruit.*" # FusionCast # ============================================================================ # Password Managers & Security # ============================================================================ - "com.1password.*" # 1Password - "com.agilebits.*" # 1Password legacy - "com.lastpass.*" # LastPass - "com.dashlane.*" # Dashlane - "com.bitwarden.*" # Bitwarden - "com.keepassx.*" # KeePassXC - "org.keepassx.*" # KeePassX - "com.authy.*" # Authy - "com.yubico.*" # YubiKey Manager + "com.1password.*" # 1Password + "com.agilebits.*" # 1Password legacy + "com.lastpass.*" # LastPass + "com.dashlane.*" # Dashlane + "com.bitwarden.*" # Bitwarden + "com.keepassx.*" # KeePassXC + "org.keepassx.*" # KeePassX + "com.authy.*" # Authy + "com.yubico.*" # YubiKey Manager # ============================================================================ # Development Tools - IDEs & Editors # ============================================================================ - "com.jetbrains.*" # JetBrains IDEs (IntelliJ, DataGrip, etc.) - "JetBrains*" # JetBrains Application Support folders - "com.microsoft.VSCode" # Visual Studio Code - "com.visualstudio.code.*" # VS Code variants - "com.sublimetext.*" # Sublime Text - "com.sublimehq.*" # Sublime Merge - "com.microsoft.VSCodeInsiders" # VS Code Insiders - "com.apple.dt.Xcode" # Xcode (keep settings) - "com.coteditor.CotEditor" # CotEditor - "com.macromates.TextMate" # TextMate - "com.panic.Nova" # Nova - "abnerworks.Typora" # Typora (Markdown editor) - "com.uranusjr.macdown" # MacDown + "com.jetbrains.*" # JetBrains IDEs (IntelliJ, DataGrip, etc.) + "JetBrains*" # JetBrains Application Support folders + "com.microsoft.VSCode" # Visual Studio Code + "com.visualstudio.code.*" # VS Code variants + "com.sublimetext.*" # Sublime Text + "com.sublimehq.*" # Sublime Merge + "com.microsoft.VSCodeInsiders" # VS Code Insiders + "com.apple.dt.Xcode" # Xcode (keep settings) + "com.coteditor.CotEditor" # CotEditor + "com.macromates.TextMate" # TextMate + "com.panic.Nova" # Nova + "abnerworks.Typora" # Typora (Markdown editor) + "com.uranusjr.macdown" # MacDown # ============================================================================ # Development Tools - Database Clients # ============================================================================ - "com.sequelpro.*" # Sequel Pro - "com.sequel-ace.*" # Sequel Ace - "com.tinyapp.*" # TablePlus - "com.dbeaver.*" # DBeaver - "com.navicat.*" # Navicat - "com.mongodb.compass" # MongoDB Compass - "com.redis.RedisInsight" # Redis Insight - "com.pgadmin.pgadmin4" # pgAdmin - "com.eggerapps.Sequel-Pro" # Sequel Pro legacy + "com.sequelpro.*" # Sequel Pro + "com.sequel-ace.*" # Sequel Ace + "com.tinyapp.*" # TablePlus + "com.dbeaver.*" # DBeaver + "com.navicat.*" # Navicat + "com.mongodb.compass" # MongoDB Compass + "com.redis.RedisInsight" # Redis Insight + "com.pgadmin.pgadmin4" # pgAdmin + "com.eggerapps.Sequel-Pro" # Sequel Pro legacy "com.valentina-db.Valentina-Studio" # Valentina Studio - "com.dbvis.DbVisualizer" # DbVisualizer + "com.dbvis.DbVisualizer" # DbVisualizer # ============================================================================ # Development Tools - API & Network # ============================================================================ - "com.postmanlabs.mac" # Postman - "com.konghq.insomnia" # Insomnia - "com.CharlesProxy.*" # Charles Proxy - "com.proxyman.*" # Proxyman - "com.getpaw.*" # Paw - "com.luckymarmot.Paw" # Paw legacy - "com.charlesproxy.charles" # Charles - "com.telerik.Fiddler" # Fiddler - "com.usebruno.app" # Bruno (API client) + "com.postmanlabs.mac" # Postman + "com.konghq.insomnia" # Insomnia + "com.CharlesProxy.*" # Charles Proxy + "com.proxyman.*" # Proxyman + "com.getpaw.*" # Paw + "com.luckymarmot.Paw" # Paw legacy + "com.charlesproxy.charles" # Charles + "com.telerik.Fiddler" # Fiddler + "com.usebruno.app" # Bruno (API client) # ============================================================================ # Development Tools - Git & Version Control # ============================================================================ - "com.github.GitHubDesktop" # GitHub Desktop - "com.sublimemerge" # Sublime Merge - "com.torusknot.SourceTreeNotMAS" # SourceTree - "com.git-tower.Tower*" # Tower - "com.gitfox.GitFox" # GitFox - "com.github.Gitify" # Gitify - "com.fork.Fork" # Fork - "com.axosoft.gitkraken" # GitKraken + "com.github.GitHubDesktop" # GitHub Desktop + "com.sublimemerge" # Sublime Merge + "com.torusknot.SourceTreeNotMAS" # SourceTree + "com.git-tower.Tower*" # Tower + "com.gitfox.GitFox" # GitFox + "com.github.Gitify" # Gitify + "com.fork.Fork" # Fork + "com.axosoft.gitkraken" # GitKraken # ============================================================================ # Development Tools - Terminal & Shell # ============================================================================ - "com.googlecode.iterm2" # iTerm2 - "net.kovidgoyal.kitty" # Kitty - "io.alacritty" # Alacritty - "com.github.wez.wezterm" # WezTerm - "com.hyper.Hyper" # Hyper - "com.mizage.divvy" # Divvy - "com.fig.Fig" # Fig (terminal assistant) - "dev.warp.Warp-Stable" # Warp - "com.termius-dmg" # Termius (SSH client) + "com.googlecode.iterm2" # iTerm2 + "net.kovidgoyal.kitty" # Kitty + "io.alacritty" # Alacritty + "com.github.wez.wezterm" # WezTerm + "com.hyper.Hyper" # Hyper + "com.mizage.divvy" # Divvy + "com.fig.Fig" # Fig (terminal assistant) + "dev.warp.Warp-Stable" # Warp + "com.termius-dmg" # Termius (SSH client) # ============================================================================ # Development Tools - Docker & Virtualization # ============================================================================ - "com.docker.docker" # Docker Desktop - "com.getutm.UTM" # UTM - "com.vmware.fusion" # VMware Fusion - "com.parallels.desktop.*" # Parallels Desktop - "org.virtualbox.app.VirtualBox" # VirtualBox - "com.vagrant.*" # Vagrant - "com.orbstack.OrbStack" # OrbStack + "com.docker.docker" # Docker Desktop + "com.getutm.UTM" # UTM + "com.vmware.fusion" # VMware Fusion + "com.parallels.desktop.*" # Parallels Desktop + "org.virtualbox.app.VirtualBox" # VirtualBox + "com.vagrant.*" # Vagrant + "com.orbstack.OrbStack" # OrbStack # ============================================================================ # System Monitoring & Performance # ============================================================================ - "com.bjango.istatmenus*" # iStat Menus - "eu.exelban.Stats" # Stats - "com.monitorcontrol.*" # MonitorControl - "com.bresink.system-toolkit.*" # TinkerTool System - "com.mediaatelier.MenuMeters" # MenuMeters - "com.activity-indicator.app" # Activity Indicator - "net.cindori.sensei" # Sensei + "com.bjango.istatmenus*" # iStat Menus + "eu.exelban.Stats" # Stats + "com.monitorcontrol.*" # MonitorControl + "com.bresink.system-toolkit.*" # TinkerTool System + "com.mediaatelier.MenuMeters" # MenuMeters + "com.activity-indicator.app" # Activity Indicator + "net.cindori.sensei" # Sensei # ============================================================================ # Window Management & Productivity # ============================================================================ - "com.macitbetter.*" # BetterTouchTool, BetterSnapTool - "com.hegenberg.*" # BetterTouchTool legacy - "com.manytricks.*" # Moom, Witch, Name Mangler, Resolutionator - "com.divisiblebyzero.*" # Spectacle - "com.koingdev.*" # Koingg apps - "com.if.Amphetamine" # Amphetamine - "com.lwouis.alt-tab-macos" # AltTab - "net.matthewpalmer.Vanilla" # Vanilla - "com.lightheadsw.Caffeine" # Caffeine - "com.contextual.Contexts" # Contexts - "com.amethyst.Amethyst" # Amethyst - "com.knollsoft.Rectangle" # Rectangle - "com.knollsoft.Hookshot" # Hookshot - "com.surteesstudios.Bartender" # Bartender - "com.gaosun.eul" # eul (system monitor) - "com.pointum.hazeover" # HazeOver + "com.macitbetter.*" # BetterTouchTool, BetterSnapTool + "com.hegenberg.*" # BetterTouchTool legacy + "com.manytricks.*" # Moom, Witch, Name Mangler, Resolutionator + "com.divisiblebyzero.*" # Spectacle + "com.koingdev.*" # Koingg apps + "com.if.Amphetamine" # Amphetamine + "com.lwouis.alt-tab-macos" # AltTab + "net.matthewpalmer.Vanilla" # Vanilla + "com.lightheadsw.Caffeine" # Caffeine + "com.contextual.Contexts" # Contexts + "com.amethyst.Amethyst" # Amethyst + "com.knollsoft.Rectangle" # Rectangle + "com.knollsoft.Hookshot" # Hookshot + "com.surteesstudios.Bartender" # Bartender + "com.gaosun.eul" # eul (system monitor) + "com.pointum.hazeover" # HazeOver # ============================================================================ # Launcher & Automation # ============================================================================ - "com.runningwithcrayons.Alfred" # Alfred - "com.raycast.macos" # Raycast - "com.blacktree.Quicksilver" # Quicksilver - "com.stairways.keyboardmaestro.*" # Keyboard Maestro - "com.manytricks.Butler" # Butler - "com.happenapps.Quitter" # Quitter - "com.pilotmoon.scroll-reverser" # Scroll Reverser - "org.pqrs.Karabiner-Elements" # Karabiner-Elements - "com.apple.Automator" # Automator (system, but keep user workflows) + "com.runningwithcrayons.Alfred" # Alfred + "com.raycast.macos" # Raycast + "com.blacktree.Quicksilver" # Quicksilver + "com.stairways.keyboardmaestro.*" # Keyboard Maestro + "com.manytricks.Butler" # Butler + "com.happenapps.Quitter" # Quitter + "com.pilotmoon.scroll-reverser" # Scroll Reverser + "org.pqrs.Karabiner-Elements" # Karabiner-Elements + "com.apple.Automator" # Automator (system, but keep user workflows) # ============================================================================ # Note-Taking & Documentation # ============================================================================ - "com.bear-writer.*" # Bear - "com.typora.*" # Typora - "com.ulyssesapp.*" # Ulysses - "com.literatureandlatte.*" # Scrivener - "com.dayoneapp.*" # Day One - "notion.id" # Notion - "md.obsidian" # Obsidian - "com.logseq.logseq" # Logseq - "com.evernote.Evernote" # Evernote - "com.onenote.mac" # OneNote - "com.omnigroup.OmniOutliner*" # OmniOutliner - "net.shinyfrog.bear" # Bear legacy - "com.goodnotes.GoodNotes" # GoodNotes - "com.marginnote.MarginNote*" # MarginNote - "com.roamresearch.*" # Roam Research - "com.reflect.ReflectApp" # Reflect - "com.inkdrop.*" # Inkdrop + "com.bear-writer.*" # Bear + "com.typora.*" # Typora + "com.ulyssesapp.*" # Ulysses + "com.literatureandlatte.*" # Scrivener + "com.dayoneapp.*" # Day One + "notion.id" # Notion + "md.obsidian" # Obsidian + "com.logseq.logseq" # Logseq + "com.evernote.Evernote" # Evernote + "com.onenote.mac" # OneNote + "com.omnigroup.OmniOutliner*" # OmniOutliner + "net.shinyfrog.bear" # Bear legacy + "com.goodnotes.GoodNotes" # GoodNotes + "com.marginnote.MarginNote*" # MarginNote + "com.roamresearch.*" # Roam Research + "com.reflect.ReflectApp" # Reflect + "com.inkdrop.*" # Inkdrop # ============================================================================ # Design & Creative Tools # ============================================================================ - "com.adobe.*" # Adobe Creative Suite - "com.bohemiancoding.*" # Sketch - "com.figma.*" # Figma - "com.framerx.*" # Framer - "com.zeplin.*" # Zeplin - "com.invisionapp.*" # InVision - "com.principle.*" # Principle - "com.pixelmatorteam.*" # Pixelmator - "com.affinitydesigner.*" # Affinity Designer - "com.affinityphoto.*" # Affinity Photo - "com.affinitypublisher.*" # Affinity Publisher - "com.linearity.curve" # Linearity Curve - "com.canva.CanvaDesktop" # Canva - "com.maxon.cinema4d" # Cinema 4D - "com.autodesk.*" # Autodesk products - "com.sketchup.*" # SketchUp + "com.adobe.*" # Adobe Creative Suite + "com.bohemiancoding.*" # Sketch + "com.figma.*" # Figma + "com.framerx.*" # Framer + "com.zeplin.*" # Zeplin + "com.invisionapp.*" # InVision + "com.principle.*" # Principle + "com.pixelmatorteam.*" # Pixelmator + "com.affinitydesigner.*" # Affinity Designer + "com.affinityphoto.*" # Affinity Photo + "com.affinitypublisher.*" # Affinity Publisher + "com.linearity.curve" # Linearity Curve + "com.canva.CanvaDesktop" # Canva + "com.maxon.cinema4d" # Cinema 4D + "com.autodesk.*" # Autodesk products + "com.sketchup.*" # SketchUp # ============================================================================ # Communication & Collaboration # ============================================================================ - "com.tencent.xinWeChat" # WeChat (Chinese users) - "com.tencent.qq" # QQ - "com.alibaba.DingTalkMac" # DingTalk - "com.alibaba.AliLang.osx" # AliLang (retain login/config data) - "com.alibaba.alilang3.osx.ShipIt" # AliLang updater component + "com.tencent.xinWeChat" # WeChat (Chinese users) + "com.tencent.qq" # QQ + "com.alibaba.DingTalkMac" # DingTalk + "com.alibaba.AliLang.osx" # AliLang (retain login/config data) + "com.alibaba.alilang3.osx.ShipIt" # AliLang updater component "com.alibaba.AlilangMgr.QueryNetworkInfo" # AliLang network helper - "us.zoom.xos" # Zoom - "com.microsoft.teams*" # Microsoft Teams - "com.slack.Slack" # Slack - "com.hnc.Discord" # Discord - "org.telegram.desktop" # Telegram - "ru.keepcoder.Telegram" # Telegram legacy - "net.whatsapp.WhatsApp" # WhatsApp - "com.skype.skype" # Skype - "com.cisco.webexmeetings" # Webex - "com.ringcentral.RingCentral" # RingCentral - "com.readdle.smartemail-Mac" # Spark Email - "com.airmail.*" # Airmail - "com.postbox-inc.postbox" # Postbox - "com.tinyspeck.slackmacgap" # Slack legacy + "us.zoom.xos" # Zoom + "com.microsoft.teams*" # Microsoft Teams + "com.slack.Slack" # Slack + "com.hnc.Discord" # Discord + "org.telegram.desktop" # Telegram + "ru.keepcoder.Telegram" # Telegram legacy + "net.whatsapp.WhatsApp" # WhatsApp + "com.skype.skype" # Skype + "com.cisco.webexmeetings" # Webex + "com.ringcentral.RingCentral" # RingCentral + "com.readdle.smartemail-Mac" # Spark Email + "com.airmail.*" # Airmail + "com.postbox-inc.postbox" # Postbox + "com.tinyspeck.slackmacgap" # Slack legacy # ============================================================================ # Task Management & Productivity # ============================================================================ - "com.omnigroup.OmniFocus*" # OmniFocus - "com.culturedcode.*" # Things - "com.todoist.*" # Todoist - "com.any.do.*" # Any.do - "com.ticktick.*" # TickTick - "com.microsoft.to-do" # Microsoft To Do - "com.trello.trello" # Trello - "com.asana.nativeapp" # Asana - "com.clickup.*" # ClickUp - "com.monday.desktop" # Monday.com - "com.airtable.airtable" # Airtable - "com.notion.id" # Notion (also note-taking) - "com.linear.linear" # Linear + "com.omnigroup.OmniFocus*" # OmniFocus + "com.culturedcode.*" # Things + "com.todoist.*" # Todoist + "com.any.do.*" # Any.do + "com.ticktick.*" # TickTick + "com.microsoft.to-do" # Microsoft To Do + "com.trello.trello" # Trello + "com.asana.nativeapp" # Asana + "com.clickup.*" # ClickUp + "com.monday.desktop" # Monday.com + "com.airtable.airtable" # Airtable + "com.notion.id" # Notion (also note-taking) + "com.linear.linear" # Linear # ============================================================================ # File Transfer & Sync # ============================================================================ - "com.panic.transmit*" # Transmit (FTP/SFTP) - "com.binarynights.ForkLift*" # ForkLift - "com.noodlesoft.Hazel" # Hazel - "com.cyberduck.Cyberduck" # Cyberduck - "io.filezilla.FileZilla" # FileZilla - "com.apple.Xcode.CloudDocuments" # Xcode Cloud Documents - "com.synology.*" # Synology apps + "com.panic.transmit*" # Transmit (FTP/SFTP) + "com.binarynights.ForkLift*" # ForkLift + "com.noodlesoft.Hazel" # Hazel + "com.cyberduck.Cyberduck" # Cyberduck + "io.filezilla.FileZilla" # FileZilla + "com.apple.Xcode.CloudDocuments" # Xcode Cloud Documents + "com.synology.*" # Synology apps # ============================================================================ # Screenshot & Recording # ============================================================================ - "com.cleanshot.*" # CleanShot X - "com.xnipapp.xnip" # Xnip - "com.reincubate.camo" # Camo + "com.cleanshot.*" # CleanShot X + "com.xnipapp.xnip" # Xnip + "com.reincubate.camo" # Camo "com.tunabellysoftware.ScreenFloat" # ScreenFloat - "net.telestream.screenflow*" # ScreenFlow - "com.techsmith.snagit*" # Snagit - "com.techsmith.camtasia*" # Camtasia - "com.obsidianapp.screenrecorder" # Screen Recorder - "com.kap.Kap" # Kap - "com.getkap.*" # Kap legacy - "com.linebreak.CloudApp" # CloudApp - "com.droplr.droplr-mac" # Droplr + "net.telestream.screenflow*" # ScreenFlow + "com.techsmith.snagit*" # Snagit + "com.techsmith.camtasia*" # Camtasia + "com.obsidianapp.screenrecorder" # Screen Recorder + "com.kap.Kap" # Kap + "com.getkap.*" # Kap legacy + "com.linebreak.CloudApp" # CloudApp + "com.droplr.droplr-mac" # Droplr # ============================================================================ # Media & Entertainment # ============================================================================ - "com.spotify.client" # Spotify - "com.apple.Music" # Apple Music - "com.apple.podcasts" # Apple Podcasts - "com.apple.FinalCutPro" # Final Cut Pro - "com.apple.Motion" # Motion - "com.apple.Compressor" # Compressor - "com.blackmagic-design.*" # DaVinci Resolve - "com.colliderli.iina" # IINA - "org.videolan.vlc" # VLC - "io.mpv" # MPV - "com.noodlesoft.Hazel" # Hazel (automation) - "tv.plex.player.desktop" # Plex - "com.netease.163music" # NetEase Music + "com.spotify.client" # Spotify + "com.apple.Music" # Apple Music + "com.apple.podcasts" # Apple Podcasts + "com.apple.FinalCutPro" # Final Cut Pro + "com.apple.Motion" # Motion + "com.apple.Compressor" # Compressor + "com.blackmagic-design.*" # DaVinci Resolve + "com.colliderli.iina" # IINA + "org.videolan.vlc" # VLC + "io.mpv" # MPV + "com.noodlesoft.Hazel" # Hazel (automation) + "tv.plex.player.desktop" # Plex + "com.netease.163music" # NetEase Music # ============================================================================ # License Management & App Stores # ============================================================================ - "com.paddle.Paddle*" # Paddle (license management) - "com.setapp.DesktopClient" # Setapp - "com.devmate.*" # DevMate (license framework) - "org.sparkle-project.Sparkle" # Sparkle (update framework) + "com.paddle.Paddle*" # Paddle (license management) + "com.setapp.DesktopClient" # Setapp + "com.devmate.*" # DevMate (license framework) + "org.sparkle-project.Sparkle" # Sparkle (update framework) ) - # Legacy function - preserved for backward compatibility # Use should_protect_from_uninstall() or should_protect_data() instead readonly PRESERVED_BUNDLE_PATTERNS=("${SYSTEM_CRITICAL_BUNDLES[@]}" "${DATA_PROTECTED_BUNDLES[@]}") @@ -1390,7 +1405,7 @@ should_preserve_bundle() { for pattern in "${PRESERVED_BUNDLE_PATTERNS[@]}"; do # Use case for safer glob matching case "$bundle_id" in - $pattern) return 0 ;; + "$pattern") return 0 ;; esac done return 1 @@ -1402,7 +1417,7 @@ should_protect_from_uninstall() { for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}"; do # Use case for safer glob matching case "$bundle_id" in - $pattern) return 0 ;; + "$pattern") return 0 ;; esac done return 1 @@ -1415,7 +1430,7 @@ should_protect_data() { for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}" "${DATA_PROTECTED_BUNDLES[@]}"; do # Use case for safer glob matching case "$bundle_id" in - $pattern) return 0 ;; + "$pattern") return 0 ;; esac done return 1 @@ -1443,7 +1458,7 @@ find_app_files() { [[ -f ~/Library/Preferences/"$bundle_id".plist ]] && files_to_clean+=("$HOME/Library/Preferences/$bundle_id.plist") while IFS= read -r -d '' pref; do files_to_clean+=("$pref") - done < <(find ~/Library/Preferences/ByHost \( -name "$bundle_id*.plist" \) -print0 2>/dev/null) + done < <(find ~/Library/Preferences/ByHost \( -name "$bundle_id*.plist" \) -print0 2> /dev/null) # Logs [[ -d ~/Library/Logs/"$app_name" ]] && files_to_clean+=("$HOME/Library/Logs/$app_name") @@ -1458,7 +1473,7 @@ find_app_files() { # Group Containers while IFS= read -r -d '' container; do files_to_clean+=("$container") - done < <(find ~/Library/Group\ Containers -type d \( -name "*$bundle_id*" \) -print0 2>/dev/null) + done < <(find ~/Library/Group\ Containers -type d \( -name "*$bundle_id*" \) -print0 2> /dev/null) # WebKit data [[ -d ~/Library/WebKit/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/WebKit/$bundle_id") @@ -1482,7 +1497,7 @@ find_app_files() { # Internet Plug-Ins while IFS= read -r -d '' plugin; do files_to_clean+=("$plugin") - done < <(find ~/Library/Internet\ Plug-Ins \( -name "$bundle_id*" -o -name "$app_name*" \) -print0 2>/dev/null) + done < <(find ~/Library/Internet\ Plug-Ins \( -name "$bundle_id*" -o -name "$app_name*" \) -print0 2> /dev/null) # QuickLook Plugins [[ -d ~/Library/QuickLook/"$app_name".qlgenerator ]] && files_to_clean+=("$HOME/Library/QuickLook/$app_name.qlgenerator") @@ -1499,7 +1514,7 @@ find_app_files() { # CoreData while IFS= read -r -d '' coredata; do files_to_clean+=("$coredata") - done < <(find ~/Library/CoreData \( -name "*$bundle_id*" -o -name "*$app_name*" \) -print0 2>/dev/null) + done < <(find ~/Library/CoreData \( -name "*$bundle_id*" -o -name "*$app_name*" \) -print0 2> /dev/null) # Autosave Information [[ -d ~/Library/Autosave\ Information/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Autosave Information/$bundle_id") @@ -1510,7 +1525,7 @@ find_app_files() { # Receipts (user-level) while IFS= read -r -d '' receipt; do files_to_clean+=("$receipt") - done < <(find ~/Library/Receipts \( -name "*$bundle_id*" -o -name "*$app_name*" \) -print0 2>/dev/null) + done < <(find ~/Library/Receipts \( -name "*$bundle_id*" -o -name "*$app_name*" \) -print0 2> /dev/null) # Spotlight Plugins [[ -d ~/Library/Spotlight/"$app_name".mdimporter ]] && files_to_clean+=("$HOME/Library/Spotlight/$app_name.mdimporter") @@ -1518,7 +1533,7 @@ find_app_files() { # Scripting Additions while IFS= read -r -d '' scripting; do files_to_clean+=("$scripting") - done < <(find ~/Library/ScriptingAdditions \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2>/dev/null) + done < <(find ~/Library/ScriptingAdditions \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) # Color Pickers [[ -d ~/Library/ColorPickers/"$app_name".colorPicker ]] && files_to_clean+=("$HOME/Library/ColorPickers/$app_name.colorPicker") @@ -1526,37 +1541,37 @@ find_app_files() { # Quartz Compositions while IFS= read -r -d '' composition; do files_to_clean+=("$composition") - done < <(find ~/Library/Compositions \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2>/dev/null) + done < <(find ~/Library/Compositions \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) # Address Book Plug-Ins while IFS= read -r -d '' plugin; do files_to_clean+=("$plugin") - done < <(find ~/Library/Address\ Book\ Plug-Ins \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2>/dev/null) + done < <(find ~/Library/Address\ Book\ Plug-Ins \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) # Mail Bundles while IFS= read -r -d '' bundle; do files_to_clean+=("$bundle") - done < <(find ~/Library/Mail/Bundles \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2>/dev/null) + done < <(find ~/Library/Mail/Bundles \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) # Input Managers (app-specific only) while IFS= read -r -d '' manager; do files_to_clean+=("$manager") - done < <(find ~/Library/InputManagers \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2>/dev/null) + done < <(find ~/Library/InputManagers \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) # Custom Sounds while IFS= read -r -d '' sound; do files_to_clean+=("$sound") - done < <(find ~/Library/Sounds \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2>/dev/null) + done < <(find ~/Library/Sounds \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) # Plugins while IFS= read -r -d '' plugin; do files_to_clean+=("$plugin") - done < <(find ~/Library/Plugins \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2>/dev/null) + done < <(find ~/Library/Plugins \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) # Private Frameworks while IFS= read -r -d '' framework; do files_to_clean+=("$framework") - done < <(find ~/Library/PrivateFrameworks \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2>/dev/null) + done < <(find ~/Library/PrivateFrameworks \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) # Only print if array has elements to avoid unbound variable error if [[ ${#files_to_clean[@]} -gt 0 ]]; then @@ -1583,7 +1598,7 @@ find_app_system_files() { # Privileged Helper Tools while IFS= read -r -d '' helper; do system_files+=("$helper") - done < <(find /Library/PrivilegedHelperTools \( -name "$bundle_id*" \) -print0 2>/dev/null) + done < <(find /Library/PrivilegedHelperTools \( -name "$bundle_id*" \) -print0 2> /dev/null) # System Preferences [[ -f /Library/Preferences/"$bundle_id".plist ]] && system_files+=("/Library/Preferences/$bundle_id.plist") @@ -1591,7 +1606,7 @@ find_app_system_files() { # Installation Receipts while IFS= read -r -d '' receipt; do system_files+=("$receipt") - done < <(find /private/var/db/receipts \( -name "*$bundle_id*" \) -print0 2>/dev/null) + done < <(find /private/var/db/receipts \( -name "*$bundle_id*" \) -print0 2> /dev/null) # System Logs [[ -d /Library/Logs/"$app_name" ]] && system_files+=("/Library/Logs/$app_name") @@ -1603,7 +1618,7 @@ find_app_system_files() { # System Internet Plug-Ins while IFS= read -r -d '' plugin; do system_files+=("$plugin") - done < <(find /Library/Internet\ Plug-Ins \( -name "$bundle_id*" -o -name "$app_name*" \) -print0 2>/dev/null) + done < <(find /Library/Internet\ Plug-Ins \( -name "$bundle_id*" -o -name "$app_name*" \) -print0 2> /dev/null) # System QuickLook Plugins [[ -d /Library/QuickLook/"$app_name".qlgenerator ]] && system_files+=("/Library/QuickLook/$app_name.qlgenerator") @@ -1611,7 +1626,7 @@ find_app_system_files() { # System Receipts while IFS= read -r -d '' receipt; do system_files+=("$receipt") - done < <(find /Library/Receipts \( -name "*$bundle_id*" -o -name "*$app_name*" \) -print0 2>/dev/null) + done < <(find /Library/Receipts \( -name "*$bundle_id*" -o -name "*$app_name*" \) -print0 2> /dev/null) # System Spotlight Plugins [[ -d /Library/Spotlight/"$app_name".mdimporter ]] && system_files+=("/Library/Spotlight/$app_name.mdimporter") @@ -1619,7 +1634,7 @@ find_app_system_files() { # System Scripting Additions while IFS= read -r -d '' scripting; do system_files+=("$scripting") - done < <(find /Library/ScriptingAdditions \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2>/dev/null) + done < <(find /Library/ScriptingAdditions \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) # System Color Pickers [[ -d /Library/ColorPickers/"$app_name".colorPicker ]] && system_files+=("/Library/ColorPickers/$app_name.colorPicker") @@ -1627,32 +1642,32 @@ find_app_system_files() { # System Quartz Compositions while IFS= read -r -d '' composition; do system_files+=("$composition") - done < <(find /Library/Compositions \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2>/dev/null) + done < <(find /Library/Compositions \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) # System Address Book Plug-Ins while IFS= read -r -d '' plugin; do system_files+=("$plugin") - done < <(find /Library/Address\ Book\ Plug-Ins \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2>/dev/null) + done < <(find /Library/Address\ Book\ Plug-Ins \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) # System Mail Bundles while IFS= read -r -d '' bundle; do system_files+=("$bundle") - done < <(find /Library/Mail/Bundles \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2>/dev/null) + done < <(find /Library/Mail/Bundles \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) # System Input Managers while IFS= read -r -d '' manager; do system_files+=("$manager") - done < <(find /Library/InputManagers \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2>/dev/null) + done < <(find /Library/InputManagers \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) # System Sounds while IFS= read -r -d '' sound; do system_files+=("$sound") - done < <(find /Library/Sounds \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2>/dev/null) + done < <(find /Library/Sounds \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) # System Contextual Menu Items while IFS= read -r -d '' item; do system_files+=("$item") - done < <(find /Library/Contextual\ Menu\ Items \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2>/dev/null) + done < <(find /Library/Contextual\ Menu\ Items \( -name "$app_name*" -o -name "$bundle_id*" \) -print0 2> /dev/null) # System Preference Panes [[ -d /Library/PreferencePanes/"$app_name".prefPane ]] && system_files+=("/Library/PreferencePanes/$app_name.prefPane") @@ -1677,7 +1692,8 @@ calculate_total_size() { while IFS= read -r file; do if [[ -n "$file" && -e "$file" ]]; then - local size_kb=$(du -sk "$file" 2>/dev/null | awk '{print $1}' || echo "0") + local size_kb + size_kb=$(du -sk "$file" 2> /dev/null | awk '{print $1}' || echo "0") ((total_kb += size_kb)) fi done <<< "$files" @@ -1691,20 +1707,20 @@ get_brand_name() { # Brand name mapping for better user recognition case "$name" in - "qiyimac"|"爱奇艺") echo "iQiyi" ;; - "wechat"|"微信") echo "WeChat" ;; + "qiyimac" | "爱奇艺") echo "iQiyi" ;; + "wechat" | "微信") echo "WeChat" ;; "QQ") echo "QQ" ;; - "VooV Meeting"|"腾讯会议") echo "VooV Meeting" ;; - "dingtalk"|"钉钉") echo "DingTalk" ;; - "NeteaseMusic"|"网易云音乐") echo "NetEase Music" ;; - "BaiduNetdisk"|"百度网盘") echo "Baidu NetDisk" ;; - "alipay"|"支付宝") echo "Alipay" ;; - "taobao"|"淘宝") echo "Taobao" ;; - "futunn"|"富途牛牛") echo "Futu NiuNiu" ;; - "tencent lemon"|"Tencent Lemon Cleaner") echo "Tencent Lemon" ;; - "keynote"|"Keynote") echo "Keynote" ;; - "pages"|"Pages") echo "Pages" ;; - "numbers"|"Numbers") echo "Numbers" ;; - *) echo "$name" ;; # Return original if no mapping found + "VooV Meeting" | "腾讯会议") echo "VooV Meeting" ;; + "dingtalk" | "钉钉") echo "DingTalk" ;; + "NeteaseMusic" | "网易云音乐") echo "NetEase Music" ;; + "BaiduNetdisk" | "百度网盘") echo "Baidu NetDisk" ;; + "alipay" | "支付宝") echo "Alipay" ;; + "taobao" | "淘宝") echo "Taobao" ;; + "futunn" | "富途牛牛") echo "Futu NiuNiu" ;; + "tencent lemon" | "Tencent Lemon Cleaner") echo "Tencent Lemon" ;; + "keynote" | "Keynote") echo "Keynote" ;; + "pages" | "Pages") echo "Pages" ;; + "numbers" | "Numbers") echo "Numbers" ;; + *) echo "$name" ;; # Return original if no mapping found esac } diff --git a/lib/paginated_menu.sh b/lib/paginated_menu.sh index 6cdcf88..4496a6b 100755 --- a/lib/paginated_menu.sh +++ b/lib/paginated_menu.sh @@ -4,8 +4,8 @@ set -euo pipefail # Terminal control functions -enter_alt_screen() { tput smcup 2>/dev/null || true; } -leave_alt_screen() { tput rmcup 2>/dev/null || true; } +enter_alt_screen() { tput smcup 2> /dev/null || true; } +leave_alt_screen() { tput rmcup 2> /dev/null || true; } # Main paginated multi-select menu function paginated_multi_select() { @@ -47,16 +47,16 @@ paginated_multi_select() { # Preserve original TTY settings so we can restore them reliably local original_stty="" - if [[ -t 0 ]] && command -v stty >/dev/null 2>&1; then - original_stty=$(stty -g 2>/dev/null || echo "") + if [[ -t 0 ]] && command -v stty > /dev/null 2>&1; then + original_stty=$(stty -g 2> /dev/null || echo "") fi restore_terminal() { show_cursor if [[ -n "${original_stty-}" ]]; then - stty "${original_stty}" 2>/dev/null || stty sane 2>/dev/null || stty echo icanon 2>/dev/null || true + stty "${original_stty}" 2> /dev/null || stty sane 2> /dev/null || stty echo icanon 2> /dev/null || true else - stty sane 2>/dev/null || stty echo icanon 2>/dev/null || true + stty sane 2> /dev/null || stty echo icanon 2> /dev/null || true fi if [[ "${external_alt_screen:-false}" == false ]]; then leave_alt_screen @@ -72,14 +72,14 @@ paginated_multi_select() { # Interrupt handler handle_interrupt() { cleanup - exit 130 # Standard exit code for Ctrl+C + exit 130 # Standard exit code for Ctrl+C } trap cleanup EXIT trap handle_interrupt INT TERM # Setup terminal - preserve interrupt character - stty -echo -icanon intr ^C 2>/dev/null || true + stty -echo -icanon intr ^C 2> /dev/null || true if [[ $external_alt_screen == false ]]; then enter_alt_screen # Clear screen once on entry to alt screen @@ -108,7 +108,7 @@ paginated_multi_select() { draw_menu() { # Move to home position without clearing (reduces flicker) printf "\033[H" >&2 - + # Clear each line as we go instead of clearing entire screen local clear_line="\r\033[2K" @@ -169,7 +169,7 @@ paginated_multi_select() { # Clear any remaining lines at bottom printf "${clear_line}\n" >&2 printf "${clear_line}${GRAY}${ICON_NAV_UP}/${ICON_NAV_DOWN}${NC} Navigate ${GRAY}|${NC} ${GRAY}Space${NC} Select ${GRAY}|${NC} ${GRAY}Enter${NC} Confirm ${GRAY}|${NC} ${GRAY}Q/ESC${NC} Quit\n" >&2 - + # Clear one more line to ensure no artifacts printf "${clear_line}" >&2 } @@ -177,7 +177,7 @@ paginated_multi_select() { # Show help screen show_help() { printf "\033[H\033[J" >&2 - cat >&2 <&2 << EOF Help - Navigation Controls ========================== @@ -269,7 +269,7 @@ EOF local IFS=',' final_result="${selected_indices[*]}" fi - + # Remove the trap to avoid cleanup on normal exit trap - EXIT INT TERM diff --git a/lib/whitelist_manager.sh b/lib/whitelist_manager.sh index dab1f7d..6d2c3ef 100755 --- a/lib/whitelist_manager.sh +++ b/lib/whitelist_manager.sh @@ -114,7 +114,6 @@ patterns_equivalent() { return 1 } - load_whitelist() { local -a patterns=() @@ -163,14 +162,13 @@ is_whitelisted() { if [[ "$check_pattern" == "$existing_expanded" ]]; then return 0 fi - if [[ "$check_pattern" == $existing_expanded ]]; then + if [[ "$check_pattern" == "$existing_expanded" ]]; then return 0 fi done return 1 } - manage_whitelist() { manage_whitelist_categories } @@ -286,7 +284,6 @@ manage_whitelist_categories() { printf '\n' } - if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then manage_whitelist fi diff --git a/mole b/mole index d3099ed..8523336 100755 --- a/mole +++ b/mole @@ -28,7 +28,7 @@ MOLE_TAGLINE="can dig deep to clean your Mac." # Get latest version from remote repository get_latest_version() { curl -fsSL --connect-timeout 2 --max-time 3 -H "Cache-Control: no-cache" \ - "https://raw.githubusercontent.com/tw93/mole/main/mole" 2>/dev/null | \ + "https://raw.githubusercontent.com/tw93/mole/main/mole" 2> /dev/null | grep '^VERSION=' | head -1 | sed 's/VERSION="\(.*\)"/\1/' } @@ -37,11 +37,11 @@ check_for_updates() { local cache="$HOME/.cache/mole/version_check" local msg_cache="$HOME/.cache/mole/update_message" local ttl="${MO_UPDATE_CHECK_TTL:-3600}" - mkdir -p "$(dirname "$cache")" 2>/dev/null + mkdir -p "$(dirname "$cache")" 2> /dev/null # Skip if checked recently if [[ -f "$cache" ]]; then - local age=$(($(date +%s) - $(stat -f%m "$cache" 2>/dev/null || echo 0))) + local age=$(($(date +%s) - $(stat -f%m "$cache" 2> /dev/null || echo 0))) [[ $age -lt $ttl ]] && return fi @@ -55,9 +55,9 @@ check_for_updates() { else echo -n > "$msg_cache" fi - touch "$cache" 2>/dev/null + touch "$cache" 2> /dev/null ) & - disown 2>/dev/null || true + disown 2> /dev/null || true } # Show update notification if available @@ -93,7 +93,7 @@ animate_mole_intro() { local -a mole_lines=() while IFS= read -r line; do mole_lines+=("$line") - done <<'EOF' + done << 'EOF' /\_/\ ____/ o o \ /~____ =o= / @@ -108,7 +108,7 @@ EOF local body_color="${PURPLE}" local ground_color="${GREEN}" for idx in "${!mole_lines[@]}"; do - if (( idx < body_cutoff )); then + if ((idx < body_cutoff)); then printf "%s\n" "${body_color}${mole_lines[$idx]}${NC}" else printf "%s\n" "${ground_color}${mole_lines[$idx]}${NC}" @@ -150,7 +150,7 @@ show_help() { # Simple update function update_mole() { # Check if installed via Homebrew - if command -v brew >/dev/null 2>&1 && brew list mole >/dev/null 2>&1; then + if command -v brew > /dev/null 2>&1 && brew list mole > /dev/null 2>&1; then update_via_homebrew "$VERSION" exit 0 fi @@ -180,17 +180,20 @@ update_mole() { local installer_url="https://raw.githubusercontent.com/tw93/mole/main/install.sh" local tmp_installer - tmp_installer="$(mktemp_file)" || { log_error "Update failed"; exit 1; } + tmp_installer="$(mktemp_file)" || { + log_error "Update failed" + exit 1 + } # Download installer with progress - if command -v curl >/dev/null 2>&1; then + if command -v curl > /dev/null 2>&1; then if ! curl -fsSL --connect-timeout 10 --max-time 60 "$installer_url" -o "$tmp_installer" 2>&1; then if [[ -t 1 ]]; then stop_inline_spinner; fi rm -f "$tmp_installer" log_error "Update failed. Check network connection." exit 1 fi - elif command -v wget >/dev/null 2>&1; then + elif command -v wget > /dev/null 2>&1; then if ! wget --timeout=10 --tries=3 -qO "$tmp_installer" "$installer_url" 2>&1; then if [[ -t 1 ]]; then stop_inline_spinner; fi rm -f "$tmp_installer" @@ -209,7 +212,7 @@ update_mole() { # Determine install directory local mole_path - mole_path="$(command -v mole 2>/dev/null || echo "$0")" + mole_path="$(command -v mole 2> /dev/null || echo "$0")" local install_dir install_dir="$(cd "$(dirname "$mole_path")" && pwd)" @@ -231,7 +234,7 @@ update_mole() { # Only show success message if installer didn't already do so if ! printf '%s\n' "$install_output" | grep -Eq "Updated to latest version|Already on latest version"; then local new_version - new_version=$("$mole_path" --version 2>/dev/null | awk 'NF {print $NF}' || echo "") + new_version=$("$mole_path" --version 2> /dev/null | awk 'NF {print $NF}' || echo "") printf '\n%s\n\n' "${GREEN}${ICON_SUCCESS}${NC} Updated to latest version (${new_version:-unknown})" else printf '\n' @@ -247,7 +250,7 @@ update_mole() { fi if ! printf '%s\n' "$install_output" | grep -Eq "Updated to latest version|Already on latest version"; then local new_version - new_version=$("$mole_path" --version 2>/dev/null | awk 'NF {print $NF}' || echo "") + new_version=$("$mole_path" --version 2> /dev/null | awk 'NF {print $NF}' || echo "") printf '\n%s\n\n' "${GREEN}${ICON_SUCCESS}${NC} Updated to latest version (${new_version:-unknown})" else printf '\n' @@ -256,7 +259,7 @@ update_mole() { if [[ -t 1 ]]; then stop_inline_spinner; fi rm -f "$tmp_installer" log_error "Update failed" - echo "$install_output" | tail -10 >&2 # Show last 10 lines of error + echo "$install_output" | tail -10 >&2 # Show last 10 lines of error exit 1 fi fi @@ -279,7 +282,7 @@ remove_mole() { local -a alias_installs=() # Check Homebrew - if command -v brew >/dev/null 2>&1 && brew list mole >/dev/null 2>&1; then + if command -v brew > /dev/null 2>&1 && brew list mole > /dev/null 2>&1; then is_homebrew=true fi @@ -318,7 +321,9 @@ remove_mole() { printf '\n' # Check if anything to remove - if [[ "$is_homebrew" == "false" && ${#manual_installs[@]:-0} -eq 0 && ${#alias_installs[@]:-0} -eq 0 ]]; then + local manual_count=${#manual_installs[@]} + local alias_count=${#alias_installs[@]} + if [[ "$is_homebrew" == "false" && ${manual_count:-0} -eq 0 && ${alias_count:-0} -eq 0 ]]; then printf '%s\n\n' "${YELLOW}No Mole installation detected${NC}" exit 0 fi @@ -343,8 +348,8 @@ remove_mole() { echo "" exit 0 ;; - ""|$'\n'|$'\r') - printf "\r\033[K" # Clear the prompt line + "" | $'\n' | $'\r') + printf "\r\033[K" # Clear the prompt line # Continue with removal ;; *) @@ -357,32 +362,32 @@ remove_mole() { # Remove Homebrew installation (silent) local has_error=false if [[ "$is_homebrew" == "true" ]]; then - if ! brew uninstall mole >/dev/null 2>&1; then + if ! brew uninstall mole > /dev/null 2>&1; then has_error=true fi fi # Remove manual installations (silent) - if [[ ${#manual_installs[@]:-0} -gt 0 ]]; then + if [[ ${manual_count:-0} -gt 0 ]]; then for install in "${manual_installs[@]}"; do if [[ -f "$install" ]]; then - rm -f "$install" 2>/dev/null || has_error=true + rm -f "$install" 2> /dev/null || has_error=true fi done fi - if [[ ${#alias_installs[@]} -gt 0 ]]; then + if [[ ${alias_count:-0} -gt 0 ]]; then for alias in "${alias_installs[@]}"; do if [[ -f "$alias" ]]; then - rm -f "$alias" 2>/dev/null || true + rm -f "$alias" 2> /dev/null || true fi done fi # Clean up cache first (silent) if [[ -d "$HOME/.cache/mole" ]]; then - rm -rf "$HOME/.cache/mole" 2>/dev/null || true + rm -rf "$HOME/.cache/mole" 2> /dev/null || true fi # Clean up configuration last (silent) if [[ -d "$HOME/.config/mole" ]]; then - rm -rf "$HOME/.config/mole" 2>/dev/null || true + rm -rf "$HOME/.config/mole" 2> /dev/null || true fi # Show final result @@ -400,7 +405,7 @@ remove_mole() { # Display main menu options with minimal refresh to avoid flicker show_main_menu() { local selected="${1:-1}" - local _full_draw="${2:-true}" # Kept for compatibility (unused) + local _full_draw="${2:-true}" # Kept for compatibility (unused) local banner="${MAIN_MENU_BANNER:-}" local update_message="${MAIN_MENU_UPDATE_MESSAGE:-}" @@ -410,7 +415,7 @@ show_main_menu() { MAIN_MENU_BANNER="$banner" fi - printf '\033[H' # Move cursor to home + printf '\033[H' # Move cursor to home local line="" # Leading spacer @@ -452,13 +457,13 @@ interactive_main_menu() { # Show intro animation only once per terminal tab if [[ -t 1 ]]; then local tty_name - tty_name=$(tty 2>/dev/null || echo "") + tty_name=$(tty 2> /dev/null || echo "") if [[ -n "$tty_name" ]]; then local flag_file flag_file="/tmp/mole_intro_$(echo "$tty_name" | tr -c '[:alnum:]_' '_')" if [[ ! -f "$flag_file" ]]; then animate_mole_intro - touch "$flag_file" 2>/dev/null || true + touch "$flag_file" 2> /dev/null || true fi fi fi @@ -472,7 +477,7 @@ interactive_main_menu() { MAIN_MENU_BANNER="$brand_banner" if [[ -f "$msg_cache" && -s "$msg_cache" ]]; then - update_message="$(cat "$msg_cache" 2>/dev/null || echo "")" + update_message="$(cat "$msg_cache" 2> /dev/null || echo "")" fi MAIN_MENU_UPDATE_MESSAGE="$update_message" @@ -501,7 +506,7 @@ interactive_main_menu() { case "$key" in "UP") ((current_option > 1)) && ((current_option--)) ;; "DOWN") ((current_option < 5)) && ((current_option++)) ;; - "ENTER"|"$current_option") + "ENTER" | "$current_option") show_cursor case $current_option in 1) @@ -509,7 +514,11 @@ interactive_main_menu() { ;; 2) exec "$SCRIPT_DIR/bin/uninstall.sh" ;; 3) exec "$SCRIPT_DIR/bin/analyze.sh" ;; - 4) clear; show_help; exit 0 ;; + 4) + clear + show_help + exit 0 + ;; 5) cleanup_and_exit ;; esac ;; @@ -522,7 +531,11 @@ interactive_main_menu() { ;; 2) exec "$SCRIPT_DIR/bin/uninstall.sh" ;; 3) exec "$SCRIPT_DIR/bin/analyze.sh" ;; - 4) clear; show_help; exit 0 ;; + 4) + clear + show_help + exit 0 + ;; 5) cleanup_and_exit ;; esac ;; @@ -552,11 +565,11 @@ main() { remove_mole exit 0 ;; - "help"|"--help"|"-h") + "help" | "--help" | "-h") show_help exit 0 ;; - "version"|"--version"|"-V") + "version" | "--version" | "-V") show_version exit 0 ;; diff --git a/scripts/format.sh b/scripts/format.sh new file mode 100755 index 0000000..121f7d5 --- /dev/null +++ b/scripts/format.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Format all shell scripts in the Mole project +# +# Usage: +# ./scripts/format.sh # Format all scripts +# ./scripts/format.sh --check # Check only, don't modify + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +CHECK_ONLY=false + +# Parse arguments +if [[ "${1:-}" == "--check" ]]; then + CHECK_ONLY=true +elif [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then + cat << 'EOF' +Usage: ./scripts/format.sh [--check] + +Format shell scripts using shfmt. + +Options: + --check Check formatting without modifying files + --help Show this help + +Install: brew install shfmt +EOF + exit 0 +fi + +# Check if shfmt is installed +if ! command -v shfmt > /dev/null 2>&1; then + echo "Error: shfmt not installed" + echo "Install: brew install shfmt" + exit 1 +fi + +# Find all shell scripts +cd "$PROJECT_ROOT" + +# shfmt options: -i 4 (4 spaces), -ci (indent switch cases), -sr (space after redirect) +if [[ "$CHECK_ONLY" == "true" ]]; then + echo "Checking formatting..." + if shfmt -i 4 -ci -sr -d . > /dev/null 2>&1; then + echo "✓ All scripts properly formatted" + exit 0 + else + echo "✗ Some scripts need formatting:" + shfmt -i 4 -ci -sr -d . + echo "" + echo "Run './scripts/format.sh' to fix" + exit 1 + fi +else + echo "Formatting scripts..." + shfmt -i 4 -ci -sr -w . + echo "✓ Done" +fi diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh new file mode 100755 index 0000000..a28f983 --- /dev/null +++ b/scripts/install-hooks.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Install git hooks for Mole project +# +# Usage: +# ./scripts/install-hooks.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' + +cd "$PROJECT_ROOT" + +# Check if this is a git repository +if [ ! -d ".git" ]; then + echo "Error: Not a git repository" + exit 1 +fi + +echo -e "${BLUE}Installing git hooks...${NC}" + +# Install pre-commit hook +if [ -f ".git/hooks/pre-commit" ]; then + echo "Pre-commit hook already exists, creating backup..." + mv .git/hooks/pre-commit .git/hooks/pre-commit.backup +fi + +ln -s ../../scripts/pre-commit.sh .git/hooks/pre-commit +chmod +x .git/hooks/pre-commit + +echo -e "${GREEN}✓ Pre-commit hook installed${NC}" +echo "" +echo "The hook will:" +echo " • Auto-format shell scripts before commit" +echo " • Run shellcheck on changed files" +echo " • Show warnings but won't block commits" +echo "" +echo "To uninstall:" +echo " rm .git/hooks/pre-commit" +echo "" diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh new file mode 100755 index 0000000..1237812 --- /dev/null +++ b/scripts/pre-commit.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# Git pre-commit hook for Mole +# Automatically formats shell scripts before commit +# +# Installation: +# ln -s ../../scripts/pre-commit.sh .git/hooks/pre-commit +# chmod +x .git/hooks/pre-commit +# +# Or use the install script: +# ./scripts/install-hooks.sh + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# Only check shell files that are staged +STAGED_SH_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.sh$|^mole$' || true) + +if [ -z "$STAGED_SH_FILES" ]; then + exit 0 +fi + +echo -e "${YELLOW}Running pre-commit checks on shell files...${NC}" + +# Check if shfmt is installed +if ! command -v shfmt &> /dev/null; then + echo -e "${RED}shfmt is not installed. Install with: brew install shfmt${NC}" + exit 1 +fi + +# Check if shellcheck is installed +if ! command -v shellcheck &> /dev/null; then + echo -e "${RED}shellcheck is not installed. Install with: brew install shellcheck${NC}" + exit 1 +fi + +NEEDS_FORMAT=0 + +# Check formatting +for file in $STAGED_SH_FILES; do + if ! shfmt -i 4 -ci -sr -d "$file" > /dev/null 2>&1; then + echo -e "${YELLOW}Formatting $file...${NC}" + shfmt -i 4 -ci -sr -w "$file" + git add "$file" + NEEDS_FORMAT=1 + fi +done + +# Run shellcheck +for file in $STAGED_SH_FILES; do + if ! shellcheck -S warning "$file" > /dev/null 2>&1; then + echo -e "${YELLOW}ShellCheck warnings in $file:${NC}" + shellcheck -S warning "$file" + echo -e "${YELLOW}Continuing with commit (warnings are non-critical)...${NC}" + fi +done + +if [ $NEEDS_FORMAT -eq 1 ]; then + echo -e "${GREEN}✓ Files formatted and re-staged${NC}" +fi + +echo -e "${GREEN}✓ Pre-commit checks passed${NC}" +exit 0 diff --git a/tests/analyze.bats b/tests/analyze.bats index 2a780ef..872980f 100644 --- a/tests/analyze.bats +++ b/tests/analyze.bats @@ -1,31 +1,31 @@ #!/usr/bin/env bats setup_file() { - PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" - export PROJECT_ROOT + PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" + export PROJECT_ROOT - ORIGINAL_HOME="${HOME:-}" - export ORIGINAL_HOME + ORIGINAL_HOME="${HOME:-}" + export ORIGINAL_HOME - HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-analyze-home.XXXXXX")" - export HOME + HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-analyze-home.XXXXXX")" + export HOME } teardown_file() { - rm -rf "$HOME" - if [[ -n "${ORIGINAL_HOME:-}" ]]; then - export HOME="$ORIGINAL_HOME" - fi + rm -rf "$HOME" + if [[ -n "${ORIGINAL_HOME:-}" ]]; then + export HOME="$ORIGINAL_HOME" + fi } setup() { - export TERM="dumb" - rm -rf "${HOME:?}"/* - mkdir -p "$HOME" + export TERM="dumb" + rm -rf "${HOME:?}"/* + mkdir -p "$HOME" } @test "scan_directories lists largest folders first" { - run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF' set -euo pipefail source "$PROJECT_ROOT/bin/analyze.sh" @@ -40,12 +40,12 @@ scan_directories "$root" "$output_file" 1 head -n1 "$output_file" EOF - [ "$status" -eq 0 ] - [[ "$output" == *"Large"* ]] + [ "$status" -eq 0 ] + [[ "$output" == *"Large"* ]] } @test "aggregate_by_directory sums child sizes per parent" { - run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF' set -euo pipefail source "$PROJECT_ROOT/bin/analyze.sh" @@ -65,7 +65,7 @@ aggregate_by_directory "$input_file" "$output_file" cat "$output_file" EOF - [ "$status" -eq 0 ] - [[ "$output" == *"3072|$HOME/group/a/"* ]] - [[ "$output" == *"512|$HOME/group/b/"* ]] + [ "$status" -eq 0 ] + [[ "$output" == *"3072|$HOME/group/a/"* ]] + [[ "$output" == *"512|$HOME/group/b/"* ]] } diff --git a/tests/clean.bats b/tests/clean.bats index cab2eaa..6e4646b 100644 --- a/tests/clean.bats +++ b/tests/clean.bats @@ -1,60 +1,60 @@ #!/usr/bin/env bats setup_file() { - PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" - export PROJECT_ROOT + PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" + export PROJECT_ROOT - ORIGINAL_HOME="${HOME:-}" - export ORIGINAL_HOME + ORIGINAL_HOME="${HOME:-}" + export ORIGINAL_HOME - HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-clean-home.XXXXXX")" - export HOME + HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-clean-home.XXXXXX")" + export HOME - mkdir -p "$HOME" + mkdir -p "$HOME" } teardown_file() { - rm -rf "$HOME" - if [[ -n "${ORIGINAL_HOME:-}" ]]; then - export HOME="$ORIGINAL_HOME" - fi + rm -rf "$HOME" + if [[ -n "${ORIGINAL_HOME:-}" ]]; then + export HOME="$ORIGINAL_HOME" + fi } setup() { - export TERM="xterm-256color" - rm -rf "${HOME:?}"/* - rm -rf "$HOME/Library" "$HOME/.config" - mkdir -p "$HOME/Library/Caches" "$HOME/.config/mole" + export TERM="xterm-256color" + rm -rf "${HOME:?}"/* + rm -rf "$HOME/Library" "$HOME/.config" + mkdir -p "$HOME/Library/Caches" "$HOME/.config/mole" } @test "mo clean --dry-run skips system cleanup in non-interactive mode" { - run env HOME="$HOME" "$PROJECT_ROOT/mole" clean --dry-run - [ "$status" -eq 0 ] - [[ "$output" == *"Dry Run Mode"* ]] - [[ "$output" != *"Deep system-level cleanup"* ]] + run env HOME="$HOME" "$PROJECT_ROOT/mole" clean --dry-run + [ "$status" -eq 0 ] + [[ "$output" == *"Dry Run Mode"* ]] + [[ "$output" != *"Deep system-level cleanup"* ]] } @test "mo clean --dry-run reports user cache without deleting it" { - mkdir -p "$HOME/Library/Caches/TestApp" - echo "cache data" > "$HOME/Library/Caches/TestApp/cache.tmp" + mkdir -p "$HOME/Library/Caches/TestApp" + echo "cache data" > "$HOME/Library/Caches/TestApp/cache.tmp" - run env HOME="$HOME" "$PROJECT_ROOT/mole" clean --dry-run - [ "$status" -eq 0 ] - [[ "$output" == *"User app cache"* ]] - [[ "$output" == *"Potential space"* ]] - [ -f "$HOME/Library/Caches/TestApp/cache.tmp" ] + run env HOME="$HOME" "$PROJECT_ROOT/mole" clean --dry-run + [ "$status" -eq 0 ] + [[ "$output" == *"User app cache"* ]] + [[ "$output" == *"Potential space"* ]] + [ -f "$HOME/Library/Caches/TestApp/cache.tmp" ] } @test "mo clean honors whitelist entries" { - mkdir -p "$HOME/Library/Caches/WhitelistedApp" - echo "keep me" > "$HOME/Library/Caches/WhitelistedApp/data.tmp" + mkdir -p "$HOME/Library/Caches/WhitelistedApp" + echo "keep me" > "$HOME/Library/Caches/WhitelistedApp/data.tmp" - cat > "$HOME/.config/mole/whitelist" < "$HOME/.config/mole/whitelist" << EOF $HOME/Library/Caches/WhitelistedApp* EOF - run env HOME="$HOME" "$PROJECT_ROOT/mole" clean --dry-run - [ "$status" -eq 0 ] - [[ "$output" == *"Protected: 1"* ]] - [ -f "$HOME/Library/Caches/WhitelistedApp/data.tmp" ] + run env HOME="$HOME" "$PROJECT_ROOT/mole" clean --dry-run + [ "$status" -eq 0 ] + [[ "$output" == *"Protected: 1"* ]] + [ -f "$HOME/Library/Caches/WhitelistedApp/data.tmp" ] } diff --git a/tests/cli.bats b/tests/cli.bats index f84a097..820a8a3 100644 --- a/tests/cli.bats +++ b/tests/cli.bats @@ -1,80 +1,80 @@ #!/usr/bin/env bats setup_file() { - PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" - export PROJECT_ROOT + PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" + export PROJECT_ROOT - ORIGINAL_HOME="${HOME:-}" - export ORIGINAL_HOME + ORIGINAL_HOME="${HOME:-}" + export ORIGINAL_HOME - HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-cli-home.XXXXXX")" - export HOME + HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-cli-home.XXXXXX")" + export HOME - mkdir -p "$HOME" + mkdir -p "$HOME" } teardown_file() { - rm -rf "$HOME" - if [[ -n "${ORIGINAL_HOME:-}" ]]; then - export HOME="$ORIGINAL_HOME" - fi + rm -rf "$HOME" + if [[ -n "${ORIGINAL_HOME:-}" ]]; then + export HOME="$ORIGINAL_HOME" + fi } setup() { - rm -rf "$HOME/.config" - mkdir -p "$HOME" + rm -rf "$HOME/.config" + mkdir -p "$HOME" } @test "mole --help prints command overview" { - run env HOME="$HOME" "$PROJECT_ROOT/mole" --help - [ "$status" -eq 0 ] - [[ "$output" == *"mo clean"* ]] - [[ "$output" == *"mo analyze"* ]] + run env HOME="$HOME" "$PROJECT_ROOT/mole" --help + [ "$status" -eq 0 ] + [[ "$output" == *"mo clean"* ]] + [[ "$output" == *"mo analyze"* ]] } @test "mole --version reports script version" { - expected_version="$(grep '^VERSION=' "$PROJECT_ROOT/mole" | head -1 | sed 's/VERSION=\"\(.*\)\"/\1/')" - run env HOME="$HOME" "$PROJECT_ROOT/mole" --version - [ "$status" -eq 0 ] - [[ "$output" == *"$expected_version"* ]] + expected_version="$(grep '^VERSION=' "$PROJECT_ROOT/mole" | head -1 | sed 's/VERSION=\"\(.*\)\"/\1/')" + run env HOME="$HOME" "$PROJECT_ROOT/mole" --version + [ "$status" -eq 0 ] + [[ "$output" == *"$expected_version"* ]] } @test "mole unknown command returns error" { - run env HOME="$HOME" "$PROJECT_ROOT/mole" unknown-command - [ "$status" -ne 0 ] - [[ "$output" == *"Unknown command: unknown-command"* ]] + run env HOME="$HOME" "$PROJECT_ROOT/mole" unknown-command + [ "$status" -ne 0 ] + [[ "$output" == *"Unknown command: unknown-command"* ]] } @test "clean.sh --help shows usage details" { - run env HOME="$HOME" "$PROJECT_ROOT/bin/clean.sh" --help - [ "$status" -eq 0 ] - [[ "$output" == *"Mole - Deeper system cleanup"* ]] - [[ "$output" == *"--dry-run"* ]] + run env HOME="$HOME" "$PROJECT_ROOT/bin/clean.sh" --help + [ "$status" -eq 0 ] + [[ "$output" == *"Mole - Deeper system cleanup"* ]] + [[ "$output" == *"--dry-run"* ]] } @test "uninstall.sh --help highlights controls" { - run env HOME="$HOME" "$PROJECT_ROOT/bin/uninstall.sh" --help - [ "$status" -eq 0 ] - [[ "$output" == *"Usage: mole uninstall"* ]] - [[ "$output" == *"Keyboard Controls"* ]] + run env HOME="$HOME" "$PROJECT_ROOT/bin/uninstall.sh" --help + [ "$status" -eq 0 ] + [[ "$output" == *"Usage: mole uninstall"* ]] + [[ "$output" == *"Keyboard Controls"* ]] } @test "analyze.sh --help outlines explorer features" { - run env HOME="$HOME" "$PROJECT_ROOT/bin/analyze.sh" --help - [ "$status" -eq 0 ] - [[ "$output" == *"Interactive disk space explorer"* ]] - [[ "$output" == *"mole analyze"* ]] + run env HOME="$HOME" "$PROJECT_ROOT/bin/analyze.sh" --help + [ "$status" -eq 0 ] + [[ "$output" == *"Interactive disk space explorer"* ]] + [[ "$output" == *"mole analyze"* ]] } @test "touchid --help describes configuration options" { - run env HOME="$HOME" "$PROJECT_ROOT/mole" touchid --help - [ "$status" -eq 0 ] - [[ "$output" == *"Touch ID"* ]] - [[ "$output" == *"mo touchid enable"* ]] + run env HOME="$HOME" "$PROJECT_ROOT/mole" touchid --help + [ "$status" -eq 0 ] + [[ "$output" == *"Touch ID"* ]] + [[ "$output" == *"mo touchid enable"* ]] } @test "touchid status reports current configuration" { - run env HOME="$HOME" "$PROJECT_ROOT/mole" touchid status - [ "$status" -eq 0 ] - [[ "$output" == *"Touch ID"* ]] + run env HOME="$HOME" "$PROJECT_ROOT/mole" touchid status + [ "$status" -eq 0 ] + [[ "$output" == *"Touch ID"* ]] } diff --git a/tests/common.bats b/tests/common.bats index 6aa7833..53e0619 100644 --- a/tests/common.bats +++ b/tests/common.bats @@ -1,123 +1,124 @@ #!/usr/bin/env bats setup_file() { - PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" - export PROJECT_ROOT + PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" + export PROJECT_ROOT - ORIGINAL_HOME="${HOME:-}" - export ORIGINAL_HOME + ORIGINAL_HOME="${HOME:-}" + export ORIGINAL_HOME - HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-home.XXXXXX")" - export HOME + HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-home.XXXXXX")" + export HOME - mkdir -p "$HOME" + mkdir -p "$HOME" } teardown_file() { - rm -rf "$HOME" - if [[ -n "${ORIGINAL_HOME:-}" ]]; then - export HOME="$ORIGINAL_HOME" - fi + rm -rf "$HOME" + if [[ -n "${ORIGINAL_HOME:-}" ]]; then + export HOME="$ORIGINAL_HOME" + fi } setup() { - rm -rf "$HOME/.config" - mkdir -p "$HOME" + rm -rf "$HOME/.config" + mkdir -p "$HOME" } teardown() { - unset MO_SPINNER_CHARS || true + unset MO_SPINNER_CHARS || true } @test "mo_spinner_chars returns default sequence when unset" { - result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; mo_spinner_chars")" - [ "$result" = "|/-\\" ] + result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; mo_spinner_chars")" + [ "$result" = "|/-\\" ] } @test "mo_spinner_chars respects MO_SPINNER_CHARS override" { - export MO_SPINNER_CHARS="abcd" - result="$(HOME="$HOME" MO_SPINNER_CHARS="$MO_SPINNER_CHARS" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; mo_spinner_chars")" - [ "$result" = "abcd" ] + export MO_SPINNER_CHARS="abcd" + result="$(HOME="$HOME" MO_SPINNER_CHARS="$MO_SPINNER_CHARS" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; mo_spinner_chars")" + [ "$result" = "abcd" ] } @test "detect_architecture maps current CPU to friendly label" { - expected="Intel" - if [[ "$(uname -m)" == "arm64" ]]; then - expected="Apple Silicon" - fi - result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; detect_architecture")" - [ "$result" = "$expected" ] + expected="Intel" + if [[ "$(uname -m)" == "arm64" ]]; then + expected="Apple Silicon" + fi + result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; detect_architecture")" + [ "$result" = "$expected" ] } @test "get_free_space returns a non-empty value" { - result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; get_free_space")" - [[ -n "$result" ]] + result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; get_free_space")" + [[ -n "$result" ]] } @test "log_info prints message and appends to log file" { - local message="Informational message from test" - local stdout_output - stdout_output="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; log_info '$message'")" - [[ "$stdout_output" == *"$message"* ]] + local message="Informational message from test" + local stdout_output + stdout_output="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; log_info '$message'")" + [[ "$stdout_output" == *"$message"* ]] - local log_file="$HOME/.config/mole/mole.log" - [[ -f "$log_file" ]] - grep -q "INFO: $message" "$log_file" + local log_file="$HOME/.config/mole/mole.log" + [[ -f "$log_file" ]] + grep -q "INFO: $message" "$log_file" } @test "log_error writes to stderr and log file" { - local message="Something went wrong" - local stderr_file="$HOME/log_error_stderr.txt" + local message="Something went wrong" + local stderr_file="$HOME/log_error_stderr.txt" - HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; log_error '$message' 1>/dev/null 2>'$stderr_file'" + HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; log_error '$message' 1>/dev/null 2>'$stderr_file'" - [[ -s "$stderr_file" ]] - grep -q "$message" "$stderr_file" + [[ -s "$stderr_file" ]] + grep -q "$message" "$stderr_file" - local log_file="$HOME/.config/mole/mole.log" - [[ -f "$log_file" ]] - grep -q "ERROR: $message" "$log_file" + local log_file="$HOME/.config/mole/mole.log" + [[ -f "$log_file" ]] + grep -q "ERROR: $message" "$log_file" } @test "bytes_to_human converts byte counts into readable units" { - output="$(HOME="$HOME" bash --noprofile --norc <<'EOF' + output="$( + HOME="$HOME" bash --noprofile --norc << 'EOF' source "$PROJECT_ROOT/lib/common.sh" bytes_to_human 512 bytes_to_human 2048 bytes_to_human $((5 * 1024 * 1024)) bytes_to_human $((3 * 1024 * 1024 * 1024)) EOF -)" + )" - bytes_lines=() - while IFS= read -r line; do - bytes_lines+=("$line") - done <<< "$output" + bytes_lines=() + while IFS= read -r line; do + bytes_lines+=("$line") + done <<< "$output" - [ "${bytes_lines[0]}" = "512B" ] - [ "${bytes_lines[1]}" = "2KB" ] - [ "${bytes_lines[2]}" = "5.0MB" ] - [ "${bytes_lines[3]}" = "3.00GB" ] + [ "${bytes_lines[0]}" = "512B" ] + [ "${bytes_lines[1]}" = "2KB" ] + [ "${bytes_lines[2]}" = "5.0MB" ] + [ "${bytes_lines[3]}" = "3.00GB" ] } @test "create_temp_file and create_temp_dir are tracked and cleaned" { - HOME="$HOME" bash --noprofile --norc <<'EOF' + HOME="$HOME" bash --noprofile --norc << 'EOF' source "$PROJECT_ROOT/lib/common.sh" create_temp_file > "$HOME/temp_file_path.txt" create_temp_dir > "$HOME/temp_dir_path.txt" cleanup_temp_files EOF - file_path="$(cat "$HOME/temp_file_path.txt")" - dir_path="$(cat "$HOME/temp_dir_path.txt")" - [ ! -e "$file_path" ] - [ ! -e "$dir_path" ] - rm -f "$HOME/temp_file_path.txt" "$HOME/temp_dir_path.txt" + file_path="$(cat "$HOME/temp_file_path.txt")" + dir_path="$(cat "$HOME/temp_dir_path.txt")" + [ ! -e "$file_path" ] + [ ! -e "$dir_path" ] + rm -f "$HOME/temp_file_path.txt" "$HOME/temp_dir_path.txt" } @test "parallel_execute runs worker across all items" { - output_file="$HOME/parallel_output.txt" - HOME="$HOME" bash --noprofile --norc <<'EOF' + output_file="$HOME/parallel_output.txt" + HOME="$HOME" bash --noprofile --norc << 'EOF' source "$PROJECT_ROOT/lib/common.sh" worker() { echo "$1" >> "$HOME/parallel_output.txt" @@ -125,16 +126,16 @@ worker() { parallel_execute 2 worker "first" "second" "third" EOF - sort "$output_file" > "$output_file.sorted" - results=() - while IFS= read -r line; do - results+=("$line") - done < "$output_file.sorted" + sort "$output_file" > "$output_file.sorted" + results=() + while IFS= read -r line; do + results+=("$line") + done < "$output_file.sorted" - [ "${#results[@]}" -eq 3 ] - joined=" ${results[*]} " - [[ "$joined" == *" first "* ]] - [[ "$joined" == *" second "* ]] - [[ "$joined" == *" third "* ]] - rm -f "$output_file" "$output_file.sorted" + [ "${#results[@]}" -eq 3 ] + joined=" ${results[*]} " + [[ "$joined" == *" first "* ]] + [[ "$joined" == *" second "* ]] + [[ "$joined" == *" third "* ]] + rm -f "$output_file" "$output_file.sorted" } diff --git a/tests/run.sh b/tests/run.sh index 7589c19..098334c 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -4,41 +4,41 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -if command -v shellcheck >/dev/null 2>&1; then - SHELLCHECK_TARGETS=() - while IFS= read -r file; do - SHELLCHECK_TARGETS+=("$file") - done < <(find "$PROJECT_ROOT/tests" -type f \( -name '*.bats' -o -name '*.sh' \) | sort) +if command -v shellcheck > /dev/null 2>&1; then + SHELLCHECK_TARGETS=() + while IFS= read -r file; do + SHELLCHECK_TARGETS+=("$file") + done < <(find "$PROJECT_ROOT/tests" -type f \( -name '*.bats' -o -name '*.sh' \) | sort) - if [[ ${#SHELLCHECK_TARGETS[@]} -gt 0 ]]; then - shellcheck --rcfile "$PROJECT_ROOT/.shellcheckrc" "${SHELLCHECK_TARGETS[@]}" - else - echo "No shell files to lint under tests/." >&2 - fi + if [[ ${#SHELLCHECK_TARGETS[@]} -gt 0 ]]; then + shellcheck --rcfile "$PROJECT_ROOT/.shellcheckrc" "${SHELLCHECK_TARGETS[@]}" + else + echo "No shell files to lint under tests/." >&2 + fi else - echo "shellcheck not found; skipping linting." >&2 + echo "shellcheck not found; skipping linting." >&2 fi -if command -v bats >/dev/null 2>&1; then - cd "$PROJECT_ROOT" +if command -v bats > /dev/null 2>&1; then + cd "$PROJECT_ROOT" - if [[ -z "${TERM:-}" ]]; then - export TERM="xterm-256color" - fi + if [[ -z "${TERM:-}" ]]; then + export TERM="xterm-256color" + fi - if [[ $# -eq 0 ]]; then - set -- tests - fi + if [[ $# -eq 0 ]]; then + set -- tests + fi - if [[ -t 1 ]]; then - bats -p "$@" - else - TERM="${TERM:-xterm-256color}" bats --tap "$@" - fi + if [[ -t 1 ]]; then + bats -p "$@" + else + TERM="${TERM:-xterm-256color}" bats --tap "$@" + fi else - cat <<'EOF' >&2 + cat << 'EOF' >&2 bats is required to run Mole's test suite. Install via Homebrew with 'brew install bats-core' or via npm with 'npm install -g bats'. EOF - exit 1 + exit 1 fi diff --git a/tests/uninstall.bats b/tests/uninstall.bats index 6edbc39..b7f66f7 100644 --- a/tests/uninstall.bats +++ b/tests/uninstall.bats @@ -1,79 +1,81 @@ #!/usr/bin/env bats setup_file() { - PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" - export PROJECT_ROOT + PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" + export PROJECT_ROOT - ORIGINAL_HOME="${HOME:-}" - export ORIGINAL_HOME + ORIGINAL_HOME="${HOME:-}" + export ORIGINAL_HOME - HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-uninstall-home.XXXXXX")" - export HOME + HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-uninstall-home.XXXXXX")" + export HOME } teardown_file() { - rm -rf "$HOME" - if [[ -n "${ORIGINAL_HOME:-}" ]]; then - export HOME="$ORIGINAL_HOME" - fi + rm -rf "$HOME" + if [[ -n "${ORIGINAL_HOME:-}" ]]; then + export HOME="$ORIGINAL_HOME" + fi } setup() { - export TERM="dumb" - rm -rf "${HOME:?}"/* - mkdir -p "$HOME" + export TERM="dumb" + rm -rf "${HOME:?}"/* + mkdir -p "$HOME" } create_app_artifacts() { - mkdir -p "$HOME/Applications/TestApp.app" - mkdir -p "$HOME/Library/Application Support/TestApp" - mkdir -p "$HOME/Library/Caches/TestApp" - mkdir -p "$HOME/Library/Containers/com.example.TestApp" - mkdir -p "$HOME/Library/Preferences" - touch "$HOME/Library/Preferences/com.example.TestApp.plist" - mkdir -p "$HOME/Library/Preferences/ByHost" - touch "$HOME/Library/Preferences/ByHost/com.example.TestApp.ABC123.plist" - mkdir -p "$HOME/Library/Saved Application State/com.example.TestApp.savedState" + mkdir -p "$HOME/Applications/TestApp.app" + mkdir -p "$HOME/Library/Application Support/TestApp" + mkdir -p "$HOME/Library/Caches/TestApp" + mkdir -p "$HOME/Library/Containers/com.example.TestApp" + mkdir -p "$HOME/Library/Preferences" + touch "$HOME/Library/Preferences/com.example.TestApp.plist" + mkdir -p "$HOME/Library/Preferences/ByHost" + touch "$HOME/Library/Preferences/ByHost/com.example.TestApp.ABC123.plist" + mkdir -p "$HOME/Library/Saved Application State/com.example.TestApp.savedState" } @test "find_app_files discovers user-level leftovers" { - create_app_artifacts + create_app_artifacts - result="$(HOME="$HOME" bash --noprofile --norc <<'EOF' + result="$( + HOME="$HOME" bash --noprofile --norc << 'EOF' set -euo pipefail source "$PROJECT_ROOT/lib/common.sh" find_app_files "com.example.TestApp" "TestApp" EOF -)" + )" - [[ "$result" == *"Application Support/TestApp"* ]] - [[ "$result" == *"Caches/TestApp"* ]] - [[ "$result" == *"Preferences/com.example.TestApp.plist"* ]] - [[ "$result" == *"Saved Application State/com.example.TestApp.savedState"* ]] - [[ "$result" == *"Containers/com.example.TestApp"* ]] + [[ "$result" == *"Application Support/TestApp"* ]] + [[ "$result" == *"Caches/TestApp"* ]] + [[ "$result" == *"Preferences/com.example.TestApp.plist"* ]] + [[ "$result" == *"Saved Application State/com.example.TestApp.savedState"* ]] + [[ "$result" == *"Containers/com.example.TestApp"* ]] } @test "calculate_total_size returns aggregate kilobytes" { - mkdir -p "$HOME/sized" - dd if=/dev/zero of="$HOME/sized/file1" bs=1024 count=1 >/dev/null 2>&1 - dd if=/dev/zero of="$HOME/sized/file2" bs=1024 count=2 >/dev/null 2>&1 + mkdir -p "$HOME/sized" + dd if=/dev/zero of="$HOME/sized/file1" bs=1024 count=1 > /dev/null 2>&1 + dd if=/dev/zero of="$HOME/sized/file2" bs=1024 count=2 > /dev/null 2>&1 - result="$(HOME="$HOME" bash --noprofile --norc <<'EOF' + result="$( + HOME="$HOME" bash --noprofile --norc << 'EOF' set -euo pipefail source "$PROJECT_ROOT/lib/common.sh" files="$(printf '%s\n%s\n' "$HOME/sized/file1" "$HOME/sized/file2")" calculate_total_size "$files" EOF -)" + )" - # Result should be >=3 KB (some filesystems allocate slightly more) - [ "$result" -ge 3 ] + # Result should be >=3 KB (some filesystems allocate slightly more) + [ "$result" -ge 3 ] } @test "batch_uninstall_applications removes selected app data" { - create_app_artifacts + create_app_artifacts - run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF' set -euo pipefail source "$PROJECT_ROOT/lib/common.sh" source "$PROJECT_ROOT/lib/batch_uninstall.sh" @@ -111,5 +113,5 @@ printf '\n' | batch_uninstall_applications >/dev/null [[ ! -f "$HOME/Library/Preferences/com.example.TestApp.plist" ]] || exit 1 EOF - [ "$status" -eq 0 ] + [ "$status" -eq 0 ] } diff --git a/tests/update_remove.bats b/tests/update_remove.bats index 1f1e7e5..9c88a3e 100644 --- a/tests/update_remove.bats +++ b/tests/update_remove.bats @@ -1,31 +1,31 @@ #!/usr/bin/env bats setup_file() { - PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" - export PROJECT_ROOT + PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" + export PROJECT_ROOT - ORIGINAL_HOME="${HOME:-}" - export ORIGINAL_HOME + ORIGINAL_HOME="${HOME:-}" + export ORIGINAL_HOME - HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-update-home.XXXXXX")" - export HOME + HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-update-home.XXXXXX")" + export HOME } teardown_file() { - rm -rf "$HOME" - if [[ -n "${ORIGINAL_HOME:-}" ]]; then - export HOME="$ORIGINAL_HOME" - fi + rm -rf "$HOME" + if [[ -n "${ORIGINAL_HOME:-}" ]]; then + export HOME="$ORIGINAL_HOME" + fi } setup() { - export TERM="dumb" - rm -rf "${HOME:?}"/* - mkdir -p "$HOME" + export TERM="dumb" + rm -rf "${HOME:?}"/* + mkdir -p "$HOME" } @test "update_via_homebrew reports already on latest version" { - run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF' set -euo pipefail MOLE_TEST_BREW_UPDATE_OUTPUT="Updated 0 formulae" MOLE_TEST_BREW_UPGRADE_OUTPUT="Warning: mole 1.7.9 already installed" @@ -44,12 +44,12 @@ source "$PROJECT_ROOT/lib/common.sh" update_via_homebrew "1.7.9" EOF - [ "$status" -eq 0 ] - [[ "$output" == *"Already on latest version"* ]] + [ "$status" -eq 0 ] + [[ "$output" == *"Already on latest version"* ]] } @test "update_mole skips download when already latest" { - run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" PATH="$HOME/fake-bin:/usr/bin:/bin" TERM="dumb" bash --noprofile --norc <<'EOF' + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" PATH="$HOME/fake-bin:/usr/bin:/bin" TERM="dumb" bash --noprofile --norc << 'EOF' set -euo pipefail mkdir -p "$HOME/fake-bin" cat > "$HOME/fake-bin/curl" <<'SCRIPT' @@ -85,17 +85,17 @@ chmod +x "$HOME/fake-bin/brew" "$PROJECT_ROOT/mole" update EOF - [ "$status" -eq 0 ] - [[ "$output" == *"Already on latest version"* ]] + [ "$status" -eq 0 ] + [[ "$output" == *"Already on latest version"* ]] } @test "remove_mole deletes manual binaries and caches" { - mkdir -p "$HOME/.local/bin" - touch "$HOME/.local/bin/mole" - touch "$HOME/.local/bin/mo" - mkdir -p "$HOME/.config/mole" "$HOME/.cache/mole" + mkdir -p "$HOME/.local/bin" + touch "$HOME/.local/bin/mole" + touch "$HOME/.local/bin/mo" + mkdir -p "$HOME/.config/mole" "$HOME/.cache/mole" - run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" PATH="/usr/bin:/bin" bash --noprofile --norc <<'EOF' + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" PATH="/usr/bin:/bin" bash --noprofile --norc << 'EOF' set -euo pipefail start_inline_spinner() { :; } stop_inline_spinner() { :; } @@ -103,9 +103,9 @@ export -f start_inline_spinner stop_inline_spinner printf '\n' | "$PROJECT_ROOT/mole" remove EOF - [ "$status" -eq 0 ] - [ ! -f "$HOME/.local/bin/mole" ] - [ ! -f "$HOME/.local/bin/mo" ] - [ ! -d "$HOME/.config/mole" ] - [ ! -d "$HOME/.cache/mole" ] + [ "$status" -eq 0 ] + [ ! -f "$HOME/.local/bin/mole" ] + [ ! -f "$HOME/.local/bin/mo" ] + [ ! -d "$HOME/.config/mole" ] + [ ! -d "$HOME/.cache/mole" ] } diff --git a/tests/whitelist.bats b/tests/whitelist.bats index 97f083e..f6beac3 100644 --- a/tests/whitelist.bats +++ b/tests/whitelist.bats @@ -1,99 +1,99 @@ #!/usr/bin/env bats setup_file() { - PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" - export PROJECT_ROOT + PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" + export PROJECT_ROOT - ORIGINAL_HOME="${HOME:-}" - export ORIGINAL_HOME + ORIGINAL_HOME="${HOME:-}" + export ORIGINAL_HOME - HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-whitelist-home.XXXXXX")" - export HOME + HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-whitelist-home.XXXXXX")" + export HOME - mkdir -p "$HOME" + mkdir -p "$HOME" } teardown_file() { - rm -rf "$HOME" - if [[ -n "${ORIGINAL_HOME:-}" ]]; then - export HOME="$ORIGINAL_HOME" - fi + rm -rf "$HOME" + if [[ -n "${ORIGINAL_HOME:-}" ]]; then + export HOME="$ORIGINAL_HOME" + fi } setup() { - rm -rf "$HOME/.config" - mkdir -p "$HOME" - WHITELIST_PATH="$HOME/.config/mole/whitelist" + rm -rf "$HOME/.config" + mkdir -p "$HOME" + WHITELIST_PATH="$HOME/.config/mole/whitelist" } @test "patterns_equivalent treats paths with tilde expansion as equal" { - local status - if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; patterns_equivalent '~/.cache/test' \"\$HOME/.cache/test\""; then - status=0 - else - status=$? - fi - [ "$status" -eq 0 ] + local status + if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; patterns_equivalent '~/.cache/test' \"\$HOME/.cache/test\""; then + status=0 + else + status=$? + fi + [ "$status" -eq 0 ] } @test "patterns_equivalent distinguishes different paths" { - local status - if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; patterns_equivalent '~/.cache/test' \"\$HOME/.cache/other\""; then - status=0 - else - status=$? - fi - [ "$status" -ne 0 ] + local status + if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; patterns_equivalent '~/.cache/test' \"\$HOME/.cache/other\""; then + status=0 + else + status=$? + fi + [ "$status" -ne 0 ] } @test "save_whitelist_patterns keeps unique entries and preserves header" { - HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; save_whitelist_patterns \"\$HOME/.cache/foo\" \"\$HOME/.cache/foo\" \"\$HOME/.cache/bar\"" + HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; save_whitelist_patterns \"\$HOME/.cache/foo\" \"\$HOME/.cache/foo\" \"\$HOME/.cache/bar\"" - [[ -f "$WHITELIST_PATH" ]] + [[ -f "$WHITELIST_PATH" ]] - lines=() - while IFS= read -r line; do - lines+=("$line") - done < "$WHITELIST_PATH" - # Header is at least two lines (comments), plus two unique patterns - [ "${#lines[@]}" -ge 4 ] - # Ensure duplicate was not written twice - occurrences=$(grep -c "$HOME/.cache/foo" "$WHITELIST_PATH") - [ "$occurrences" -eq 1 ] + lines=() + while IFS= read -r line; do + lines+=("$line") + done < "$WHITELIST_PATH" + # Header is at least two lines (comments), plus two unique patterns + [ "${#lines[@]}" -ge 4 ] + # Ensure duplicate was not written twice + occurrences=$(grep -c "$HOME/.cache/foo" "$WHITELIST_PATH") + [ "$occurrences" -eq 1 ] } @test "load_whitelist falls back to defaults when config missing" { - rm -f "$WHITELIST_PATH" - HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; rm -f \"\$HOME/.config/mole/whitelist\"; load_whitelist; printf '%s\n' \"\${CURRENT_WHITELIST_PATTERNS[@]}\"" > "$HOME/current_whitelist.txt" - HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; printf '%s\n' \"\${DEFAULT_WHITELIST_PATTERNS[@]}\"" > "$HOME/default_whitelist.txt" + rm -f "$WHITELIST_PATH" + HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; rm -f \"\$HOME/.config/mole/whitelist\"; load_whitelist; printf '%s\n' \"\${CURRENT_WHITELIST_PATTERNS[@]}\"" > "$HOME/current_whitelist.txt" + HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; printf '%s\n' \"\${DEFAULT_WHITELIST_PATTERNS[@]}\"" > "$HOME/default_whitelist.txt" - current=() - while IFS= read -r line; do - current+=("$line") - done < "$HOME/current_whitelist.txt" + current=() + while IFS= read -r line; do + current+=("$line") + done < "$HOME/current_whitelist.txt" - defaults=() - while IFS= read -r line; do - defaults+=("$line") - done < "$HOME/default_whitelist.txt" + defaults=() + while IFS= read -r line; do + defaults+=("$line") + done < "$HOME/default_whitelist.txt" - [ "${#current[@]}" -eq "${#defaults[@]}" ] - [ "${current[0]}" = "${defaults[0]/\$HOME/$HOME}" ] + [ "${#current[@]}" -eq "${#defaults[@]}" ] + [ "${current[0]}" = "${defaults[0]/\$HOME/$HOME}" ] } @test "is_whitelisted matches saved patterns exactly" { - local status - if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; save_whitelist_patterns \"\$HOME/.cache/unique-pattern\"; load_whitelist; is_whitelisted \"\$HOME/.cache/unique-pattern\""; then - status=0 - else - status=$? - fi - [ "$status" -eq 0 ] + local status + if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; save_whitelist_patterns \"\$HOME/.cache/unique-pattern\"; load_whitelist; is_whitelisted \"\$HOME/.cache/unique-pattern\""; then + status=0 + else + status=$? + fi + [ "$status" -eq 0 ] - if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; save_whitelist_patterns \"\$HOME/.cache/unique-pattern\"; load_whitelist; is_whitelisted \"\$HOME/.cache/other-pattern\""; then - status=0 - else - status=$? - fi - [ "$status" -ne 0 ] + if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; save_whitelist_patterns \"\$HOME/.cache/unique-pattern\"; load_whitelist; is_whitelisted \"\$HOME/.cache/other-pattern\""; then + status=0 + else + status=$? + fi + [ "$status" -ne 0 ] } diff --git a/tests/z_shellcheck.bats b/tests/z_shellcheck.bats index 11253c5..8b96030 100644 --- a/tests/z_shellcheck.bats +++ b/tests/z_shellcheck.bats @@ -1,16 +1,16 @@ #!/usr/bin/env bats setup_file() { - PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" - export PROJECT_ROOT + PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" + export PROJECT_ROOT } @test "shellcheck passes for test scripts" { - if ! command -v shellcheck >/dev/null 2>&1; then - skip "shellcheck not installed" - fi + if ! command -v shellcheck > /dev/null 2>&1; then + skip "shellcheck not installed" + fi - run env PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' + run env PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF' set -euo pipefail cd "$PROJECT_ROOT" targets=() @@ -24,6 +24,6 @@ fi shellcheck --rcfile "$PROJECT_ROOT/.shellcheckrc" "${targets[@]}" EOF - printf '%s\n' "$output" >&3 - [ "$status" -eq 0 ] + printf '%s\n' "$output" >&3 + [ "$status" -eq 0 ] }