├── screenshots └── scan-leftovers.jpg ├── cmd └── brew-scan-leftovers.rb └── README.md /screenshots/scan-leftovers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jysperm/homebrew-leftover-scanner/HEAD/screenshots/scan-leftovers.jpg -------------------------------------------------------------------------------- /cmd/brew-scan-leftovers.rb: -------------------------------------------------------------------------------- 1 | require "cask/caskroom" 2 | require "livecheck/livecheck" 3 | 4 | class Uninstaller < Cask::Artifact::AbstractUninstall 5 | def scan_paths(paths) 6 | result = [] 7 | 8 | each_resolved_path(:scan, paths) do |path, resolved_paths| 9 | if $paths_being_used.include? path 10 | odebug "Skipped path being used: #{path}" 11 | else 12 | result.push(*resolved_paths) 13 | end 14 | end 15 | 16 | result 17 | end 18 | end 19 | 20 | def get_all_casks 21 | CoreCaskTap.instance.cask_files.map do |f| 22 | Cask::CaskLoader::FromTapPathLoader.new(f).load(config: nil) 23 | rescue Cask::CaskUnreadableError => e 24 | opoo e.message 25 | 26 | nil 27 | end.compact 28 | end 29 | 30 | def cask_artifacts_exists?(cask) 31 | app_artifacts = cask.artifacts.select { |a| a.is_a?(Cask::Artifact::App) } 32 | 33 | app_artifacts.map do |app_artifact| 34 | if Cask::Utils.path_occupied?(app_artifact.target) 35 | odebug "#{app_artifact.target} from #{Formatter.identifier(cask.token)} exists" 36 | return true 37 | end 38 | end 39 | 40 | return false 41 | end 42 | 43 | def get_cask_uninstall_paths(cask, type) 44 | uninstall_paths = [] 45 | 46 | stanzas = cask.artifacts.select do |a| 47 | a.is_a?(Cask::Artifact::Zap) or a.is_a?(Cask::Artifact::Uninstall) 48 | end 49 | 50 | stanzas.each do |stanza| 51 | uninstall_paths.push *stanza.directives[type] 52 | end 53 | 54 | return uninstall_paths 55 | end 56 | 57 | def scan_cask(cask) 58 | if cask.artifacts.find { |a| a.is_a?(Cask::Artifact::App) } 59 | odebug "Searching #{cask.token} ..." 60 | 61 | begin 62 | delete_paths = Uninstaller.from_args(cask).scan_paths get_cask_uninstall_paths cask, :delete 63 | trash_paths = Uninstaller.from_args(cask).scan_paths get_cask_uninstall_paths cask, :trash 64 | 65 | if delete_paths.length + trash_paths.length > 0 66 | ohai "Found leftovers from #{Formatter.identifier(cask.token)}, get rid of them via:" 67 | ohai "#{Formatter.identifier("brew uninstall -f --zap #{cask.token}")}" 68 | puts delete_paths.map { |path| "#{path} (delete #{path.abv})" } 69 | puts trash_paths.map { |path| "#{path} (trash #{path.abv})" } 70 | end 71 | rescue Exception => e 72 | ofail e 73 | end 74 | else 75 | odebug "Skipped #{cask} because no app artifact in cask" 76 | end 77 | end 78 | 79 | $all_casks = get_all_casks 80 | 81 | $casks_installed = Set.new 82 | $paths_being_used = Set.new 83 | 84 | ohai "#{$all_casks.length} casks to scan ..." 85 | 86 | def scan_install_casks 87 | cask_installed = Set.new 88 | cask_artifacts_exists = Set.new 89 | 90 | $all_casks.each do |cask| 91 | if cask.installed? 92 | cask_installed.add(cask.token) 93 | $paths_being_used.merge(get_cask_uninstall_paths(cask, :delete)) 94 | $paths_being_used.merge(get_cask_uninstall_paths(cask, :trash)) 95 | elsif cask_artifacts_exists?(cask) 96 | cask_artifacts_exists.add(cask.token) 97 | $paths_being_used.merge(get_cask_uninstall_paths(cask, :delete)) 98 | $paths_being_used.merge(get_cask_uninstall_paths(cask, :trash)) 99 | end 100 | end 101 | 102 | ohai "Installed from cask:" 103 | puts cask_installed.to_a.join ', ' 104 | ohai "Installed from other ways:" 105 | puts cask_artifacts_exists.to_a.join ', ' 106 | end 107 | 108 | scan_install_casks 109 | 110 | $all_casks.each do |cask| 111 | scan_cask cask 112 | end 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Homebrew Leftover Scanner 2 | Use rules from Homebrew Cask to scan for leftover files from uninstalled software. 3 | 4 | 3413 casks are supported now. 5 | 6 | Currently an MVP version, TODO list: 7 | 8 | - Support detecting `launchctl` and `login_item` 9 | 10 | Known issues: 11 | 12 | - JetBrains' IDEs will be detected as uninstalled if you install them via JetBrains Toolbox 13 | 14 | ![Screenshot](https://raw.githubusercontent.com/jysperm/homebrew-leftover-scanner/main/screenshots/scan-leftovers.jpg) 15 | 16 | ## Install & Usages 17 | 18 | Install via `brew tap`: 19 | 20 | ``` 21 | brew tap jysperm/leftover-scanner 22 | brew tap homebrew/cask # If you haven't tapped before (Homebrew don't do this by default since 4.0) 23 | ``` 24 | 25 | Run it: 26 | 27 | ``` 28 | brew scan-leftovers 29 | ``` 30 | 31 | This script doesn't actually delete files, you can follow the instructions in the output to run `brew uninstall` (at your own risk): 32 | 33 | ``` 34 | $ brew scan-leftovers 35 | ==> 4154 casks to scan ... 36 | ==> Installed from cask: 37 | netspot, slack, sketch, steam, brave-browser, powerphotos, downie, paw, tg-pro, clashx, imazing, visual-studio-code, electrum, logseq, handbrake, obs, netnewswire, iterm2, numi, gitup, docker, blender, telegram, discord, wireshark, firefox, iina, google-chrome, zoom, grammarly, squirrel, bettertouchtool, keka, xbar 38 | ==> Installed from other ways: 39 | bitwarden, wechat, planet, qq, medis 40 | ==> Found leftovers from bitbar, get rid of them via: 41 | ==> brew uninstall -f --zap bitbar 42 | /Users/jysperm/Library/Caches/com.matryer.BitBar (trash 3 files, 84.2KB) 43 | /Users/jysperm/Library/Preferences/com.matryer.BitBar.plist (trash 531B) 44 | ==> Found leftovers from epic-games, get rid of them via: 45 | ==> brew uninstall -f --zap epic-games 46 | /Users/jysperm/Library/Application Support/Epic (trash 264B) 47 | ==> Found leftovers from setapp, get rid of them via: 48 | ==> brew uninstall -f --zap setapp 49 | /Users/jysperm/Library/Application Scripts/com.setapp.DesktopClient.SetappAgent.FinderSyncExt (trash 64B) 50 | /Users/jysperm/Library/Caches/com.setapp.DesktopClient (trash 3 files, 84.2KB) 51 | /Users/jysperm/Library/Caches/com.setapp.DesktopClient.SetappAgent (trash 4 files, 6.1MB) 52 | /Users/jysperm/Library/Logs/Setapp (trash 7 files, 344.8KB) 53 | ``` 54 | 55 | ### Full Disk Access 56 | Full Disk Access is required for this script to scan paths across the entire file system. 57 | 58 | Please enable Full Disk Access for your terminal under System Preferences > Security & Privacy > Privacy > Full Disk Access. 59 | 60 | ## About the rules 61 | Most Homebrew casks have a `zap` section, it contains the cache files or logs of that software which can be deleted when you are no longer using it. 62 | 63 | However `brew` doesn't delete these files by default, so the `zap` section may not be well maintained. If you find any issues, you can contribute to the official [homebrew-cask](https://github.com/Homebrew/homebrew-cask) repository. 64 | 65 | ``` 66 | $ brew cat bitbar 67 | cask "bitbar" do 68 | version "1.10.1" 69 | sha256 "8a7013dca92715ba80cccef98b84dd1bc8d0b4c4b603f732e006eb204bab43fa" 70 | 71 | url "https://github.com/matryer/bitbar/releases/download/v#{version}/BitBar.app.zip" 72 | name "BitBar" 73 | desc "Utility to display the output from any script or program in the menu bar" 74 | homepage "https://github.com/matryer/bitbar/" 75 | 76 | app "BitBar.app" 77 | 78 | zap trash: [ 79 | "~/Library/BitBar Plugins", 80 | "~/Library/Caches/com.matryer.BitBar", 81 | "~/Library/Preferences/com.matryer.BitBar.plist", 82 | ] 83 | end 84 | ``` 85 | --------------------------------------------------------------------------------