Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 573
neoneye Avatar asked Jan 20 '18 01:01

neoneye


1 Answers

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:

  • "." and ".." in the given URLs are removed, and relative file URLs are made absolute (both using the standardized method of URL).
  • Case (in)sensitivity is not handled.
  • It is assumed that the base URL represents a directory.

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

like image 197
Martin R Avatar answered Sep 28 '22 09:09

Martin R