求Ruby的Pathname.relative_path_from的纯Swift等效实现(支持Linux)
我刚好在Linux上用Swift解决过类似的相对路径计算问题,确实Swift自带的FileManager.getRelationship(_:of:in:toItemAt:)没法直接返回相对路径,得自己实现逻辑。下面是我打磨后的纯Swift解决方案,支持file://格式URL和普通路径字符串,还能适配大小写敏感/不敏感的文件系统:
核心实现思路
要准确计算相对路径,关键步骤是:
- 先把输入的路径(不管是URL还是字符串)统一转换成标准化的路径组件数组,自动处理掉
.和..这类相对片段 - 找到两个路径的最长公共前缀组件(根据文件系统是否大小写敏感来匹配)
- 对基准路径中超出公共前缀的每个组件,添加
../来回溯 - 最后拼接目标路径中超出公共前缀的组件,得到最终的相对路径
完整代码实现
import Foundation extension URL { func relativePath(from baseURL: URL) throws -> String { // 只支持file://格式的URL guard self.isFileURL, baseURL.isFileURL else { throw NSError(domain: "RelativePathError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Only file URLs are supported"]) } // 获取标准化后的路径组件(自动处理.和..) let targetComponents = self.standardized.pathComponents let baseComponents = baseURL.standardized.pathComponents // 判断文件系统是否大小写敏感 let fileManager = FileManager.default let isCaseSensitive: Bool do { let attributes = try fileManager.attributesOfItem(atPath: baseURL.path) isCaseSensitive = attributes[.systemFileCaseSensitive] as? Bool ?? true } catch { // 若无法获取文件系统属性,默认使用Linux常见的大小写敏感配置 isCaseSensitive = true } // 寻找最长公共前缀的索引 var commonPrefixIndex = 0 while commonPrefixIndex < targetComponents.count && commonPrefixIndex < baseComponents.count { let targetComp = targetComponents[commonPrefixIndex] let baseComp = baseComponents[commonPrefixIndex] let componentsMatch: Bool if isCaseSensitive { componentsMatch = targetComp == baseComp } else { componentsMatch = targetComp.lowercased() == baseComp.lowercased() } guard componentsMatch else { break } commonPrefixIndex += 1 } // 构建相对路径组件 var relativeComponents: [String] = [] // 添加回溯的../:基准路径超出公共前缀的组件数(减去1是因为最后一个组件是目录本身) let backtrackCount = baseComponents.count - commonPrefixIndex - 1 relativeComponents.append(contentsOf: Array(repeating: "..", count: backtrackCount)) // 添加目标路径超出公共前缀的组件 relativeComponents.append(contentsOf: targetComponents[commonPrefixIndex...]) // 拼接成最终路径,处理根目录前缀问题 let relativePath = relativeComponents.joined(separator: "/") return relativePath.hasPrefix("/") ? String(relativePath.dropFirst()) : relativePath } } // 适配普通字符串路径的扩展 extension String { func relativePath(from basePath: String) throws -> String { // 尝试解析为URL,优先处理file://格式,不行则作为本地路径 guard let targetURL = URL(string: self) ?? URL(fileURLWithPath: self), let baseURL = URL(string: basePath) ?? URL(fileURLWithPath: basePath) else { throw NSError(domain: "RelativePathError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid path format"]) } return try targetURL.relativePath(from: baseURL) } }
测试示例验证
你可以用提供的测试用例验证效果:
// 测试用例1 do { let result = try "/usr/X11/agent/47.gz".relativePath(from: "/usr/X11") print(result) // 输出: agent/47.gz } catch { print(error.localizedDescription) } // 测试用例2 do { let result = try "/usr/share/man/meltdown.1".relativePath(from: "/usr/share/cups") print(result) // 输出: ../man/meltdown.1 } catch { print(error.localizedDescription) } // 测试用例3(file:// URL) do { let targetURL = URL(string: "file:///var/logs/x/y/z/log.txt")! let baseURL = URL(string: "file:///var/logs")! let result = try targetURL.relativePath(from: baseURL) print(result) // 输出: x/y/z/log.txt } catch { print(error.localizedDescription) }
关键细节说明
- 大小写敏感适配:通过读取文件系统的
systemFileCaseSensitive属性判断匹配规则,若获取失败则默认Linux的大小写敏感配置 - 路径标准化:利用
URL.standardized自动处理路径中的.、..和重复的斜杠,确保路径格式统一 - 多输入格式支持:同时兼容
file://格式URL和本地路径字符串,自动识别解析
内容的提问来源于stack exchange,提问作者neoneye
相关产品推荐
相关产品推荐

