You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:48:46