swifturlnsurlnsfilemanagerserver-side-swift

Swift equivalent of Ruby's "Pathname.relative_path_from"


Ruby's Pathname.relative_path_from documentation.

In objc there is already KSFileUtilities' ks_stringRelativeToURL method, that is very close. I'm looking for a pure swift solution that can run on Linux.

I prefer a solution uses file:// URL's, but String is also fine.

Filesystems can be case sensitive/insensitive. It may be tricky to determine the relative path.

Example of inputs and expected output:

| 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 already has FileManager.getRelationship(_:of:in:toItemAt:), but it doesn't return a relative path.


Solution

  • There is no such method in the Swift standard library or in the Foundation framework, as far as I know.

    Here is a possible implementation as an extension method of 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: "/")
        }
    }
    

    My test code:

    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"
    

    Remarks:

    Addendum: @neoneye wrapped this into a Swift package: SwiftyRelativePath.