174 lines
6.2 KiB
Swift
174 lines
6.2 KiB
Swift
import Foundation
|
||
|
||
/**
|
||
Represents a filesystem absolute path.
|
||
|
||
`Path` supports `Codable`, and can be configured to
|
||
[encode paths *relatively*](https://github.com/mxcl/Path.swift/#codable).
|
||
|
||
Sorting a `Sequence` of `Path`s will return the locale-aware sort order, which
|
||
will give you the same order as Finder.
|
||
|
||
All functions on `Path` are chainable and short to facilitate doing sequences
|
||
of file operations in a concise manner.
|
||
|
||
Converting from a `String` is a common first step, here are the recommended
|
||
ways to do that:
|
||
|
||
let p1 = Path.root/pathString
|
||
let p2 = Path.root/url.path
|
||
let p3 = Path.cwd/relativePathString
|
||
let p4 = Path(userInput) ?? Path.cwd/userInput
|
||
|
||
- Note: There may not be an actual filesystem entry at the path. The underlying
|
||
representation for `Path` is `String`.
|
||
*/
|
||
public struct Path: Equatable, Hashable, Comparable {
|
||
|
||
init(string: String) {
|
||
self.string = string
|
||
}
|
||
|
||
/// Returns `nil` unless fed an absolute path
|
||
public init?(_ description: String) {
|
||
guard description.starts(with: "/") || description.starts(with: "~/") else { return nil }
|
||
self.init(string: (description as NSString).standardizingPath)
|
||
}
|
||
|
||
//MARK: Properties
|
||
|
||
/// The underlying filesystem path
|
||
public let string: String
|
||
|
||
/// Returns a `URL` representing this file path.
|
||
public var url: URL {
|
||
return URL(fileURLWithPath: string)
|
||
}
|
||
|
||
/**
|
||
Returns the parent directory for this path.
|
||
|
||
Path is not aware of the nature of the underlying file, but this is
|
||
irrlevant since the operation is the same irrespective of this fact.
|
||
|
||
- Note: always returns a valid path, `Path.root.parent` *is* `Path.root`.
|
||
*/
|
||
public var parent: Path {
|
||
return Path(string: (string as NSString).deletingLastPathComponent)
|
||
}
|
||
|
||
/**
|
||
Returns the filename extension of this path.
|
||
- Remark: Implemented via `NSString.pathExtension`.
|
||
*/
|
||
@inlinable
|
||
public var `extension`: String {
|
||
return (string as NSString).pathExtension
|
||
}
|
||
|
||
//MARK: Pathing
|
||
|
||
/**
|
||
Joins a path and a string to produce a new path.
|
||
|
||
Path.root.join("a") // => /a
|
||
Path.root.join("a/b") // => /a/b
|
||
Path.root.join("a").join("b") // => /a/b
|
||
Path.root.join("a").join("/b") // => /a/b
|
||
|
||
- Parameter pathComponent: The string to join with this path.
|
||
- Returns: A new joined path.
|
||
- SeeAlso: `Path./(_:, _:)`
|
||
*/
|
||
public func join<S>(_ pathComponent: S) -> Path where S: StringProtocol {
|
||
//TODO standardizingPath does more than we want really (eg tilde expansion)
|
||
let str = (string as NSString).appendingPathComponent(String(pathComponent))
|
||
return Path(string: (str as NSString).standardizingPath)
|
||
}
|
||
|
||
/**
|
||
Joins a path and a string to produce a new path.
|
||
|
||
Path.root/"a" // => /a
|
||
Path.root/"a/b" // => /a/b
|
||
Path.root/"a"/"b" // => /a/b
|
||
Path.root/"a"/"/b" // => /a/b
|
||
|
||
- Parameter lhs: The base path to join with `rhs`.
|
||
- Parameter rhs: The string to join with this `lhs`.
|
||
- Returns: A new joined path.
|
||
- SeeAlso: `join(_:)`
|
||
*/
|
||
@inlinable
|
||
public static func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol {
|
||
return lhs.join(rhs)
|
||
}
|
||
|
||
/**
|
||
Returns a string representing the relative path to `base`.
|
||
|
||
- Note: If `base` is not a logical prefix for `self` your result will be prefixed some number of `../` components.
|
||
- Parameter base: The base to which we calculate the relative path.
|
||
- ToDo: Another variant that returns `nil` if result would start with `..`
|
||
*/
|
||
public func relative(to base: Path) -> String {
|
||
// Split the two paths into their components.
|
||
// FIXME: The is needs to be optimized to avoid unncessary copying.
|
||
let pathComps = (string as NSString).pathComponents
|
||
let baseComps = (base.string as NSString).pathComponents
|
||
|
||
// It's common for the base to be an ancestor, so try that first.
|
||
if pathComps.starts(with: baseComps) {
|
||
// Special case, which is a plain path without `..` components. It
|
||
// might be an empty path (when self and the base are equal).
|
||
let relComps = pathComps.dropFirst(baseComps.count)
|
||
return relComps.joined(separator: "/")
|
||
} else {
|
||
// General case, in which we might well need `..` components to go
|
||
// "up" before we can go "down" the directory tree.
|
||
var newPathComps = ArraySlice(pathComps)
|
||
var newBaseComps = ArraySlice(baseComps)
|
||
while newPathComps.prefix(1) == newBaseComps.prefix(1) {
|
||
// First component matches, so drop it.
|
||
newPathComps = newPathComps.dropFirst()
|
||
newBaseComps = newBaseComps.dropFirst()
|
||
}
|
||
// Now construct a path consisting of as many `..`s as are in the
|
||
// `newBaseComps` followed by what remains in `newPathComps`.
|
||
var relComps = Array(repeating: "..", count: newBaseComps.count)
|
||
relComps.append(contentsOf: newPathComps)
|
||
return relComps.joined(separator: "/")
|
||
}
|
||
}
|
||
|
||
/**
|
||
The basename for the provided file, optionally dropping the file extension.
|
||
|
||
Path.root.join("foo.swift").basename() // => "foo.swift"
|
||
Path.root.join("foo.swift").basename(dropExtension: true) // => "foo"
|
||
|
||
- Returns: A string that is the filename’s basename.
|
||
- Parameter dropExtension: If `true` returns the basename without its file extension.
|
||
*/
|
||
public func basename(dropExtension: Bool = false) -> String {
|
||
let str = string as NSString
|
||
if !dropExtension {
|
||
return str.lastPathComponent
|
||
} else {
|
||
let ext = str.pathExtension
|
||
if !ext.isEmpty {
|
||
return String(str.lastPathComponent.dropLast(ext.count + 1))
|
||
} else {
|
||
return str.lastPathComponent
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Returns the locale-aware sort order for the two paths.
|
||
/// :nodoc:
|
||
@inlinable
|
||
public static func <(lhs: Path, rhs: Path) -> Bool {
|
||
return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending
|
||
}
|
||
}
|