diff --git a/README.md b/README.md index 5c249e9..c23e01c 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ print(foo) // => /bar/foo print(foo.isFile) // => true // A practical example: installing a helper executable -try Bundle.resources.join("helper").copy(into: Path.home.join(".local/bin").mkpath()).chmod(0o500) +try Bundle.resources.join("helper").copy(into: Path.home.join(".local/bin").mkdir(.p)).chmod(0o500) ``` We emphasize safety and correctness, just like Swift, and also (again like @@ -184,6 +184,16 @@ Path("~/foo")! // => /Users/mxcl/foo Path("~foo") // => nil ``` +*Path.swift* has the general policty that if the desired end result preexists, +then it’s a noop: + +* If you try to delete a file, but the file doesn't exist, we do nothing. +* If you try to make a directory and it already exists, we do nothing. + +However notably if you try to copy or move a file with specifying `overwrite` +and the file already exists at the destination and is identical, we don’t check +for that as the check was deemed too expensive to be worthwhile. + # Installation SwiftPM: diff --git a/Sources/Path+Codable.swift b/Sources/Path+Codable.swift index 7ced28f..07ff994 100644 --- a/Sources/Path+Codable.swift +++ b/Sources/Path+Codable.swift @@ -25,7 +25,7 @@ public extension CodingUserInfoKey { */ extension Path: Codable { /// - SeeAlso: `CodingUserInfoKey.relativePath` - // :nodoc: + /// :nodoc: public init(from decoder: Decoder) throws { let value = try decoder.singleValueContainer().decode(String.self) if value.hasPrefix("/") { @@ -39,7 +39,7 @@ extension Path: Codable { } /// - SeeAlso: `CodingUserInfoKey.relativePath` - // :nodoc: + /// :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+FileManager.swift b/Sources/Path+FileManager.swift index 5b52708..ba46eae 100644 --- a/Sources/Path+FileManager.swift +++ b/Sources/Path+FileManager.swift @@ -5,10 +5,18 @@ public extension Path { /** Copies a file. + + try Path.root.join("bar").copy(to: Path.home/"foo") + // => "/Users/mxcl/foo" + - Note: `throws` if `to` is a directory. - Parameter to: Destination filename. - Parameter overwrite: If `true` and both `self` and `to` are files, overwrites `to`. - Note: If either `self` or `to are directories, `overwrite` is ignored. + - Note: Throws if `overwrite` is `false` yet `to` is *already* identical to + `self` because even though *Path.swift’s* policy is to noop if the desired + end result preexists, checking for this condition is too expensive a + trade-off. - Returns: `to` to allow chaining - SeeAlso: `copy(into:overwrite:)` */ @@ -24,15 +32,22 @@ public extension Path { /** Copies a file into a directory - If the destination does not exist, this function creates the directory first. - - // Create ~/.local/bin, copy `ls` there and make the new copy executable - try Path.root.join("bin/ls").copy(into: Path.home.join(".local/bin").mkpath()).chmod(0o500) + try Path.root.join("bar").copy(into: .home) + // => "/Users/mxcl/bar" + + // Create ~/.local/bin, copy `ls` there and make the new copy executable + try Path.root.join("bin/ls").copy(into: Path.home.join(".local/bin").mkdir(.p)).chmod(0o500) + + 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 the newly copied file. + - Note: `throws` if `into` is a file. + - Note: Throws if `overwrite` is `false` yet `to` is *already* identical to + `self` because even though *Path.swift’s* policy is to noop if the desired + end result preexists, checking for this condition is too expensive a + trade-off. - SeeAlso: `copy(into:overwrite:)` */ @discardableResult @@ -59,10 +74,18 @@ public extension Path { /** Moves a file. - - Note: `throws` if `to` is a directory. + + try Path.root.join("bar").move(to: Path.home/"foo") + // => "/Users/mxcl/foo" + - Parameter to: Destination filename. - Parameter overwrite: If true overwrites any file that already exists at `to`. - Returns: `to` to allow chaining + - Note: `throws` if `to` is a directory. + - Note: Throws if `overwrite` is `false` yet `to` is *already* identical to + `self` because even though *Path.swift’s* policy is to noop if the desired + end result preexists, checking for this condition is too expensive a + trade-off. - SeeAlso: move(into:overwrite:) */ @discardableResult @@ -77,18 +100,21 @@ public extension Path { /** Moves a file into a directory + try Path.root.join("bar").move(into: .home) + // => "/Users/mxcl/bar" + 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`. + - Parameter overwrite: If true *overwrites* any file that already exists at `into`. + - Note: `throws` if `into` is a file. - Returns: The `Path` of destination filename. - SeeAlso: move(into:overwrite:) */ @discardableResult func move(into: Path) throws -> Path { if !into.exists { - try into.mkpath() + try into.mkdir(.p) } else if !into.isDirectory { throw CocoaError.error(.fileWriteFileExists) } @@ -97,10 +123,16 @@ public extension Path { return rv } - /// Deletes the path, recursively if a directory. + /** + Deletes the path, recursively if a directory. + - Note: noop: if the path doesn’t exist + ∵ *Path.swift* doesn’t error if desired end result preexists. + */ @inlinable func delete() throws { - try FileManager.default.removeItem(at: url) + if exists { + try FileManager.default.removeItem(at: url) + } } /** @@ -113,49 +145,37 @@ public extension Path { return try "".write(to: self) } - /// Helper due to Linux Swift being incomplete. - private func _foo(go: () throws -> Void) throws { - #if !os(Linux) + /** + Creates the directory at this path. + - Note: Does not create any intermediary directories. + - Parameter options: Specify `mkdir(.p)` to create intermediary directories. + - Note: We do not error if the directory already exists (even without `.p`) + because *Path.swift* noops if the desired end result preexists. + - Returns: `self` to allow chaining. + */ + @discardableResult + func mkdir(_ options: MakeDirectoryOptions? = nil) throws -> Path { do { - try go() + let wid = options == .p + try FileManager.default.createDirectory(at: self.url, withIntermediateDirectories: wid, attributes: nil) } catch CocoaError.Code.fileWriteFileExists { - // noop - } - #else - do { - try go() + //noop (fails to trigger on Linux) } catch { + #if os(Linux) let error = error as NSError guard error.domain == NSCocoaErrorDomain, error.code == CocoaError.Code.fileWriteFileExists.rawValue else { throw error } - } - #endif - } - - /** - Creates the directory at this path. - - Note: Does not create any intermediary directories. - - Returns: `self` to allow chaining. - */ - @discardableResult - func mkdir() throws -> Path { - try _foo { - try FileManager.default.createDirectory(at: self.url, withIntermediateDirectories: false, attributes: nil) - } - return self - } - - /** - Creates the directory at this path. - - Note: Creates any intermediary directories, if required. - - Returns: `self` to allow chaining. - */ - @discardableResult - func mkpath() throws -> Path { - try _foo { - try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) + #else + throw error + #endif } return self } } + +/// Options for `Path.mkdir` +public enum MakeDirectoryOptions { + /// Creates intermediary directories. Works the same as mkdir -p. + case p +} diff --git a/Tests/PathTests/PathTests.swift b/Tests/PathTests/PathTests.swift index 6d23262..aa88eba 100644 --- a/Tests/PathTests/PathTests.swift +++ b/Tests/PathTests/PathTests.swift @@ -83,7 +83,7 @@ class PathTests: XCTestCase { try Path.mktemp { for _ in 0...1 { try $0.join("a").mkdir() - try $0.join("b/c").mkpath() + try $0.join("b/c").mkdir(.p) } } }