Swift 相当于 Ruby 的 "Pathname.relative_path_from"
Swift equivalent of Ruby's "Pathname.relative_path_from"
Ruby 的 Pathname.relative_path_from 文档。
objc 中已经有 KSFileUtilities 的 ks_stringRelativeToURL 方法,非常接近。我正在寻找可以 运行 on Linux 的纯 swift 解决方案。
我更喜欢使用 file://
URL 的解决方案,但 String
也可以。
文件系统可以是 case sensitive/insensitive。确定相对路径可能很棘手。
输入和预期输出示例:
| Long Path | Relative to Path | Return Value |
|--------------------------------|------------------|-------------------|
| /usr/X11/agent/47.gz | /usr/X11 | agent/47.gz |
| /usr/share/man/meltdown.1 | /usr/share/cups | ../man/meltdown.1 |
| file:///var/logs/x/y/z/log.txt | file:///var/logs | x/y/z/log.txt |
Swift 已经有 FileManager.getRelationship(_:of:in:toItemAt:),但它没有 return 相对路径。
Swift标准库或
基础框架,据我所知
这是URL
的扩展方法的可能实现:
extension URL {
func relativePath(from base: URL) -> String? {
// Ensure that both URLs represent files:
guard self.isFileURL && base.isFileURL else {
return nil
}
// Remove/replace "." and "..", make paths absolute:
let destComponents = self.standardized.pathComponents
let baseComponents = base.standardized.pathComponents
// Find number of common path components:
var i = 0
while i < destComponents.count && i < baseComponents.count
&& destComponents[i] == baseComponents[i] {
i += 1
}
// Build relative path:
var relComponents = Array(repeating: "..", count: baseComponents.count - i)
relComponents.append(contentsOf: destComponents[i...])
return relComponents.joined(separator: "/")
}
}
我的测试代码:
func test(_ p1: String, _ p2: String) {
let u1 = URL(fileURLWithPath: p1)
let u2 = URL(fileURLWithPath: p2)
print(u1.relativePath(from: u2) ?? "<ERROR>")
}
test("/usr/X11/agent/47.gz", "/usr/X11") // "agent/47.gz"
test("/usr/share/man/meltdown.1", "/usr/share/cups") // "../man/meltdown.1"
test("/var/logs/x/y/z/log.txt", "/var/logs") // "x/y/z/log.txt"
备注:
- “。”和 ".." 在给定的 URLs 被删除,并且相关文件 URLs
是绝对的(都使用
URL
的 standardized
方法)。
- 区分大小写未处理。
- 假设基础URL代表一个目录。
附录: @neoneye 将其包装到 Swift 包中:
SwiftyRelativePath.
Martin R 的答案是正确的。但是,当基础 URL 本身就是一个文件时,我遇到了一个问题。因此,我做了一些调整:
func relativePath(from base: URL) -> String? {
// Ensure that both URLs represent files:
guard self.isFileURL && base.isFileURL else {
return nil
}
//this is the new part, clearly, need to use workBase in lower part
var workBase = base
if workBase.pathExtension != "" {
workBase = workBase.deletingLastPathComponent()
}
// Remove/replace "." and "..", make paths absolute:
let destComponents = self.standardized.resolvingSymlinksInPath().pathComponents
let baseComponents = workBase.standardized.resolvingSymlinksInPath().pathComponents
// Find number of common path components:
var i = 0
while i < destComponents.count &&
i < baseComponents.count &&
destComponents[i] == baseComponents[i] {
i += 1
}
// Build relative path:
var relComponents = Array(repeating: "..", count: baseComponents.count - i)
relComponents.append(contentsOf: destComponents[i...])
return relComponents.joined(separator: "/")
}
我的测试用例有点扩展。案例 4 是我进行此小更改的触发因素。
func testRelativePath() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
func test(_ p1: String, _ p2: String,_ result: String,_ nr: Int) {
let u1 = URL(fileURLWithPath: p1)
let u2 = URL(fileURLWithPath: p2)
let r = u1.relativePath(from: u2)!
XCTAssert( r == result,"\(nr): '\(r)' != '\(result)'")
}
test("/usr/X11/agent/47.gz", "/usr/X11","agent/47.gz", 1)
test("/usr/share/man/meltdown.1", "/usr/share/cups", "../man/meltdown.1",2 )
test("/var/logs/x/y/z/log.txt", "/var/logs", "x/y/z/log.txt",3)
test("/usr/embedded.jpg", "/usr/main.html", "embedded.jpg",4)
test("/usr/embedded.jpg", "/usr", "embedded.jpg",5)
test("~/Downloads/resources", "~/", "Downloads/resources",6)
test("~/Downloads/embedded.jpg", "~/Downloads/main.html", "embedded.jpg",7)
test("/private/var/logs/x/y/z/log.txt", "/var/logs", "x/y/z/log.txt",8)
}
我使用的是 Wizard of Kneup 的版本,但是当基本目录有扩展名时遇到了问题。所以我添加代码来检查路径是否存在并且是一个目录。
public extension URL
{
func relativePath(from base: URL) -> String?
{
// Ensure that both URLs represent files:
guard self.isFileURL &&
FileManager.default.fileExists(atPath: self.path) else
{
NSLog("self is not a fileURL or it does not exists")
return nil
}
var isDir = ObjCBool(true)
guard FileManager.default.fileExists(atPath: base.path, isDirectory: &isDir) &&
isDir.boolValue else
{
NSLog("base is not a directory or it does not exists")
return nil
}
// Remove/replace "." and "..", make paths absolute:
let destComponents = self.resolvingSymlinksInPath().pathComponents
let baseComponents = base.resolvingSymlinksInPath().pathComponents
// Find number of common path components:
let i = Set(destComponents).intersection(Set(baseComponents)).count
// Build relative path:
let relComponents = Array(repeating: "..", count: baseComponents.count - i) +
destComponents[i...]
return relComponents.joined(separator: "/")
}
}
Ruby 的 Pathname.relative_path_from 文档。
objc 中已经有 KSFileUtilities 的 ks_stringRelativeToURL 方法,非常接近。我正在寻找可以 运行 on Linux 的纯 swift 解决方案。
我更喜欢使用 file://
URL 的解决方案,但 String
也可以。
文件系统可以是 case sensitive/insensitive。确定相对路径可能很棘手。
输入和预期输出示例:
| Long Path | Relative to Path | Return Value |
|--------------------------------|------------------|-------------------|
| /usr/X11/agent/47.gz | /usr/X11 | agent/47.gz |
| /usr/share/man/meltdown.1 | /usr/share/cups | ../man/meltdown.1 |
| file:///var/logs/x/y/z/log.txt | file:///var/logs | x/y/z/log.txt |
Swift 已经有 FileManager.getRelationship(_:of:in:toItemAt:),但它没有 return 相对路径。
Swift标准库或 基础框架,据我所知
这是URL
的扩展方法的可能实现:
extension URL {
func relativePath(from base: URL) -> String? {
// Ensure that both URLs represent files:
guard self.isFileURL && base.isFileURL else {
return nil
}
// Remove/replace "." and "..", make paths absolute:
let destComponents = self.standardized.pathComponents
let baseComponents = base.standardized.pathComponents
// Find number of common path components:
var i = 0
while i < destComponents.count && i < baseComponents.count
&& destComponents[i] == baseComponents[i] {
i += 1
}
// Build relative path:
var relComponents = Array(repeating: "..", count: baseComponents.count - i)
relComponents.append(contentsOf: destComponents[i...])
return relComponents.joined(separator: "/")
}
}
我的测试代码:
func test(_ p1: String, _ p2: String) {
let u1 = URL(fileURLWithPath: p1)
let u2 = URL(fileURLWithPath: p2)
print(u1.relativePath(from: u2) ?? "<ERROR>")
}
test("/usr/X11/agent/47.gz", "/usr/X11") // "agent/47.gz"
test("/usr/share/man/meltdown.1", "/usr/share/cups") // "../man/meltdown.1"
test("/var/logs/x/y/z/log.txt", "/var/logs") // "x/y/z/log.txt"
备注:
- “。”和 ".." 在给定的 URLs 被删除,并且相关文件 URLs
是绝对的(都使用
URL
的standardized
方法)。 - 区分大小写未处理。
- 假设基础URL代表一个目录。
附录: @neoneye 将其包装到 Swift 包中: SwiftyRelativePath.
Martin R 的答案是正确的。但是,当基础 URL 本身就是一个文件时,我遇到了一个问题。因此,我做了一些调整:
func relativePath(from base: URL) -> String? {
// Ensure that both URLs represent files:
guard self.isFileURL && base.isFileURL else {
return nil
}
//this is the new part, clearly, need to use workBase in lower part
var workBase = base
if workBase.pathExtension != "" {
workBase = workBase.deletingLastPathComponent()
}
// Remove/replace "." and "..", make paths absolute:
let destComponents = self.standardized.resolvingSymlinksInPath().pathComponents
let baseComponents = workBase.standardized.resolvingSymlinksInPath().pathComponents
// Find number of common path components:
var i = 0
while i < destComponents.count &&
i < baseComponents.count &&
destComponents[i] == baseComponents[i] {
i += 1
}
// Build relative path:
var relComponents = Array(repeating: "..", count: baseComponents.count - i)
relComponents.append(contentsOf: destComponents[i...])
return relComponents.joined(separator: "/")
}
我的测试用例有点扩展。案例 4 是我进行此小更改的触发因素。
func testRelativePath() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
func test(_ p1: String, _ p2: String,_ result: String,_ nr: Int) {
let u1 = URL(fileURLWithPath: p1)
let u2 = URL(fileURLWithPath: p2)
let r = u1.relativePath(from: u2)!
XCTAssert( r == result,"\(nr): '\(r)' != '\(result)'")
}
test("/usr/X11/agent/47.gz", "/usr/X11","agent/47.gz", 1)
test("/usr/share/man/meltdown.1", "/usr/share/cups", "../man/meltdown.1",2 )
test("/var/logs/x/y/z/log.txt", "/var/logs", "x/y/z/log.txt",3)
test("/usr/embedded.jpg", "/usr/main.html", "embedded.jpg",4)
test("/usr/embedded.jpg", "/usr", "embedded.jpg",5)
test("~/Downloads/resources", "~/", "Downloads/resources",6)
test("~/Downloads/embedded.jpg", "~/Downloads/main.html", "embedded.jpg",7)
test("/private/var/logs/x/y/z/log.txt", "/var/logs", "x/y/z/log.txt",8)
}
我使用的是 Wizard of Kneup 的版本,但是当基本目录有扩展名时遇到了问题。所以我添加代码来检查路径是否存在并且是一个目录。
public extension URL
{
func relativePath(from base: URL) -> String?
{
// Ensure that both URLs represent files:
guard self.isFileURL &&
FileManager.default.fileExists(atPath: self.path) else
{
NSLog("self is not a fileURL or it does not exists")
return nil
}
var isDir = ObjCBool(true)
guard FileManager.default.fileExists(atPath: base.path, isDirectory: &isDir) &&
isDir.boolValue else
{
NSLog("base is not a directory or it does not exists")
return nil
}
// Remove/replace "." and "..", make paths absolute:
let destComponents = self.resolvingSymlinksInPath().pathComponents
let baseComponents = base.resolvingSymlinksInPath().pathComponents
// Find number of common path components:
let i = Set(destComponents).intersection(Set(baseComponents)).count
// Build relative path:
let relComponents = Array(repeating: "..", count: baseComponents.count - i) +
destComponents[i...]
return relComponents.joined(separator: "/")
}
}