macos: normalize command paths as file URLs

#13319
#13748

Normalize command-line file arguments as file URLs internally while
keeping the AppKit and FileManager string boundaries unchanged.

This handles relative paths, URL-sensitive characters, and trailing
directory separators consistently when matching duplicate open-file
events.
This commit is contained in:
Mitchell Hashimoto
2026-08-11 09:15:05 -07:00
parent a858bd4d35
commit 8c9fd7aa79
2 changed files with 39 additions and 15 deletions

View File

@@ -4,14 +4,18 @@ import Foundation
/// `-e`. Each matching event is consumed once so later requests to open the same
/// file are handled normally.
final class CommandLineOpenFileFilter {
private let workingDirectory: String
private var filesToIgnore: Set<String>
private let workingDirectory: URL
private var filesToIgnore: Set<URL>
init(
arguments: [String],
workingDirectory: String,
fileExists: (String) -> Bool
) {
let workingDirectory = URL(
filePath: workingDirectory,
directoryHint: .isDirectory
).absoluteURL.standardizedFileURL
self.workingDirectory = workingDirectory
guard let commandIndex = arguments.firstIndex(of: "-e") else {
@@ -25,32 +29,31 @@ final class CommandLineOpenFileFilter {
self.filesToIgnore = Set(arguments[arguments.index(after: commandIndex)...]
.compactMap { argument in
// Command arguments can be relative, while AppKit normally
// reports absolute paths for the corresponding open event.
let path = Self.absolutePath(argument, relativeTo: workingDirectory)
// reports absolute paths for the corresponding open event. Use
// file URLs internally so both forms have the same identity.
let url = Self.fileURL(argument, relativeTo: workingDirectory)
// Ignore only paths that exist during launch. A non-path
// argument cannot produce the duplicate event and retaining it
// could suppress a legitimate open if that path appears later.
return fileExists(path) ? path : nil
return fileExists(url.path) ? url : nil
})
}
func shouldIgnore(_ filename: String) -> Bool {
let path = Self.absolutePath(filename, relativeTo: workingDirectory)
let url = Self.fileURL(filename, relativeTo: workingDirectory)
// Consume each match once. Later requests to open the same file may
// come from Finder, the Dock, or another invocation and must proceed.
return filesToIgnore.remove(path) != nil
return filesToIgnore.remove(url) != nil
}
private static func absolutePath(_ path: String, relativeTo workingDirectory: String) -> String {
private static func fileURL(_ path: String, relativeTo workingDirectory: URL) -> URL {
let expanded = (path as NSString).expandingTildeInPath
let absolute = if (expanded as NSString).isAbsolutePath {
expanded
} else {
(workingDirectory as NSString).appendingPathComponent(expanded)
}
return (absolute as NSString).standardizingPath
return URL(
filePath: expanded,
directoryHint: .notDirectory,
relativeTo: workingDirectory
).absoluteURL.standardizedFileURL
}
}

View File

@@ -61,4 +61,25 @@ struct CommandLineOpenFileFilterTests {
#expect(!filter.shouldIgnore("/tmp/finder-file.txt"))
#expect(filter.shouldIgnore("/tmp/command-file.txt"))
}
@Test func normalizesFileURLs() {
let existing: Set<String> = [
"/tmp/project/file #100%.txt",
"/tmp/project/directory",
]
let filter = CommandLineOpenFileFilter(
arguments: [
"ghostty",
"-e",
"command",
"./file #100%.txt",
"./directory/",
],
workingDirectory: "/tmp/project",
fileExists: { existing.contains($0) }
)
#expect(filter.shouldIgnore("/tmp/project/file #100%.txt"))
#expect(filter.shouldIgnore("/tmp/project/directory"))
}
}