Repository navigation
[BUG] pear CLI uses a separate UserDefaults domain from the app #590
Description
New Issue Checklist
- I updated Pearcleaner to the latest version and still observe the issue
- I searched existing issues, pull requests, and discussions
- OS Version: macOS 27.0
- Pearcleaner Version: 5.4.3 (121)
Issue:
/usr/local/bin/pear and the app don't use the same preferences domain.
The app uses:
com.alienator88.Pearcleaner.plist
The symlinked CLI uses:
pear.plist
As a result, pear list-orphaned doesn't see exclusions added through Settings.
Steps:
-
Add a path to the orphan exception list in Settings.
-
Confirm it's stored in the app domain:
defaults read com.alienator88.Pearcleaner settings.folders.zombie -
Run both forms of the executable:
/usr/local/bin/pear list-orphaned /Applications/Pearcleaner.app/Contents/MacOS/Pearcleaner list-orphaned
The symlinked CLI includes the excluded path. The direct app executable omits it.
The installed CLI points to that same executable:
$ readlink /usr/local/bin/pear
/Applications/Pearcleaner.app/Contents/MacOS/PearcleanerAs a control, copying the same settings.folders.zombie array into the pear domain made /usr/local/bin/pear omit the excluded path. I restored the test preference afterward.
Source
FolderSettingsManager loads the folder settings from UserDefaults.standard:
Pearcleaner/Pearcleaner/Views/Settings/Folders.swift
Lines 410 to 438 in c52695d
| class FolderSettingsManager: ObservableObject { | |
| static let shared = FolderSettingsManager() | |
| @Published var folderPaths: [String] = [] | |
| @Published var fileFolderPathsZ: [String] = [] | |
| @Published var fileFolderPathsApps: [String] = [] | |
| private let appsKey = "settings.folders.apps" | |
| private let zombieKey = "settings.folders.zombie" | |
| private let appsExclusionKey = "settings.folders.appsExclusion" | |
| let defaultPaths = ["/Applications", "\(NSHomeDirectory())/Applications"] | |
| init() { | |
| loadDefaultPathsIfNeeded() | |
| } | |
| // Application folders ////////////////////////////////////////////////////////////////////////////////// | |
| private func loadDefaultPathsIfNeeded() { | |
| var appsPaths = UserDefaults.standard.stringArray(forKey: appsKey) ?? defaultPaths | |
| let zombiePaths = UserDefaults.standard.stringArray(forKey: zombieKey) ?? [] | |
| let appsExclusionPaths = UserDefaults.standard.stringArray(forKey: appsExclusionKey) ?? [] | |
| if appsPaths.count < 2 { | |
| appsPaths = defaultPaths | |
| } | |
| UserDefaults.standard.set(appsPaths, forKey: appsKey) | |
| self.folderPaths = appsPaths | |
| self.fileFolderPathsZ = zombiePaths | |
| self.fileFolderPathsApps = appsExclusionPaths |
The CLI creates that manager during launch:
Pearcleaner/Pearcleaner/Logic/Logic.swift
Lines 583 to 620 in c52695d
| // Process CLI // ======================================================================================================== | |
| func handleLaunchMode() { | |
| var arguments = CommandLine.arguments | |
| // Filter out arguments that break CLI commands on startup | |
| arguments = arguments.filter { | |
| !["-NSDocumentRevisionsDebugMode", "YES", "-AppleTextDirection", "NO"].contains($0) | |
| } | |
| if let langIndex = arguments.firstIndex(of: "-AppleLanguages"), langIndex + 1 < arguments.count { | |
| arguments.remove(at: langIndex) | |
| arguments.remove(at: langIndex) | |
| } | |
| let termType = ProcessInfo.processInfo.environment["TERM"] | |
| let isRunningInTerminal = termType != nil && termType != "dumb" | |
| // Check if any CLI command arguments are present (even if not in terminal) | |
| // This handles cases like SUDO_ASKPASS where the app is launched without a terminal | |
| let cliCommands = ["uninstall", "list", "search", "help", "ask-password"] | |
| let hasCLICommand = arguments.dropFirst().contains { arg in | |
| cliCommands.contains(arg) || arg.hasPrefix("--") | |
| } | |
| if isRunningInTerminal || hasCLICommand { | |
| let locations = Locations() | |
| let fsm = FolderSettingsManager() | |
| PearCLI.setupDependencies(locations: locations, fsm: fsm) | |
| do { | |
| // Drop the program name as to not interfere with argument parsing | |
| let args = Array(arguments.dropFirst()) | |
| var command = try PearCLI.parseAsRoot(args) | |
| // Run the command if no errors in parsing were caught | |
| try command.run() | |
| } catch { | |
| PearCLI.exit(withError: error) // Cli exit | |
| } |
The orphan search reads its exclusion list from the manager:
Pearcleaner/Pearcleaner/Logic/ReversePathsFetch.swift
Lines 25 to 28 in c52695d
| // Cached formatted data to avoid repeated .pearFormat() calls | |
| private lazy var formattedExclusionList: [String] = { | |
| fsm.fileFolderPathsZ.map { $0.pearFormat() } | |
| }() |
The same initializer loads settings.folders.apps and settings.folders.appsExclusion. I only directly tested settings.folders.zombie.
Using an explicit shared suite for these settings would avoid the domain changing with the executable's invocation name.
Related
- [BUG] pear.plist shown as orphaned file #445 reported
pear.plistas an orphan and was closed on the assumption that it came from an older build. Version 5.4.3 still creates and reads it when invoked as/usr/local/bin/pear. - Fix race in CLI list-orphaned / remove-orphaned that produced false positives #572 covers the separate app-discovery race in the orphan CLI commands.
Screenshots:
Not applicable.
Debug Console
Not applicable.
Console Logs
Not applicable.
Activity