diff --git a/Sources/Extensions.swift b/Sources/Extensions.swift index d31812b..f7c1df0 100644 --- a/Sources/Extensions.swift +++ b/Sources/Extensions.swift @@ -1,26 +1,31 @@ import Foundation public extension Bundle { + /// Returns the path for requested resource in this bundle. func path(forResource: String, ofType: String?) -> Path? { let f: (String?, String?) -> String? = path(forResource:ofType:) let str = f(forResource, ofType) return str.flatMap(Path.init) } + /// Returns the path for the shared-frameworks directory in this bundle. public var sharedFrameworks: Path? { return sharedFrameworksPath.flatMap(Path.init) } + /// Returns the path for the resources directory in this bundle. public var resources: Path? { return resourcePath.flatMap(Path.init) } + /// Returns the path for this bundle. public var path: Path { return Path(string: bundlePath) } } public extension String { + /// Initializes this `String` with the contents of the provided path. @inlinable init(contentsOf path: Path) throws { try self.init(contentsOfFile: path.string) @@ -36,6 +41,7 @@ public extension String { } public extension Data { + /// Initializes this `Data` with the contents of the provided path. @inlinable init(contentsOf path: Path) throws { try self.init(contentsOf: path.url) diff --git a/Sources/Path+Attributes.swift b/Sources/Path+Attributes.swift index 7baee84..613c473 100644 --- a/Sources/Path+Attributes.swift +++ b/Sources/Path+Attributes.swift @@ -43,14 +43,18 @@ public extension Path { return self } - /// - Returns: modification-time or creation-time if none + /** + Returns the modification-time. + - Note: Returns the creation time if there is no modification time. + - Note: Returns UNIX-time-zero if neither are available, though this *should* be impossible. + */ public var mtime: Date { do { let attrs = try FileManager.default.attributesOfItem(atPath: string) return attrs[.modificationDate] as? Date ?? attrs[.creationDate] as? Date ?? Date() } catch { - //TODO print(error) - return Date() + //TODO log error + return Date(timeIntervalSince1970: 0) } } } diff --git a/Sources/Path+Codable.swift b/Sources/Path+Codable.swift index c57bf5c..b87d424 100644 --- a/Sources/Path+Codable.swift +++ b/Sources/Path+Codable.swift @@ -1,6 +1,7 @@ import Foundation public extension CodingUserInfoKey { + /// If set paths are encoded as relative to this path. static let relativePath = CodingUserInfoKey(rawValue: "dev.mxcl.Path.relative")! } diff --git a/Sources/Path+FileManager.swift b/Sources/Path+FileManager.swift index 84d7fa9..b1d3927 100644 --- a/Sources/Path+FileManager.swift +++ b/Sources/Path+FileManager.swift @@ -41,6 +41,14 @@ public extension Path { return rv } + /** + Moves a file. + - Note: `throws` if `to` is a directory. + - Parameter to: Destination filename. + - Parameter overwrite: If true overwrites any file that already exists at `to`. + - Returns: `to` to allow chaining + - SeeAlso: move(into:overwrite:) + */ @discardableResult public func move(to: Path, overwrite: Bool = false) throws -> Path { if overwrite, to.exists { @@ -50,6 +58,17 @@ public extension Path { return to } + /** + Moves a file into a directory + + If the destination does not exist, this function creates the directory first. + + - Note: `throws` if `into` is a file. + - Parameter into: Destination directory + - Parameter overwrite: If true overwrites any file that already exists at `into`. + - Returns: The `Path` of destination filename. + - SeeAlso: move(into:overwrite:) + */ @discardableResult public func move(into: Path) throws -> Path { if !into.exists { @@ -62,17 +81,23 @@ public extension Path { return rv } + /// Deletes the path, recursively if a directory. @inlinable public func delete() throws { try FileManager.default.removeItem(at: url) } + /** + Creates an empty file at this path. + - Returns: `self` to allow chaining. + */ @inlinable @discardableResult func touch() throws -> Path { return try "".write(to: self) } + /// Helper due to Linux Swift being incomplete. private func _foo(go: () throws -> Void) throws { #if !os(Linux) do { @@ -92,6 +117,11 @@ public extension Path { #endif } + /** + Creates the directory at this path. + - Note: Does not create any intermediary directories. + - Returns: `self` to allow chaining. + */ @discardableResult public func mkdir() throws -> Path { try _foo { @@ -100,6 +130,11 @@ public extension Path { return self } + /** + Creates the directory at this path. + - Note: Creates any intermediary directories, if required. + - Returns: `self` to allow chaining. + */ @discardableResult public func mkpath() throws -> Path { try _foo { @@ -108,8 +143,15 @@ public extension Path { return self } - /// - Note: If file doesn’t exist, creates file - /// - Note: If file is not writable, makes writable first, resetting permissions after the write + /** + Replaces the contents of the file at this path with the provided string. + - Note: If file doesn’t exist, creates file + - Note: If file is not writable, makes writable first, resetting permissions after the write + - Parameter contents: The string that will become the contents of this file. + - Parameter atomically: If `true` the operation will be performed atomically. + - Parameter encoding: The string encoding to use. + - Returns: `self` to allow chaining. + */ @discardableResult public func replaceContents(with contents: String, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> Path { let resetPerms: Int? diff --git a/Sources/Path+StringConvertibles.swift b/Sources/Path+StringConvertibles.swift index b907691..e197016 100644 --- a/Sources/Path+StringConvertibles.swift +++ b/Sources/Path+StringConvertibles.swift @@ -9,12 +9,14 @@ extension Path: LosslessStringConvertible { } extension Path: CustomStringConvertible { + /// Returns `Path.string` public var description: String { return string } } extension Path: CustomDebugStringConvertible { + /// Returns eg. `Path(string: "/foo")` public var debugDescription: String { return "Path(string: \(string))" } diff --git a/Sources/Path+ls.swift b/Sources/Path+ls.swift index c0d81c3..1dfe475 100644 --- a/Sources/Path+ls.swift +++ b/Sources/Path+ls.swift @@ -13,12 +13,14 @@ public extension Path { } public extension Array where Element == Path.Entry { + /// Filters the list of entries to be a list of Paths that are directories. var directories: [Path] { return compactMap { $0.kind == .directory ? $0.path : nil } } + /// Filters the list of entries to be a list of Paths that are files with the specified extension func files(withExtension ext: String) -> [Path] { return compactMap { $0.kind == .file && $0.path.extension == ext ? $0.path : nil diff --git a/Sources/Path->Bool.swift b/Sources/Path->Bool.swift index ad1f885..874cf21 100644 --- a/Sources/Path->Bool.swift +++ b/Sources/Path->Bool.swift @@ -1,24 +1,29 @@ import Foundation public extension Path { + /// Returns true if the path represents an actual file that is also writable by the current user. var isWritable: Bool { return FileManager.default.isWritableFile(atPath: string) } + /// Returns true if the path represents an actual directory. var isDirectory: Bool { var isDir: ObjCBool = false return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && isDir.boolValue } + /// Returns true if the path represents an actual filesystem entry that is *not* a directory. var isFile: Bool { var isDir: ObjCBool = true return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && !isDir.boolValue } + /// Returns true if the path represents an actual file that is also executable by the current user. var isExecutable: Bool { return FileManager.default.isExecutableFile(atPath: string) } + /// Returns true if the path represents an actual filesystem entry. var exists: Bool { return FileManager.default.fileExists(atPath: string) } diff --git a/Sources/Path.swift b/Sources/Path.swift index 858e3c0..145f7a5 100644 --- a/Sources/Path.swift +++ b/Sources/Path.swift @@ -1,16 +1,32 @@ import Foundation +/** + Represents a platform filesystem absolute path. + + The recommended conversions from string are: + + 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 filename at the path. + */ public struct Path: Equatable, Hashable, Comparable { + /// The underlying filesystem path public let string: String + /// Returns a `Path` containing ``FileManager.default.currentDirectoryPath`. public static var cwd: Path { return Path(string: FileManager.default.currentDirectoryPath) } + /// Returns a `Path` representing the root path. public static var root: Path { return Path(string: "/") } + /// Returns a `Path` representing the user’s home directory public static var home: Path { let string: String #if os(macOS) @@ -25,21 +41,42 @@ public struct Path: Equatable, Hashable, Comparable { return Path(string: string) } + /** + Returns the filename extension of this path. + - Remark: Implemented via `NSString.pathExtension`. + */ @inlinable public var `extension`: String { return (string as NSString).pathExtension } - /// - Note: always returns a valid path, `Path.root.parent` *is* `Path.root` + /** + 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 a `URL` representing this file path. @inlinable public var url: URL { return URL(fileURLWithPath: string) } + /** + 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 { @@ -54,7 +91,13 @@ public struct Path: Equatable, Hashable, Comparable { } } - //TODO another variant that returns `nil` if result would start with `..` + /** + 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. @@ -85,27 +128,59 @@ public struct Path: Equatable, Hashable, Comparable { } } - public func join(_ part: S) -> Path where S: StringProtocol { + /** + 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,:String) + */ + public func join(_ pathComponent: S) -> Path where S: StringProtocol { //TODO standardizingPath does more than we want really (eg tilde expansion) - let str = (string as NSString).appendingPathComponent(String(part)) + let str = (string as NSString).appendingPathComponent(String(pathComponent)) return Path(string: (str as NSString).standardizingPath) } + /// Returns the locale-aware sort order for the two paths. @inlinable public static func <(lhs: Path, rhs: Path) -> Bool { return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending } + /// A file entry from a directory listing. public struct Entry { + /// The kind of this directory entry. public enum Kind { + /// The path is a file. case file + /// The path is a directory. case directory } + /// The kind of this entry. public let kind: Kind + /// The path of this entry. public let path: Path } } +/** + 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: Path.join(_:) + */ @inlinable public func /(lhs: Path, rhs: S) -> Path where S: StringProtocol { return lhs.join(rhs)