Compare commits

...

17 Commits
0.1.0 ... 0.3.0

Author SHA1 Message Date
Max Howell
6c24de4875 Fix actually deploying 2019-01-19 15:08:11 -05:00
Max Howell
6df12b3bb9 Refer to alternative packages 2019-01-19 15:07:47 -05:00
Max Howell
d52cdf96c4 Merge pull request #4 from mxcl/more-docs
More docs
2019-01-19 14:46:46 -05:00
Max Howell
3eda9a9741 100% documentation please 2019-01-19 14:36:27 -05:00
Max Howell
29149da72b Use FileManager.homeDirectory where possible 2019-01-19 14:35:42 -05:00
Max Howell
cd30e89808 Remove TemporaryDirectory
This violates the responsibility of this framework.
2019-01-19 14:20:12 -05:00
Max Howell
9f76eeb507 Link to Jazzy docs 2019-01-19 12:55:18 -05:00
Max Howell
b5bdaa6ceb Fix Jazzy root-url 2019-01-19 12:52:47 -05:00
Max Howell
17dd706115 Merge pull request #3 from mxcl/jazzy
Generate Jazzy docs in Travis
2019-01-19 12:40:21 -05:00
Max Howell
903038ae80 Generate Jazzy docs in Travis 2019-01-19 12:34:35 -05:00
Max Howell
f56811d64f Document various rules and caveats 2019-01-18 17:47:04 -05:00
Max Howell
f99e7b5ae7 Test iOS, tvOS & watchOS 2019-01-18 13:37:28 -05:00
Max Howell
9553968d66 Add Travis badge 2019-01-18 12:27:05 -05:00
Max Howell
3541c6ec8d Tidy 2019-01-18 09:25:24 -05:00
Max Howell
b4c92f86dc Fix tests on Linux 2019-01-17 18:23:22 -05:00
Max Howell
cac06d89fb Linux 2019-01-17 17:31:29 -05:00
Max Howell
4af0ee3983 Missing functions 2019-01-17 17:31:14 -05:00
14 changed files with 401 additions and 54 deletions

66
.travis.yml Normal file
View File

@@ -0,0 +1,66 @@
# only run for: merge commits, releases and pull-requests
if: type != push OR branch = master OR branch =~ /^\d+\.\d+(\.\d+)?(-\S*)?$/
stages:
- name: test
- name: deploy
if: branch =~ ^\d+\.\d+\.\d+$
os: osx
language: swift
osx_image: xcode10.1
xcode_project: Path.swift.xcodeproj
xcode_scheme: Path.swift-Package
jobs:
include:
- script: swift test
name: macOS
- &xcodebuild
before_install: swift package generate-xcodeproj
xcode_destination: platform=iOS Simulator,OS=latest,name=iPhone XS
name: iOS
- <<: *xcodebuild
xcode_destination: platform=tvOS Simulator,OS=latest,name=Apple TV
name: tvOS
- <<: *xcodebuild
name: watchOS
script: |
set -o pipefail
xcodebuild \
-project Path.swift.xcodeproj \
-scheme Path.swift-Package \
-destination 'platform=watchOS Simulator,OS=latest,name=Apple Watch Series 4 - 40mm' \
build | xcpretty
- env: SWIFT_VERSION=4.2.1
os: linux
name: Linux
language: generic
dist: trusty
sudo: false
install: eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"
script: swift test
- <<: *xcodebuild
stage: deploy
name: Jazzy
install: gem install jazzy
script: |
jazzy \
--min-acl internal \
--no-hide-documentation-coverage \
--theme fullwidth \
--output output \
--readme README.md \
--root-url https://mxcl.github.io/Path.swift/ \
--github_url https://github.com/mxcl/Path.swift
deploy:
provider: pages
skip-cleanup: true
github-token: $GITHUB_TOKEN
local-dir: output
on:
tags: true

View File

@@ -1,9 +1,11 @@
# Path.swift # Path.swift ![badge-platforms] ![badge-languages] [![Build Status](https://travis-ci.com/mxcl/Path.swift.svg)](https://travis-ci.com/mxcl/Path.swift)
A file-system pathing library focused on developer experience and robust A file-system pathing library focused on developer experience and robust
endresults. endresults.
```swift ```swift
import Path
// convenient static members // convenient static members
let home = Path.home let home = Path.home
@@ -14,10 +16,10 @@ let docs = Path.home/"Documents"
let path = Path(userInput) ?? Path.cwd/userInput let path = Path(userInput) ?? Path.cwd/userInput
// chainable syntax so you have less boilerplate // chainable syntax so you have less boilerplate
try Path.home.join("foo").mkpath().join("bar").chmod(0o555) try Path.home.join("foo").mkpath().join("bar").touch().chmod(0o555)
// easy file-management // easy file-management
try Path.root.join("foo").copy(to: Path.root.join("bar")) try Path.root.join("foo").copy(to: Path.root/"bar")
// careful API to avoid common bugs // careful API to avoid common bugs
try Path.root.join("foo").copy(into: Path.root.mkdir("bar")) try Path.root.join("foo").copy(into: Path.root.mkdir("bar"))
@@ -26,7 +28,8 @@ try Path.root.join("foo").copy(into: Path.root.mkdir("bar"))
// were meant to be directory destinations // were meant to be directory destinations
``` ```
Paths are just string representations, there *may not* be a real file there. We emphasize safety and correctness, just like Swift, and also just
like Swift, we provide a thoughtful and comprehensive (yet concise) API.
# Support mxcl # Support mxcl
@@ -38,6 +41,10 @@ can continue to make tools and software you need and love. I appreciate it x.
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160"> <img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160">
</a> </a>
# Handbook
Our [online API documentation] is automatically updated for new releases.
## Codable ## Codable
We support `Codable` as you would expect: We support `Codable` as you would expect:
@@ -112,16 +119,16 @@ We provide `ls()`, called because it behaves like the Terminal `ls` function,
the name thus implies its behavior, ie. that it is not recursive. the name thus implies its behavior, ie. that it is not recursive.
```swift ```swift
for path in Path.home.ls() { for entry in Path.home.ls() {
print(path.path) print(entry.path)
print(path.kind) // .directory or .file print(entry.kind) // .directory or .file
} }
for path in Path.home.ls() where path.kind == .file { for entry in Path.home.ls() where entry.kind == .file {
// //
} }
for path in Path.home.ls() where path.mtime > yesterday { for entry in Path.home.ls() where entry.path.mtime > yesterday {
// //
} }
@@ -132,6 +139,38 @@ let dirs = Path.home.ls().directories().filter {
let swiftFiles = Path.home.ls().files(withExtension: "swift") let swiftFiles = Path.home.ls().files(withExtension: "swift")
``` ```
# Rules & Caveats
Paths are just string representations, there *might not* be a real file there.
```swift
Path.home/"b" // => /Users/mxcl/b
// joining multiple strings works as youd expect
Path.home/"b"/"c" // => /Users/mxcl/b/c
// joining multiple parts at a time is fine
Path.home/"b/c" // => /Users/mxcl/b/c
// joining with absolute paths omits prefixed slash
Path.home/"/b" // => /Users/mxcl/b
// of course, feel free to join variables:
let b = "b"
let c = "c"
Path.home/b/c // => /Users/mxcl/b/c
// tilde is not special here
Path.root/"~b" // => /~b
Path.root/"~/b" // => /~/b
// but is here
Path("~/foo")! // => /Users/foo
// this does not work though
Path("~foo") // => nil
```
# Installation # Installation
SwiftPM only: SwiftPM only:
@@ -143,3 +182,14 @@ package.append(.package(url: "https://github.com/mxcl/Path.swift", from: "0.0.0"
### Get push notifications for new releases ### Get push notifications for new releases
https://codebasesaga.com/canopy/ https://codebasesaga.com/canopy/
# Alternatives
* [PathKit](https://github.com/kylef/PathKit) by Kyle Fuller
* [Files](https://github.com/JohnSundell/Files) by John Sundell
* [Utility](https://github.com/apple/swift-package-manager) by Apple
[badge-platforms]: https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20iOS%20%7C%20tvOS%20%7C%20watchOS-lightgrey.svg
[badge-languages]: https://img.shields.io/badge/swift-4.2-orange.svg
[online API documentation]: https://mxcl.github.io/Path.swift/

View File

@@ -1,22 +1,31 @@
import Foundation import Foundation
public extension Bundle { public extension Bundle {
/// Returns the path for requested resource in this bundle.
func path(forResource: String, ofType: String?) -> Path? { func path(forResource: String, ofType: String?) -> Path? {
let f: (String?, String?) -> String? = path(forResource:ofType:) let f: (String?, String?) -> String? = path(forResource:ofType:)
let str = f(forResource, ofType) let str = f(forResource, ofType)
return str.flatMap(Path.init) return str.flatMap(Path.init)
} }
/// Returns the path for the shared-frameworks directory in this bundle.
public var sharedFrameworks: Path? { public var sharedFrameworks: Path? {
return sharedFrameworksPath.flatMap(Path.init) return sharedFrameworksPath.flatMap(Path.init)
} }
/// Returns the path for the resources directory in this bundle.
public var resources: Path? { public var resources: Path? {
return resourcePath.flatMap(Path.init) return resourcePath.flatMap(Path.init)
} }
/// Returns the path for this bundle.
public var path: Path {
return Path(string: bundlePath)
}
} }
public extension String { public extension String {
/// Initializes this `String` with the contents of the provided path.
@inlinable @inlinable
init(contentsOf path: Path) throws { init(contentsOf path: Path) throws {
try self.init(contentsOfFile: path.string) try self.init(contentsOfFile: path.string)
@@ -32,6 +41,7 @@ public extension String {
} }
public extension Data { public extension Data {
/// Initializes this `Data` with the contents of the provided path.
@inlinable @inlinable
init(contentsOf path: Path) throws { init(contentsOf path: Path) throws {
try self.init(contentsOf: path.url) try self.init(contentsOf: path.url)
@@ -43,7 +53,11 @@ public extension Data {
func write(to: Path, atomically: Bool = false) throws -> Path { func write(to: Path, atomically: Bool = false) throws -> Path {
let opts: NSData.WritingOptions let opts: NSData.WritingOptions
if atomically { if atomically {
#if !os(Linux)
opts = .atomicWrite opts = .atomicWrite
#else
opts = .atomic
#endif
} else { } else {
opts = [] opts = []
} }

View File

@@ -43,14 +43,18 @@ public extension Path {
return self 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 { public var mtime: Date {
do { do {
let attrs = try FileManager.default.attributesOfItem(atPath: string) let attrs = try FileManager.default.attributesOfItem(atPath: string)
return attrs[.modificationDate] as? Date ?? attrs[.creationDate] as? Date ?? Date() return attrs[.modificationDate] as? Date ?? attrs[.creationDate] as? Date ?? Date()
} catch { } catch {
//TODO print(error) //TODO log error
return Date() return Date(timeIntervalSince1970: 0)
} }
} }
} }

View File

@@ -1,6 +1,7 @@
import Foundation import Foundation
public extension CodingUserInfoKey { public extension CodingUserInfoKey {
/// If set paths are encoded as relative to this path.
static let relativePath = CodingUserInfoKey(rawValue: "dev.mxcl.Path.relative")! static let relativePath = CodingUserInfoKey(rawValue: "dev.mxcl.Path.relative")!
} }

View File

@@ -41,6 +41,34 @@ public extension Path {
return rv 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 {
try FileManager.default.removeItem(at: to.url)
}
try FileManager.default.moveItem(at: url, to: to.url)
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 @discardableResult
public func move(into: Path) throws -> Path { public func move(into: Path) throws -> Path {
if !into.exists { if !into.exists {
@@ -48,45 +76,82 @@ public extension Path {
} else if !into.isDirectory { } else if !into.isDirectory {
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
try FileManager.default.moveItem(at: url, to: into.join(basename()).url) let rv = into/basename()
return self try FileManager.default.moveItem(at: url, to: rv.url)
return rv
} }
/// Deletes the path, recursively if a directory.
@inlinable @inlinable
public func delete() throws { public func delete() throws {
try FileManager.default.removeItem(at: url) try FileManager.default.removeItem(at: url)
} }
/**
Creates an empty file at this path.
- Returns: `self` to allow chaining.
*/
@inlinable @inlinable
@discardableResult @discardableResult
func touch() throws -> Path { func touch() throws -> Path {
return try "".write(to: self) return try "".write(to: self)
} }
@inlinable /// Helper due to Linux Swift being incomplete.
private func _foo(go: () throws -> Void) throws {
#if !os(Linux)
do {
try go()
} catch CocoaError.Code.fileWriteFileExists {
// noop
}
#else
do {
try go()
} catch {
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 @discardableResult
public func mkdir() throws -> Path { public func mkdir() throws -> Path {
do { try _foo {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil) try FileManager.default.createDirectory(at: self.url, withIntermediateDirectories: false, attributes: nil)
} catch CocoaError.Code.fileWriteFileExists {
// noop
} }
return self return self
} }
@inlinable /**
Creates the directory at this path.
- Note: Creates any intermediary directories, if required.
- Returns: `self` to allow chaining.
*/
@discardableResult @discardableResult
public func mkpath() throws -> Path { public func mkpath() throws -> Path {
do { try _foo {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
} catch CocoaError.Code.fileWriteFileExists {
// noop
} }
return self return self
} }
/// - Note: If file doesnt 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 doesnt 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 @discardableResult
public func replaceContents(with contents: String, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> Path { public func replaceContents(with contents: String, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> Path {
let resetPerms: Int? let resetPerms: Int?

View File

@@ -9,12 +9,14 @@ extension Path: LosslessStringConvertible {
} }
extension Path: CustomStringConvertible { extension Path: CustomStringConvertible {
/// Returns `Path.string`
public var description: String { public var description: String {
return string return string
} }
} }
extension Path: CustomDebugStringConvertible { extension Path: CustomDebugStringConvertible {
/// Returns eg. `Path(string: "/foo")`
public var debugDescription: String { public var debugDescription: String {
return "Path(string: \(string))" return "Path(string: \(string))"
} }

View File

@@ -13,12 +13,14 @@ public extension Path {
} }
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.
var directories: [Path] { var directories: [Path] {
return compactMap { return compactMap {
$0.kind == .directory ? $0.path : nil $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] { func files(withExtension ext: String) -> [Path] {
return compactMap { return compactMap {
$0.kind == .file && $0.path.extension == ext ? $0.path : nil $0.kind == .file && $0.path.extension == ext ? $0.path : nil

View File

@@ -1,20 +1,29 @@
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.
var isWritable: Bool { var isWritable: Bool {
return FileManager.default.isWritableFile(atPath: string) return FileManager.default.isWritableFile(atPath: string)
} }
/// Returns true if the path represents an actual directory.
var isDirectory: Bool { var isDirectory: Bool {
var isDir: ObjCBool = false var isDir: ObjCBool = false
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 filesystem entry that is *not* a directory.
var isFile: Bool { var isFile: Bool {
var isDir: ObjCBool = true var isDir: ObjCBool = true
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 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 { var exists: Bool {
return FileManager.default.fileExists(atPath: string) return FileManager.default.fileExists(atPath: string)
} }

View File

@@ -1,35 +1,82 @@
import Foundation 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 { public struct Path: Equatable, Hashable, Comparable {
/// The underlying filesystem path
public let string: String public let string: String
/// 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)
} }
/// Returns a `Path` representing the root path.
public static var root: Path { public static var root: Path {
return Path(string: "/") return Path(string: "/")
} }
/// Returns a `Path` representing the users home directory
public static var home: Path { public static var home: Path {
return Path(string: NSHomeDirectory()) let string: String
#if os(macOS)
if #available(OSX 10.12, *) {
string = FileManager.default.homeDirectoryForCurrentUser.path
} else {
string = NSHomeDirectory()
}
#else
string = NSHomeDirectory()
#endif
return Path(string: string)
} }
/**
Returns the filename extension of this path.
- Remark: Implemented via `NSString.pathExtension`.
*/
@inlinable @inlinable
public var `extension`: String { public var `extension`: String {
return (string as NSString).pathExtension 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 { public var parent: Path {
return Path(string: (string as NSString).deletingLastPathComponent) return Path(string: (string as NSString).deletingLastPathComponent)
} }
/// Returns a `URL` representing this file path.
@inlinable @inlinable
public var url: URL { public var url: URL {
return URL(fileURLWithPath: string) 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 filenames basename.
- Parameter dropExtension: If `true` returns the basename without its file extension.
*/
public func basename(dropExtension: Bool = false) -> String { public func basename(dropExtension: Bool = false) -> String {
let str = string as NSString let str = string as NSString
if !dropExtension { if !dropExtension {
@@ -44,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 { public func relative(to base: Path) -> String {
// Split the two paths into their components. // Split the two paths into their components.
// FIXME: The is needs to be optimized to avoid unncessary copying. // FIXME: The is needs to be optimized to avoid unncessary copying.
@@ -75,27 +128,59 @@ public struct Path: Equatable, Hashable, Comparable {
} }
} }
public func join<S>(_ 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<S>(_ pathComponent: S) -> Path where S: StringProtocol {
//TODO standardizingPath does more than we want really (eg tilde expansion) //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) return Path(string: (str as NSString).standardizingPath)
} }
/// Returns the locale-aware sort order for the two paths.
@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.
public struct Entry { public struct Entry {
/// The kind of this directory entry.
public enum Kind { public enum Kind {
/// The path is a file.
case file case file
/// The path is a directory.
case directory case directory
} }
/// The kind of this entry.
public let kind: Kind public let kind: Kind
/// The path of this entry.
public let path: Path 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 @inlinable
public func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol { public func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol {
return lhs.join(rhs) return lhs.join(rhs)

View File

@@ -1,21 +0,0 @@
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)
}
}

View File

@@ -37,12 +37,12 @@ class PathTests: XCTestCase {
func testExists() { func testExists() {
XCTAssert(Path.root.exists) XCTAssert(Path.root.exists)
XCTAssert((Path.root/"Users").exists) XCTAssert((Path.root/"bin").exists)
} }
func testIsDirectory() { func testIsDirectory() {
XCTAssert(Path.root.isDirectory) XCTAssert(Path.root.isDirectory)
XCTAssert((Path.root/"Users").isDirectory) XCTAssert((Path.root/"bin").isDirectory)
} }
func testMktemp() throws { func testMktemp() throws {
@@ -93,4 +93,20 @@ class PathTests: XCTestCase {
decoder.userInfo[.relativePath] = root decoder.userInfo[.relativePath] = root
XCTAssertEqual(try decoder.decode([Path].self, from: data), input) XCTAssertEqual(try decoder.decode([Path].self, from: data), input)
} }
func testJoin() {
let prefix = Path.root/"Users/mxcl"
XCTAssertEqual(prefix/"b", Path("/Users/mxcl/b"))
XCTAssertEqual(prefix/"b"/"c", Path("/Users/mxcl/b/c"))
XCTAssertEqual(prefix/"b/c", Path("/Users/mxcl/b/c"))
XCTAssertEqual(prefix/"/b", Path("/Users/mxcl/b"))
let b = "b"
let c = "c"
XCTAssertEqual(prefix/b/c, Path("/Users/mxcl/b/c"))
XCTAssertEqual(Path.root/"~b", Path("/~b"))
XCTAssertEqual(Path.root/"~/b", Path("/~/b"))
XCTAssertEqual(Path("~/foo"), Path.home/"foo")
XCTAssertNil(Path("~foo"))
}
} }

View File

@@ -0,0 +1,54 @@
@testable import Path
import Foundation
class TemporaryDirectory {
let url: URL
var path: Path { return Path(string: url.path) }
/**
Creates a new temporary directory.
The directory is recursively deleted when this object deallocates.
If you need a temporary directory on a specific volume use the `appropriateFor`
parameter.
- Important: If you are moving a file, ensure to use the `appropriateFor`
parameter, since it is volume aware and moving the file across volumes will take
exponentially longer!
- Important: The `appropriateFor` parameter does not work on Linux.
- Parameter appropriateFor: The temporary directory will be located on this
volume.
*/
init(appropriateFor: URL? = nil) throws {
#if !os(Linux)
let appropriate: URL
if let appropriateFor = appropriateFor {
appropriate = appropriateFor
} else if #available(OSX 10.12, iOS 10, tvOS 10, watchOS 3, *) {
appropriate = FileManager.default.temporaryDirectory
} else {
appropriate = URL(fileURLWithPath: NSTemporaryDirectory())
}
url = try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: appropriate, create: true)
#else
let envs = ProcessInfo.processInfo.environment
let env = envs["TMPDIR"] ?? envs["TEMP"] ?? envs["TMP"] ?? "/tmp"
let dir = Path.root/env/"swift-sh.XXXXXX"
var template = [UInt8](dir.string.utf8).map({ Int8($0) }) + [Int8(0)]
guard mkdtemp(&template) != nil else { throw CocoaError.error(.featureUnsupported) }
url = URL(fileURLWithPath: String(cString: template))
#endif
}
deinit {
_ = try? FileManager.default.removeItem(at: url)
}
}
extension Path {
static func mktemp<T>(body: (Path) throws -> T) throws -> T {
let tmp = try TemporaryDirectory()
return try body(tmp.path)
}
}

View File

@@ -15,7 +15,7 @@ extension PathTests {
] ]
} }
#if !os(macOS) #if os(Linux)
public func __allTests() -> [XCTestCaseEntry] { public func __allTests() -> [XCTestCaseEntry] {
return [ return [
testCase(PathTests.__allTests), testCase(PathTests.__allTests),