Improved documentation; Fixes #12

This commit is contained in:
Max Howell
2019-01-25 18:17:13 +00:00
parent b613449232
commit 4b16dac3bf
11 changed files with 181 additions and 127 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
.DS_Store .DS_Store
/.build /.build
/*.xcodeproj /*.xcodeproj
/build
/docs

View File

@@ -69,6 +69,8 @@ jobs:
- UseModernBuildSystem=NO - UseModernBuildSystem=NO
output: output output: output
github_url: https://github.com/mxcl/Path.swift github_url: https://github.com/mxcl/Path.swift
exclude:
- Sources/Path+StringConvertibles.swift
EOF EOF
sed -i '' "s/TRAVIS_TAG/$TRAVIS_TAG/" .jazzy.yaml sed -i '' "s/TRAVIS_TAG/$TRAVIS_TAG/" .jazzy.yaml
# ^^ this weirdness because Travis multiline YAML is broken and inserts # ^^ this weirdness because Travis multiline YAML is broken and inserts

View File

@@ -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 common bugs like accidentally creating `Path` objects from strings you did not
expect to be relative. expect to be relative.
Our initializer is nameless because we conform to `LosslessStringConvertible`, Our initializer is nameless to be consistent with the equivalent operation for
the same conformance as that `Int`, `Float` etc. conform. The protocol enforces converting strings to `Int`, `Float` etc. in the standard library.
a nameless initialization and since it is appropriate for us to conform to it,
we do.
## Extensions ## Extensions

View File

@@ -1,6 +1,34 @@
import Foundation import Foundation
public extension Path { 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 files 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 is already locked, does nothing
/// - Note: If file doesnt exist, throws /// - Note: If file doesnt exist, throws
@discardableResult @discardableResult
@@ -31,30 +59,4 @@ public extension Path {
} }
return self return self
} }
/**
Sets the files 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)
}
}
} }

View File

@@ -1,13 +1,31 @@
import Foundation 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 { 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")! 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 { public init(from decoder: Decoder) throws {
let value = try decoder.singleValueContainer().decode(String.self) let value = try decoder.singleValueContainer().decode(String.self)
if value.hasPrefix("/") { if value.hasPrefix("/") {
@@ -20,6 +38,8 @@ extension Path: Codable {
} }
} }
/// - SeeAlso: `CodingUserInfoKey.relativePath`
// :nodoc:
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer() var container = encoder.singleValueContainer()
if let root = encoder.userInfo[.relativePath] as? Path { if let root = encoder.userInfo[.relativePath] as? Path {

View File

@@ -1,6 +1,8 @@
import Foundation import Foundation
extension Path { extension Path {
//MARK: Common Directories
/// Returns a `Path` containing ``FileManager.default.currentDirectoryPath`. /// Returns a `Path` containing ``FileManager.default.currentDirectoryPath`.
public static var cwd: Path { public static var cwd: Path {
return Path(string: FileManager.default.currentDirectoryPath) return Path(string: FileManager.default.currentDirectoryPath)

View File

@@ -1,6 +1,8 @@
import Foundation import Foundation
public extension Path { public extension Path {
//MARK: File Management
/** /**
Copies a file. Copies a file.
- Note: `throws` if `to` is a directory. - Note: `throws` if `to` is a directory.

View File

@@ -1,14 +1,6 @@
import class Foundation.NSString import class Foundation.NSString
extension Path: LosslessStringConvertible { extension Path: CustomStringConvertible {
/// 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 {
/// Returns `Path.string` /// Returns `Path.string`
public var description: String { public var description: String {
return string return string

View File

@@ -1,8 +1,10 @@
import Foundation import Foundation
public extension Path { 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`. - Parameter includeHiddenFiles: If `true`, hidden files are included in the results. Defaults to `true`.
- Important: `includeHiddenFiles` does not work on Linux - 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 { public extension Array where Element == Path.Entry {
/// Filters the list of entries to be a list of Paths that are directories. /// Filters the list of entries to be a list of Paths that are directories.
var directories: [Path] { var directories: [Path] {

View File

@@ -1,25 +1,11 @@
import Foundation import Foundation
public extension Path { public extension Path {
/// Returns true if the path represents an actual file that is also writable by the current user. //MARK: Filesystem Properties
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)
}
/// Returns true if the path represents an actual directory. /// Returns true if the path represents an actual filesystem entry.
var isDirectory: Bool { var exists: Bool {
var isDir: ObjCBool = false return FileManager.default.fileExists(atPath: string)
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. /// 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 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. /// Returns true if the path represents an actual file that is also executable by the current user.
var isExecutable: Bool { var isExecutable: Bool {
return FileManager.default.isExecutableFile(atPath: string) 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)
}
} }

View File

@@ -3,26 +3,44 @@ import Foundation
/** /**
Represents a platform filesystem absolute path. 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 p1 = Path.root/pathString
let p2 = Path.root/url.path let p2 = Path.root/url.path
let p3 = Path.cwd/relativePathString let p3 = Path.cwd/relativePathString
let p4 = Path(userInput) ?? Path.cwd/userInput 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 { 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 /// The underlying filesystem path
public let string: String public let string: String
/** /// Returns a `URL` representing this file path.
Returns the filename extension of this path. public var url: URL {
- Remark: Implemented via `NSString.pathExtension`. return URL(fileURLWithPath: string)
*/
@inlinable
public var `extension`: String {
return (string as NSString).pathExtension
} }
/** /**
@@ -37,33 +55,51 @@ public struct Path: Equatable, Hashable, Comparable {
return Path(string: (string as NSString).deletingLastPathComponent) 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 @inlinable
public var url: URL { public var `extension`: String {
return URL(fileURLWithPath: 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)
} }
/** /**
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/"a" // => /a
Path.root.join("foo.swift").basename(dropExtension: true) // => "foo" Path.root/"a/b" // => /a/b
Path.root/"a"/"b" // => /a/b
Path.root/"a"/"/b" // => /a/b
- Returns: A string that is the filenames basename. - Parameter lhs: The base path to join with `rhs`.
- Parameter dropExtension: If `true` returns the basename without its file extension. - Parameter rhs: The string to join with this `lhs`.
- Returns: A new joined path.
- SeeAlso: `join(_:)`
*/ */
public func basename(dropExtension: Bool = false) -> String { @inlinable
let str = string as NSString public static func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol {
if !dropExtension { return lhs.join(rhs)
return str.lastPathComponent
} else {
let ext = str.pathExtension
if !ext.isEmpty {
return String(str.lastPathComponent.dropLast(ext.count + 1))
} else {
return str.lastPathComponent
}
}
} }
/** /**
@@ -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("foo.swift").basename() // => "foo.swift"
Path.root.join("a/b") // => /a/b Path.root.join("foo.swift").basename(dropExtension: true) // => "foo"
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 string that is the filenames basename.
- Returns: A new joined path. - Parameter dropExtension: If `true` returns the basename without its file extension.
- SeeAlso: `/(_:, _:)`
*/ */
public func join<S>(_ pathComponent: S) -> Path where S: StringProtocol { public func basename(dropExtension: Bool = false) -> String {
//TODO standardizingPath does more than we want really (eg tilde expansion) let str = string as NSString
let str = (string as NSString).appendingPathComponent(String(pathComponent)) if !dropExtension {
return Path(string: (str as NSString).standardizingPath) return str.lastPathComponent
} } else {
let ext = str.pathExtension
/** if !ext.isEmpty {
Joins a path and a string to produce a new path. return String(str.lastPathComponent.dropLast(ext.count + 1))
} else {
Path.root/"a" // => /a return str.lastPathComponent
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 /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol {
return lhs.join(rhs)
} }
/// Returns the locale-aware sort order for the two paths. /// Returns the locale-aware sort order for the two paths.
/// :nodoc:
@inlinable @inlinable
public static func <(lhs: Path, rhs: Path) -> Bool { public static func <(lhs: Path, rhs: Path) -> Bool {
return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending 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 { public struct Entry {
/// The kind of this directory entry. /// The kind of this directory entry.
public enum Kind { public enum Kind {