.some
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/*.xcodeproj
|
||||
27
LICENSE.md
Normal file
27
LICENSE.md
Normal file
@@ -0,0 +1,27 @@
|
||||
Unlicense (Public Domain)
|
||||
============================
|
||||
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <<http://unlicense.org/>>
|
||||
13
Package.swift
Normal file
13
Package.swift
Normal file
@@ -0,0 +1,13 @@
|
||||
// swift-tools-version:4.2
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Path.swift",
|
||||
products: [
|
||||
.library(name: "Path", targets: ["Path"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "Path", path: "Sources"),
|
||||
.testTarget(name: "PathTests", dependencies: ["Path"]),
|
||||
]
|
||||
)
|
||||
145
README.md
Normal file
145
README.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# Path.swift
|
||||
|
||||
A file-system pathing library focused on developer experience and robust
|
||||
end‐results.
|
||||
|
||||
```swift
|
||||
// convenient static members
|
||||
let home = Path.home
|
||||
|
||||
// pleasant joining syntax
|
||||
let docs = Path.home/"Documents"
|
||||
|
||||
// paths are *always* absolute thus avoiding common bugs
|
||||
let path = Path(userInput) ?? Path.cwd/userInput
|
||||
|
||||
// chainable syntax so you have less boilerplate
|
||||
try Path.home.join("foo").mkpath().join("bar").chmod(0o555)
|
||||
|
||||
// easy file-management
|
||||
try Path.root.join("foo").copy(to: Path.root.join("bar"))
|
||||
|
||||
// careful API to avoid common bugs
|
||||
try Path.root.join("foo").copy(into: Path.root.mkdir("bar"))
|
||||
// ^^ other libraries would make the `to:` form handle both these cases
|
||||
// but that can easily lead to bugs where you accidentally write files that
|
||||
// were meant to be directory destinations
|
||||
```
|
||||
|
||||
Paths are just string representations, there *may not* be a real file there.
|
||||
|
||||
# Support mxcl
|
||||
|
||||
Hi, I’m Max Howell and I have written a lot of open source software, and
|
||||
probably you already use some of it (Homebrew anyone?). Please help me so I
|
||||
can continue to make tools and software you need and love. I appreciate it x.
|
||||
|
||||
<a href="https://www.patreon.com/mxcl">
|
||||
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160">
|
||||
</a>
|
||||
|
||||
## Codable
|
||||
|
||||
We support `Codable` as you would expect:
|
||||
|
||||
```swift
|
||||
try JSONEncoder().encode([Path.home, Path.home/"foo"])
|
||||
```
|
||||
|
||||
```json
|
||||
[
|
||||
"/Users/mxcl",
|
||||
"/Users/mxcl/foo",
|
||||
]
|
||||
```
|
||||
|
||||
However, often you want to encode relative paths:
|
||||
|
||||
```swift
|
||||
let encoder = JSONEncoder()
|
||||
encoder.userInfo[.relativePath] = Path.home
|
||||
encoder.encode([Path.home, Path.home/"foo"])
|
||||
```
|
||||
|
||||
```json
|
||||
[
|
||||
"",
|
||||
"foo",
|
||||
]
|
||||
```
|
||||
|
||||
**Note** make sure you decode with this key set *also*, otherwise we `fatal`
|
||||
(unless the paths are absolute obv.)
|
||||
|
||||
```swift
|
||||
let decoder = JSONDecoder()
|
||||
decoder.userInfo[.relativePath] = Path.home
|
||||
decoder.decode(from: data)
|
||||
```
|
||||
|
||||
## Initializing from user-input
|
||||
|
||||
The `Path` initializer returns `nil` unless fed an absolute path; thus to
|
||||
initialize from user-input that may contain a relative path use this form:
|
||||
|
||||
```swift
|
||||
let path = Path(userInput) ?? Path.cwd/userInput
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Extensions
|
||||
|
||||
We have some extensions to Apple APIs:
|
||||
|
||||
```swift
|
||||
let bashProfile = try String(contentsOf: Path.home/".bash_profile")
|
||||
let history = try Data(contentsOf: Path.home/".history")
|
||||
|
||||
bashProfile += "\n\nfoo"
|
||||
|
||||
try bashProfile.write(to: Path.home/".bash_profile")
|
||||
|
||||
try Bundle.main.resources!.join("foo").copy(to: .home)
|
||||
// ^^ `-> Path?` because the underlying `Bundle` function is `-> String?`
|
||||
```
|
||||
|
||||
## Directory listings
|
||||
|
||||
We provide `ls()`, called because it behaves like the Terminal `ls` function,
|
||||
the name thus implies its behavior, ie. that it is not recursive.
|
||||
|
||||
```swift
|
||||
for path in Path.home.ls() {
|
||||
print(path.path)
|
||||
print(path.kind) // .directory or .file
|
||||
}
|
||||
|
||||
for path in Path.home.ls() where path.kind == .file {
|
||||
//…
|
||||
}
|
||||
|
||||
for path in Path.home.ls() where path.mtime > yesterday {
|
||||
//…
|
||||
}
|
||||
|
||||
let dirs = Path.home.ls().directories().filter {
|
||||
//…
|
||||
}
|
||||
|
||||
let swiftFiles = Path.home.ls().files(withExtension: "swift")
|
||||
```
|
||||
|
||||
# Installation
|
||||
|
||||
SwiftPM only:
|
||||
|
||||
```swift
|
||||
package.append(.package(url: "https://github.com/mxcl/Path.swift", from: "0.0.0"))
|
||||
```
|
||||
|
||||
### Get push notifications for new releases
|
||||
|
||||
https://codebasesaga.com/canopy/
|
||||
53
Sources/Extensions.swift
Normal file
53
Sources/Extensions.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
import Foundation
|
||||
|
||||
public extension 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)
|
||||
}
|
||||
|
||||
public var sharedFrameworks: Path? {
|
||||
return sharedFrameworksPath.flatMap(Path.init)
|
||||
}
|
||||
|
||||
public var resources: Path? {
|
||||
return resourcePath.flatMap(Path.init)
|
||||
}
|
||||
}
|
||||
|
||||
public extension String {
|
||||
@inlinable
|
||||
init(contentsOf path: Path) throws {
|
||||
try self.init(contentsOfFile: path.string)
|
||||
}
|
||||
|
||||
/// - Returns: `to` to allow chaining
|
||||
@inlinable
|
||||
@discardableResult
|
||||
func write(to: Path, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> Path {
|
||||
try write(toFile: to.string, atomically: atomically, encoding: encoding)
|
||||
return to
|
||||
}
|
||||
}
|
||||
|
||||
public extension Data {
|
||||
@inlinable
|
||||
init(contentsOf path: Path) throws {
|
||||
try self.init(contentsOf: path.url)
|
||||
}
|
||||
|
||||
/// - Returns: `to` to allow chaining
|
||||
@inlinable
|
||||
@discardableResult
|
||||
func write(to: Path, atomically: Bool = false) throws -> Path {
|
||||
let opts: NSData.WritingOptions
|
||||
if atomically {
|
||||
opts = .atomicWrite
|
||||
} else {
|
||||
opts = []
|
||||
}
|
||||
try write(to: to.url, options: opts)
|
||||
return to
|
||||
}
|
||||
}
|
||||
56
Sources/Path+Attributes.swift
Normal file
56
Sources/Path+Attributes.swift
Normal file
@@ -0,0 +1,56 @@
|
||||
import Foundation
|
||||
|
||||
public extension Path {
|
||||
/// - Note: If file is already locked, does nothing
|
||||
/// - Note: If file doesn’t exist, throws
|
||||
@discardableResult
|
||||
public func lock() throws -> Path {
|
||||
var attrs = try FileManager.default.attributesOfItem(atPath: string)
|
||||
let b = attrs[.immutable] as? Bool ?? false
|
||||
if !b {
|
||||
attrs[.immutable] = true
|
||||
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/// - Note: If file isn‘t locked, does nothing
|
||||
/// - Note: If file doesn’t exist, does nothing
|
||||
@discardableResult
|
||||
public func unlock() throws -> Path {
|
||||
var attrs: [FileAttributeKey: Any]
|
||||
do {
|
||||
attrs = try FileManager.default.attributesOfItem(atPath: string)
|
||||
} catch CocoaError.fileReadNoSuchFile {
|
||||
return self
|
||||
}
|
||||
let b = attrs[.immutable] as? Bool ?? false
|
||||
if b {
|
||||
attrs[.immutable] = false
|
||||
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the file’s attributes using UNIX octal notation.
|
||||
|
||||
Path.home.join("foo").chmod(0o555)
|
||||
*/
|
||||
@discardableResult
|
||||
public func chmod(_ octal: Int) throws -> Path {
|
||||
try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string)
|
||||
return self
|
||||
}
|
||||
|
||||
/// - Returns: modification-time or creation-time if none
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Sources/Path+Codable.swift
Normal file
28
Sources/Path+Codable.swift
Normal file
@@ -0,0 +1,28 @@
|
||||
import Foundation
|
||||
|
||||
public extension CodingUserInfoKey {
|
||||
static let relativePath = CodingUserInfoKey(rawValue: "dev.mxcl.Path.relative")!
|
||||
}
|
||||
|
||||
extension Path: Codable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let value = try decoder.singleValueContainer().decode(String.self)
|
||||
if value.hasPrefix("/") {
|
||||
string = value
|
||||
} else {
|
||||
guard let root = decoder.userInfo[.relativePath] as? Path else {
|
||||
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Path cannot decode a relative path if `userInfo[.relativePath]` not set to a Path object."))
|
||||
}
|
||||
string = (root/value).string
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
if let root = encoder.userInfo[.relativePath] as? Path {
|
||||
try container.encode(relative(to: root))
|
||||
} else {
|
||||
try container.encode(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
109
Sources/Path+FileManager.swift
Normal file
109
Sources/Path+FileManager.swift
Normal file
@@ -0,0 +1,109 @@
|
||||
import Foundation
|
||||
|
||||
public extension Path {
|
||||
/**
|
||||
Copies 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: copy(into:overwrite:)
|
||||
*/
|
||||
@discardableResult
|
||||
public func copy(to: Path, overwrite: Bool = false) throws -> Path {
|
||||
if overwrite, to.exists {
|
||||
try FileManager.default.removeItem(at: to.url)
|
||||
}
|
||||
try FileManager.default.copyItem(atPath: string, toPath: to.string)
|
||||
return to
|
||||
}
|
||||
|
||||
/**
|
||||
Copies 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 the newly copied file.
|
||||
- SeeAlso: copy(into:overwrite:)
|
||||
*/
|
||||
@discardableResult
|
||||
public func copy(into: Path, overwrite: Bool = false) throws -> Path {
|
||||
if !into.exists {
|
||||
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
||||
} else if overwrite, !into.isDirectory {
|
||||
try into.delete()
|
||||
}
|
||||
let rv = into/basename()
|
||||
try FileManager.default.copyItem(at: url, to: rv.url)
|
||||
return rv
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func move(into: Path) throws -> Path {
|
||||
if !into.exists {
|
||||
try into.mkpath()
|
||||
} else if !into.isDirectory {
|
||||
throw CocoaError.error(.fileWriteFileExists)
|
||||
}
|
||||
try FileManager.default.moveItem(at: url, to: into.join(basename()).url)
|
||||
return self
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func delete() throws {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
@discardableResult
|
||||
func touch() throws -> Path {
|
||||
return try "".write(to: self)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
@discardableResult
|
||||
public func mkdir() throws -> Path {
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil)
|
||||
} catch CocoaError.Code.fileWriteFileExists {
|
||||
// noop
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
@inlinable
|
||||
@discardableResult
|
||||
public func mkpath() throws -> Path {
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
|
||||
} catch CocoaError.Code.fileWriteFileExists {
|
||||
// noop
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/// - Note: If file doesn’t exist, creates file
|
||||
/// - Note: If file is not writable, makes writable first, resetting permissions after the write
|
||||
@discardableResult
|
||||
public func replaceContents(with contents: String, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> Path {
|
||||
let resetPerms: Int?
|
||||
if exists, !isWritable {
|
||||
resetPerms = try FileManager.default.attributesOfItem(atPath: string)[.posixPermissions] as? Int
|
||||
let perms = resetPerms ?? 0o777
|
||||
try chmod(perms | 0o200)
|
||||
} else {
|
||||
resetPerms = nil
|
||||
}
|
||||
|
||||
defer {
|
||||
_ = try? resetPerms.map(self.chmod)
|
||||
}
|
||||
|
||||
try contents.write(to: self)
|
||||
|
||||
return self
|
||||
}
|
||||
}
|
||||
21
Sources/Path+StringConvertibles.swift
Normal file
21
Sources/Path+StringConvertibles.swift
Normal file
@@ -0,0 +1,21 @@
|
||||
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 {
|
||||
public var description: String {
|
||||
return string
|
||||
}
|
||||
}
|
||||
|
||||
extension Path: CustomDebugStringConvertible {
|
||||
public var debugDescription: String {
|
||||
return "Path(string: \(string))"
|
||||
}
|
||||
}
|
||||
27
Sources/Path+ls.swift
Normal file
27
Sources/Path+ls.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
import Foundation
|
||||
|
||||
public extension Path {
|
||||
/// same as the `ls` command ∴ is ”shallow”
|
||||
func ls() throws -> [Entry] {
|
||||
let relativePaths = try FileManager.default.contentsOfDirectory(atPath: string)
|
||||
func convert(relativePath: String) -> Entry {
|
||||
let path = self/relativePath
|
||||
return Entry(kind: path.isDirectory ? .directory : .file, path: path)
|
||||
}
|
||||
return relativePaths.map(convert)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Array where Element == Path.Entry {
|
||||
var directories: [Path] {
|
||||
return compactMap {
|
||||
$0.kind == .directory ? $0.path : nil
|
||||
}
|
||||
}
|
||||
|
||||
func files(withExtension ext: String) -> [Path] {
|
||||
return compactMap {
|
||||
$0.kind == .file && $0.path.extension == ext ? $0.path : nil
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Sources/Path->Bool.swift
Normal file
21
Sources/Path->Bool.swift
Normal file
@@ -0,0 +1,21 @@
|
||||
import Foundation
|
||||
|
||||
public extension Path {
|
||||
var isWritable: Bool {
|
||||
return FileManager.default.isWritableFile(atPath: string)
|
||||
}
|
||||
|
||||
var isDirectory: Bool {
|
||||
var isDir: ObjCBool = false
|
||||
return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && isDir.boolValue
|
||||
}
|
||||
|
||||
var isFile: Bool {
|
||||
var isDir: ObjCBool = true
|
||||
return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && !isDir.boolValue
|
||||
}
|
||||
|
||||
var exists: Bool {
|
||||
return FileManager.default.fileExists(atPath: string)
|
||||
}
|
||||
}
|
||||
102
Sources/Path.swift
Normal file
102
Sources/Path.swift
Normal file
@@ -0,0 +1,102 @@
|
||||
import Foundation
|
||||
|
||||
public struct Path: Equatable, Hashable, Comparable {
|
||||
public let string: String
|
||||
|
||||
public static var cwd: Path {
|
||||
return Path(string: FileManager.default.currentDirectoryPath)
|
||||
}
|
||||
|
||||
public static var root: Path {
|
||||
return Path(string: "/")
|
||||
}
|
||||
|
||||
public static var home: Path {
|
||||
return Path(string: NSHomeDirectory())
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public var `extension`: String {
|
||||
return (string as NSString).pathExtension
|
||||
}
|
||||
|
||||
/// - Note: always returns a valid path, `Path.root.parent` *is* `Path.root`
|
||||
public var parent: Path {
|
||||
return Path(string: (string as NSString).deletingLastPathComponent)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public var url: URL {
|
||||
return URL(fileURLWithPath: string)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//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: "/")
|
||||
}
|
||||
}
|
||||
|
||||
public func join<S>(_ part: S) -> Path where S: StringProtocol {
|
||||
//TODO standardizingPath does more than we want really (eg tilde expansion)
|
||||
let str = (string as NSString).appendingPathComponent(String(part))
|
||||
return Path(string: (str as NSString).standardizingPath)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public static func <(lhs: Path, rhs: Path) -> Bool {
|
||||
return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending
|
||||
}
|
||||
|
||||
public struct Entry {
|
||||
public enum Kind {
|
||||
case file
|
||||
case directory
|
||||
}
|
||||
public let kind: Kind
|
||||
public let path: Path
|
||||
}
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol {
|
||||
return lhs.join(rhs)
|
||||
}
|
||||
21
Sources/TemporaryDirectory.swift
Normal file
21
Sources/TemporaryDirectory.swift
Normal file
@@ -0,0 +1,21 @@
|
||||
import Foundation
|
||||
|
||||
public class TemporaryDirectory {
|
||||
public let url: URL
|
||||
public var path: Path { return Path(string: url.path) }
|
||||
|
||||
public init() throws {
|
||||
url = try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: URL(fileURLWithPath: "/"), create: true)
|
||||
}
|
||||
|
||||
deinit {
|
||||
_ = try? FileManager.default.removeItem(at: url)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Path {
|
||||
static func mktemp<T>(body: (Path) throws -> T) throws -> T {
|
||||
let tmp = try TemporaryDirectory()
|
||||
return try body(tmp.path)
|
||||
}
|
||||
}
|
||||
8
Tests/LinuxMain.swift
Normal file
8
Tests/LinuxMain.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
import XCTest
|
||||
|
||||
import PathTests
|
||||
|
||||
var tests = [XCTestCaseEntry]()
|
||||
tests += PathTests.__allTests()
|
||||
|
||||
XCTMain(tests)
|
||||
96
Tests/PathTests/PathTests.swift
Normal file
96
Tests/PathTests/PathTests.swift
Normal file
@@ -0,0 +1,96 @@
|
||||
import XCTest
|
||||
import Path
|
||||
|
||||
class PathTests: XCTestCase {
|
||||
func testConcatenation() {
|
||||
XCTAssertEqual((Path.root/"bar").string, "/bar")
|
||||
XCTAssertEqual(Path.cwd.string, FileManager.default.currentDirectoryPath)
|
||||
XCTAssertEqual((Path.root/"/bar").string, "/bar")
|
||||
XCTAssertEqual((Path.root/"///bar").string, "/bar")
|
||||
XCTAssertEqual((Path.root/"foo///bar////").string, "/foo/bar")
|
||||
XCTAssertEqual((Path.root/"foo"/"/bar").string, "/foo/bar")
|
||||
}
|
||||
|
||||
func testEnumeration() throws {
|
||||
let tmpdir_ = try TemporaryDirectory()
|
||||
let tmpdir = tmpdir_.path
|
||||
try tmpdir.join("a").mkdir().join("c").touch()
|
||||
try tmpdir.join("b").touch()
|
||||
try tmpdir.join("c").touch()
|
||||
|
||||
var paths = Set<String>()
|
||||
var dirs = 0
|
||||
for entry in try tmpdir.ls() {
|
||||
if entry.kind == .directory {
|
||||
dirs += 1
|
||||
}
|
||||
paths.insert(entry.path.basename())
|
||||
}
|
||||
XCTAssertEqual(dirs, 1)
|
||||
XCTAssertEqual(paths, ["a", "b", "c"])
|
||||
}
|
||||
|
||||
func testRelativeTo() {
|
||||
XCTAssertEqual((Path.root/"tmp/foo").relative(to: .root/"tmp"), "foo")
|
||||
XCTAssertEqual((Path.root/"tmp/foo/bar").relative(to: .root/"tmp/baz"), "../foo/bar")
|
||||
}
|
||||
|
||||
func testExists() {
|
||||
XCTAssert(Path.root.exists)
|
||||
XCTAssert((Path.root/"Users").exists)
|
||||
}
|
||||
|
||||
func testIsDirectory() {
|
||||
XCTAssert(Path.root.isDirectory)
|
||||
XCTAssert((Path.root/"Users").isDirectory)
|
||||
}
|
||||
|
||||
func testMktemp() throws {
|
||||
var path: Path!
|
||||
try Path.mktemp {
|
||||
path = $0
|
||||
XCTAssert(path.isDirectory)
|
||||
}
|
||||
XCTAssert(!path.exists)
|
||||
XCTAssert(!path.isDirectory)
|
||||
}
|
||||
|
||||
func testMkpathIfExists() throws {
|
||||
try Path.mktemp {
|
||||
for _ in 0...1 {
|
||||
try $0.join("a").mkdir()
|
||||
try $0.join("b/c").mkpath()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testBasename() {
|
||||
XCTAssertEqual(Path.root.join("foo.bar").basename(dropExtension: true), "foo")
|
||||
XCTAssertEqual(Path.root.join("foo").basename(dropExtension: true), "foo")
|
||||
XCTAssertEqual(Path.root.join("foo.").basename(dropExtension: true), "foo.")
|
||||
XCTAssertEqual(Path.root.join("foo.bar.baz").basename(dropExtension: true), "foo.bar")
|
||||
}
|
||||
|
||||
func testCodable() throws {
|
||||
let input = [Path.root/"bar"]
|
||||
XCTAssertEqual(try JSONDecoder().decode([Path].self, from: try JSONEncoder().encode(input)), input)
|
||||
}
|
||||
|
||||
func testRelativePathCodable() throws {
|
||||
let root = Path.root/"bar"
|
||||
let input = [
|
||||
root/"foo"
|
||||
]
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.userInfo[.relativePath] = root
|
||||
let data = try encoder.encode(input)
|
||||
|
||||
XCTAssertEqual(try JSONSerialization.jsonObject(with: data) as? [String], ["foo"])
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
XCTAssertThrowsError(try decoder.decode([Path].self, from: data))
|
||||
decoder.userInfo[.relativePath] = root
|
||||
XCTAssertEqual(try decoder.decode([Path].self, from: data), input)
|
||||
}
|
||||
}
|
||||
24
Tests/PathTests/XCTestManifests.swift
Normal file
24
Tests/PathTests/XCTestManifests.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
import XCTest
|
||||
|
||||
extension PathTests {
|
||||
static let __allTests = [
|
||||
("testBasename", testBasename),
|
||||
("testCodable", testCodable),
|
||||
("testConcatenation", testConcatenation),
|
||||
("testEnumeration", testEnumeration),
|
||||
("testExists", testExists),
|
||||
("testIsDirectory", testIsDirectory),
|
||||
("testMkpathIfExists", testMkpathIfExists),
|
||||
("testMktemp", testMktemp),
|
||||
("testRelativePathCodable", testRelativePathCodable),
|
||||
("testRelativeTo", testRelativeTo),
|
||||
]
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
public func __allTests() -> [XCTestCaseEntry] {
|
||||
return [
|
||||
testCase(PathTests.__allTests),
|
||||
]
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user