diff --git a/.gitignore b/.gitignore index ced0a0e..b561ace 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .DS_Store /.build /*.xcodeproj +/build +/docs diff --git a/.travis.yml b/.travis.yml index 299ba1d..674c3c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -69,6 +69,8 @@ jobs: - UseModernBuildSystem=NO output: output github_url: https://github.com/mxcl/Path.swift + exclude: + - Sources/Path+StringConvertibles.swift EOF sed -i '' "s/TRAVIS_TAG/$TRAVIS_TAG/" .jazzy.yaml # ^^ this weirdness because Travis multiline YAML is broken and inserts diff --git a/README.md b/README.md index 4319e65..5c249e9 100644 --- a/README.md +++ b/README.md @@ -107,10 +107,8 @@ This is explicit, not hiding anything that code-review may miss and preventing common bugs like accidentally creating `Path` objects from strings you did not expect to be relative. -Our initializer is nameless because we conform to `LosslessStringConvertible`, -the same conformance as that `Int`, `Float` etc. conform. The protocol enforces -a nameless initialization and since it is appropriate for us to conform to it, -we do. +Our initializer is nameless to be consistent with the equivalent operation for +converting strings to `Int`, `Float` etc. in the standard library. ## Extensions diff --git a/Sources/Path+Attributes.swift b/Sources/Path+Attributes.swift index c30b940..393d3f2 100644 --- a/Sources/Path+Attributes.swift +++ b/Sources/Path+Attributes.swift @@ -1,6 +1,34 @@ import Foundation public extension Path { + //MARK: Filesystem Attributes + + /** + 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. + */ + var mtime: Date { + do { + let attrs = try FileManager.default.attributesOfItem(atPath: string) + return attrs[.modificationDate] as? Date ?? attrs[.creationDate] as? Date ?? Date(timeIntervalSince1970: 0) + } catch { + //TODO log error + return Date(timeIntervalSince1970: 0) + } + } + + /** + Sets the file’s attributes using UNIX octal notation. + + Path.home.join("foo").chmod(0o555) + */ + @discardableResult + func chmod(_ octal: Int) throws -> Path { + try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string) + return self + } + /// - Note: If file is already locked, does nothing /// - Note: If file doesn’t exist, throws @discardableResult @@ -31,30 +59,4 @@ public extension Path { } return self } - - /** - Sets the file’s attributes using UNIX octal notation. - - Path.home.join("foo").chmod(0o555) - */ - @discardableResult - func chmod(_ octal: Int) throws -> Path { - try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string) - return self - } - - /** - 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. - */ - var mtime: Date { - do { - let attrs = try FileManager.default.attributesOfItem(atPath: string) - return attrs[.modificationDate] as? Date ?? attrs[.creationDate] as? Date ?? Date(timeIntervalSince1970: 0) - } catch { - //TODO log error - return Date(timeIntervalSince1970: 0) - } - } } diff --git a/Sources/Path+Codable.swift b/Sources/Path+Codable.swift index d6176ab..7ced28f 100644 --- a/Sources/Path+Codable.swift +++ b/Sources/Path+Codable.swift @@ -1,13 +1,31 @@ import Foundation -/// Provided for relative-path coding. See the instructions in our `README`. +/** + Provided for relative-path coding. See the instructions in our + [README](https://github.com/mxcl/Path.swift/#codable). +*/ public extension CodingUserInfoKey { - /// If set paths are encoded as relative to this path. + /** + If set on an `Encoder`’s `userInfo` all paths are encoded relative to this path. + + For example: + + let encoder = JSONEncoder() + encoder.userInfo[.relativePath] = Path.home + encoder.encode([Path.home, Path.home/"foo"]) + + - Remark: See the [README](https://github.com/mxcl/Path.swift/#codable) for more information. + */ static let relativePath = CodingUserInfoKey(rawValue: "dev.mxcl.Path.relative")! } -/// Provided for relative-path coding. See the instructions in our `README`. -extension Path: Codable { +/** + Provided for relative-path coding. See the instructions in our + [README](https://github.com/mxcl/Path.swift/#codable). +*/ +extension Path: Codable { + /// - SeeAlso: `CodingUserInfoKey.relativePath` + // :nodoc: public init(from decoder: Decoder) throws { let value = try decoder.singleValueContainer().decode(String.self) if value.hasPrefix("/") { @@ -20,6 +38,8 @@ extension Path: Codable { } } + /// - SeeAlso: `CodingUserInfoKey.relativePath` + // :nodoc: public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() if let root = encoder.userInfo[.relativePath] as? Path { diff --git a/Sources/Path+CommonDirectories.swift b/Sources/Path+CommonDirectories.swift index 6ba92ae..800cd8f 100644 --- a/Sources/Path+CommonDirectories.swift +++ b/Sources/Path+CommonDirectories.swift @@ -1,6 +1,8 @@ import Foundation extension Path { + //MARK: Common Directories + /// Returns a `Path` containing ``FileManager.default.currentDirectoryPath`. public static var cwd: Path { return Path(string: FileManager.default.currentDirectoryPath) diff --git a/Sources/Path+FileManager.swift b/Sources/Path+FileManager.swift index 26ce038..5b52708 100644 --- a/Sources/Path+FileManager.swift +++ b/Sources/Path+FileManager.swift @@ -1,6 +1,8 @@ import Foundation public extension Path { + //MARK: File Management + /** Copies a file. - Note: `throws` if `to` is a directory. diff --git a/Sources/Path+StringConvertibles.swift b/Sources/Path+StringConvertibles.swift index e197016..637a20a 100644 --- a/Sources/Path+StringConvertibles.swift +++ b/Sources/Path+StringConvertibles.swift @@ -1,14 +1,6 @@ import class Foundation.NSString -extension Path: LosslessStringConvertible { - /// 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) - } -} - -extension Path: CustomStringConvertible { +extension Path: CustomStringConvertible { /// Returns `Path.string` public var description: String { return string diff --git a/Sources/Path+ls.swift b/Sources/Path+ls.swift index 13bfa6e..8ff2fab 100644 --- a/Sources/Path+ls.swift +++ b/Sources/Path+ls.swift @@ -1,8 +1,10 @@ import Foundation public extension Path { + //MARK: Directory Listings + /** - Same as the `ls -a` command ∴ is ”shallow” + Same as the `ls -a` command ∴ output is ”shallow” and unsorted. - Parameter includeHiddenFiles: If `true`, hidden files are included in the results. Defaults to `true`. - Important: `includeHiddenFiles` does not work on Linux */ @@ -22,6 +24,7 @@ public extension Path { } } +/// Convenience functions for the array return value of `Path.ls()` public extension Array where Element == Path.Entry { /// Filters the list of entries to be a list of Paths that are directories. var directories: [Path] { diff --git a/Sources/Path->Bool.swift b/Sources/Path->Bool.swift index f342703..4b61ca5 100644 --- a/Sources/Path->Bool.swift +++ b/Sources/Path->Bool.swift @@ -1,25 +1,11 @@ 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 file that is also readable by the current user. - var isReadable: Bool { - return FileManager.default.isReadableFile(atPath: string) - } - - /// Returns true if the path represents an actual file that is also deletable by the current user. - var isDeletable: Bool { - return FileManager.default.isDeletableFile(atPath: string) - } + //MARK: Filesystem Properties - /// 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. + var exists: Bool { + return FileManager.default.fileExists(atPath: string) } /// Returns true if the path represents an actual filesystem entry that is *not* a directory. @@ -28,13 +14,29 @@ public extension Path { return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && !isDir.boolValue } + /// 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 file that is also readable by the current user. + var isReadable: Bool { + return FileManager.default.isReadableFile(atPath: string) + } + + /// 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 file that is also deletable by the current user. + var isDeletable: Bool { + return FileManager.default.isDeletableFile(atPath: string) + } + /// 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 102c7cb..9e28236 100644 --- a/Sources/Path.swift +++ b/Sources/Path.swift @@ -3,26 +3,44 @@ import Foundation /** Represents a platform filesystem absolute path. - The recommended conversions from string are: + `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, (though folders will not be sorted + first). + + 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. + - 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 the filename extension of this path. - - Remark: Implemented via `NSString.pathExtension`. - */ - @inlinable - public var `extension`: String { - return (string as NSString).pathExtension + /// Returns a `URL` representing this file path. + public var url: URL { + return URL(fileURLWithPath: string) } /** @@ -37,33 +55,51 @@ public struct Path: Equatable, Hashable, Comparable { return Path(string: (string as NSString).deletingLastPathComponent) } - /// Returns a `URL` representing this file path. + /** + Returns the filename extension of this path. + - Remark: Implemented via `NSString.pathExtension`. + */ @inlinable - public var url: URL { - return URL(fileURLWithPath: string) + 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(_ 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) } /** - The basename for the provided file, optionally dropping the file extension. + Joins a path and a string to produce a new path. - Path.root.join("foo.swift").basename() // => "foo.swift" - Path.root.join("foo.swift").basename(dropExtension: true) // => "foo" + Path.root/"a" // => /a + Path.root/"a/b" // => /a/b + Path.root/"a"/"b" // => /a/b + Path.root/"a"/"/b" // => /a/b - - Returns: A string that is the filename’s basename. - - Parameter dropExtension: If `true` returns the basename without its file extension. + - 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(_:)` */ - 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 - } - } + @inlinable + public static func /(lhs: Path, rhs: S) -> Path where S: StringProtocol { + return lhs.join(rhs) } /** @@ -104,48 +140,41 @@ public struct Path: Equatable, Hashable, Comparable { } /** - Joins a path and a string to produce a new path. + The basename for the provided file, optionally dropping the file extension. - 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 + Path.root.join("foo.swift").basename() // => "foo.swift" + Path.root.join("foo.swift").basename(dropExtension: true) // => "foo" - - Parameter pathComponent: The string to join with this path. - - Returns: A new joined path. - - SeeAlso: `/(_:, _:)` + - Returns: A string that is the filename’s basename. + - Parameter dropExtension: If `true` returns the basename without its file extension. */ - 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(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: `Path.join(_:)` - */ - @inlinable - public static func /(lhs: Path, rhs: S) -> Path where S: StringProtocol { - return lhs.join(rhs) + 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 } - /// A file entry from a directory listing. +//MARK: Entry + + /** + A file entry from a directory listing. + - SeeAlso: `ls()` + */ public struct Entry { /// The kind of this directory entry. public enum Kind {