Compare commits

..

55 Commits
0.2.2 ... 0.7.0

Author SHA1 Message Date
Max Howell
bbf1f24ef6 Fix Cocoapods deploy 2019-01-26 14:42:00 -05:00
Max Howell
c08ccdfb30 Merge pull request #15 from mxcl/dynamic-members
Dynamic members
2019-01-26 13:34:17 -05:00
Max Howell
859164e59f Dynamic Members 2019-01-26 13:23:25 -05:00
Max Howell
44be1c45a9 Add Path.ctime 2019-01-26 13:17:39 -05:00
Max Howell
99b948f9c1 Minor documentation fixes
[ci skip]
2019-01-26 13:17:39 -05:00
Max Howell
3beba13677 Merge pull request #14 from mxcl/delete-noop
Delete noop
2019-01-26 11:12:17 -05:00
Max Howell
bafb05ff54 Document noop behavior 2019-01-26 11:05:49 -05:00
Max Howell
356a1b3ac2 Delete is a noop if file doesn’t exist
Closes #11.
2019-01-26 11:05:31 -05:00
Max Howell
6d8712f4d6 Remove mkpath, add mkdir(.p) 2019-01-26 11:05:10 -05:00
Max Howell
8744b68709 Make nodoc work for Codable 2019-01-26 10:59:20 -05:00
Max Howell
9ea32048f7 Merge pull request #13 from mxcl/more-docs
Improved documentation; Fixes #12
2019-01-25 21:20:24 -05:00
Max Howell
4b16dac3bf Improved documentation; Fixes #12 2019-01-25 20:46:37 -05:00
Max Howell
b613449232 Making this static will fix documentation location
[ci skip]
2019-01-25 12:23:54 -05:00
Max Howell
db135e32c8 Tag 0.5.0 2019-01-25 11:08:57 -05:00
Max Howell
bfcc48db20 Merge pull request #10 from mxcl/fixes
Swift 5 / Xcode 10.2 / Fixes
2019-01-25 11:04:35 -05:00
Max Howell
b0bf0d0074 This is not yet fixed in Linux Swift 5.0 2019-01-25 10:55:02 -05:00
Max Howell
5f364fe21b Test against Swift 5 snapshot 2019-01-25 10:40:10 -05:00
Max Howell
fdff3bcc05 Swift 5 --warnings 2019-01-24 15:02:06 -05:00
Max Howell
2388c384a1 Swift 5 Manifest (untested until Travis catches up) 2019-01-24 15:01:53 -05:00
Max Howell
80960f5876 Don’t overwrite a file with a directory 2019-01-24 14:44:01 -05:00
Max Howell
9eca479f7b Fix mtime return so it is as per doc contract 2019-01-24 14:43:45 -05:00
Max Howell
ca9f1e0a74 Parallelize tests 2019-01-23 11:25:51 -05:00
Max Howell
de4fb3ae47 Remove replaceContents, user can use String.write 2019-01-22 17:57:28 -05:00
Max Howell
58d026c8a9 Fix copy(into:) overwrite mode
Seems like Linux Foundation has a bug

I checked, seems fixed in Swift 5. But added a swift version check se we can verify and if not report the bug.
2019-01-22 15:26:04 -05:00
Max Howell
43d3e0a745 Improve docs 2019-01-21 14:30:12 -05:00
Max Howell
21fb03b9d9 Test .. paths work with join 2019-01-21 14:26:56 -05:00
Max Howell
3333c731d3 Fix Travis 2019-01-21 12:57:13 -05:00
Max Howell
e15173cfbc Merge pull request #9 from LucianoPAlmeida/is-path-conveniece-add
Adding convenience extensions on Path->Bool
2019-01-20 19:28:05 -05:00
Luciano Almeida
7be264a38e Adding convenience extensions on Path->Bool 2019-01-20 22:13:13 -02:00
Max Howell
aac81b85a4 Merge pull request #8 from mxcl/better-deploy
Better deploy
2019-01-20 18:53:11 -05:00
Max Howell
3644124a36 Fix testing on tvOS/iOS 2019-01-20 18:44:24 -05:00
Max Howell
ca4ac3ec8f Fix stage types in .travis.yml 2019-01-20 17:33:56 -05:00
Max Howell
920f007660 Fix Linux testEnumerationSkippingHiddenFiles() 2019-01-20 17:32:39 -05:00
Max Howell
751b855a26 Pretest that fails if Linux tests aren't current 2019-01-20 17:25:46 -05:00
Max Howell
c0e5023632 Better deployment of Jazzy and Pods 2019-01-20 17:13:03 -05:00
Max Howell
e0c62108e8 Update Linux tests 2019-01-20 16:30:02 -05:00
Max Howell
5cc2fcbf30 Tag 0.4.1 2019-01-20 16:26:22 -05:00
Max Howell
7595c601e8 Attempt custom Jazzy index/contents 2019-01-20 16:24:06 -05:00
Luciano Almeida
d8ea357459 Adding ls -a like functionality to Path.ls() 2019-01-20 16:23:40 -05:00
Max Howell
86798755be Deploy to CocoaPods 2019-01-20 13:37:34 -05:00
Max Howell
152ad8a8ae Implementations of CommonDirs for Linux 2019-01-20 13:03:38 -05:00
Max Howell
a98ba37e59 Merge pull request #1 from itsallmememe/master
Common Directories
2019-01-20 12:30:29 -05:00
Max Howell
1c2cffada5 Support CocoaPods 2019-01-19 22:12:06 -05:00
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
Niall McCormack
12c7b348d6 added hooks for common directories - Documents, Caches and Application Support 2019-01-19 00:24:20 +08:00
16 changed files with 795 additions and 191 deletions

2
.gitignore vendored
View File

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

View File

@@ -1,6 +1,12 @@
# only run for: merge commits, releases and pull-requests # only run for: merge commits, releases and pull-requests
if: type != push OR branch = master OR branch =~ /^\d+\.\d+(\.\d+)?(-\S*)?$/ if: type != push OR branch = master OR branch =~ /^\d+\.\d+(\.\d+)?(-\S*)?$/
stages:
- name: pretest
- name: test
- name: deploy
if: branch =~ ^\d+\.\d+\.\d+$
os: osx os: osx
language: swift language: swift
osx_image: xcode10.1 osx_image: xcode10.1
@@ -9,18 +15,18 @@ xcode_scheme: Path.swift-Package
jobs: jobs:
include: include:
- script: swift test - script: swift test --parallel
name: macOS name: macOS / Swift 4.2.1
- &xcodebuild - &xcodebuild
before_install: swift package generate-xcodeproj before_install: swift package generate-xcodeproj
xcode_destination: platform=iOS Simulator,OS=latest,name=iPhone XS xcode_destination: platform=iOS Simulator,OS=latest,name=iPhone XS
name: iOS name: iOS / Swift 4.2.1
- <<: *xcodebuild - <<: *xcodebuild
xcode_destination: platform=tvOS Simulator,OS=latest,name=Apple TV xcode_destination: platform=tvOS Simulator,OS=latest,name=Apple TV
name: tvOS name: tvOS / Swift 4.2.1
- <<: *xcodebuild - <<: *xcodebuild
name: watchOS name: watchOS / Swift 4.2.1
script: | script: |
set -o pipefail set -o pipefail
xcodebuild \ xcodebuild \
@@ -29,12 +35,94 @@ jobs:
-destination 'platform=watchOS Simulator,OS=latest,name=Apple Watch Series 4 - 40mm' \ -destination 'platform=watchOS Simulator,OS=latest,name=Apple Watch Series 4 - 40mm' \
build | xcpretty build | xcpretty
- &linux
- env: SWIFT_VERSION=4.2.1 env: SWIFT_VERSION=4.2.1
os: linux os: linux
name: Linux name: Linux / Swift 4.2.1
language: generic language: generic
dist: trusty dist: trusty
sudo: false sudo: false
install: eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" install: eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"
script: swift test script: swift test --parallel
- <<: *linux
env: SWIFT_VERSION='5.0-DEVELOPMENT-SNAPSHOT-2019-01-22-a'
name: Linux / Swift 5.0.0-dev (2019-01-22)
- stage: pretest
name: Check Linux tests are syncd
install: swift test --generate-linuxmain
script: git diff --exit-code
- stage: deploy
name: Jazzy
before_install: |
cat <<\ \ EOF> .jazzy.yaml
module: Path
module_version: TRAVIS_TAG
custom_categories:
- name: Path
children:
- Path
- /(_:_:)
xcodebuild_arguments:
- 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
# two spaces in front of the output which means we need a prefixed
# delimiter which also weirdly stops bash from doing variable substitution
install: gem install jazzy
before_script: swift package generate-xcodeproj
script: jazzy
deploy:
provider: pages
skip-cleanup: true
github-token: $GITHUB_TOKEN
local-dir: output
on:
tags: true
- name: CocoaPods
before_install: |
export DESCRIPTION=$(swift - <<\ \ EOF
import Foundation
struct Response: Decodable { let description: String }
let token = ProcessInfo.processInfo.environment["GITHUB_TOKEN"]!
let url = URL(string: "https://api.github.com/repos/mxcl/Path.swift")!
var rq = URLRequest(url: url)
rq.setValue("token \(token)", forHTTPHeaderField: "Authorization")
let semaphore = DispatchSemaphore(value: 0)
var data: Data!
URLSession.shared.dataTask(with: rq) { d, _, _ in
data = d
semaphore.signal()
}.resume()
semaphore.wait()
let rsp = try JSONDecoder().decode(Response.self, from: data)
print(rsp.description, terminator: "")
EOF)
cat <<\ \ EOF> Path.swift.podspec
Pod::Spec.new do |s|
s.name = 'Path.swift'
s.version = ENV['TRAVIS_TAG']
s.summary = ENV['DESCRIPTION']
s.homepage = 'https://github.com/mxcl/Path.swift'
s.license = { :type => 'Unlicense', :file => 'LICENSE.md' }
s.author = { 'mxcl' => 'mxcl@me.com' }
s.source = { :git => 'https://github.com/mxcl/Path.swift.git', :tag => s.version.to_s }
s.social_media_url = 'https://twitter.com/mxcl'
s.osx.deployment_target = '10.10'
s.ios.deployment_target = '8.0'
s.tvos.deployment_target = '10.0'
s.watchos.deployment_target = '3.0'
s.source_files = 'Sources/*'
s.swift_version = '4.2'
end
EOF
install: gem install cocoapods --pre
script: pod trunk push

20
Package@swift-5.0.swift Normal file
View File

@@ -0,0 +1,20 @@
// swift-tools-version:5.0
import PackageDescription
let pkg = Package(
name: "Path.swift",
products: [
.library(name: "Path", targets: ["Path"]),
],
targets: [
.target(name: "Path", path: "Sources"),
.testTarget(name: "PathTests", dependencies: ["Path"]),
]
)
pkg.platforms = [
.macOS(.v10_10), .iOS(.v8), .tvOS(.v10), .watchOS(.v3)
]
pkg.swiftLanguageVersions = [
.v4_2, .v5
]

118
README.md
View File

@@ -15,20 +15,29 @@ let docs = Path.home/"Documents"
// paths are *always* absolute thus avoiding common bugs // paths are *always* absolute thus avoiding common bugs
let path = Path(userInput) ?? Path.cwd/userInput let path = Path(userInput) ?? Path.cwd/userInput
// chainable syntax so you have less boilerplate // elegant, chainable syntax
try Path.home.join("foo").mkpath().join("bar").chmod(0o555) try Path.home.join("foo").mkdir().join("bar").touch().chmod(0o555)
// sensible considerations
try Path.home.join("bar").mkdir()
try Path.home.join("bar").mkdir() // doesnt throw we already have the desired result
// easy file-management // easy file-management
try Path.root.join("foo").copy(to: Path.root.join("bar")) let bar = try Path.root.join("foo").copy(to: Path.root/"bar")
print(bar) // => /bar
print(bar.isFile) // => true
// careful API to avoid common bugs // careful API considerations so as to avoid common bugs
try Path.root.join("foo").copy(into: Path.root.mkdir("bar")) let foo = try Path.root.join("foo").copy(into: Path.root.join("bar").mkdir())
// ^^ other libraries would make the `to:` form handle both these cases print(foo) // => /bar/foo
// but that can easily lead to bugs where you accidentally write files that print(foo.isFile) // => true
// were meant to be directory destinations
// A practical example: installing a helper executable
try Bundle.resources.join("helper").copy(into: Path.home.join(".local/bin").mkdir(.p)).chmod(0o500)
``` ```
Paths are just string representations, there *might not* be a real file there. We emphasize safety and correctness, just like Swift, and also (again like
Swift), we provide a thoughtful and comprehensive (yet concise) API.
# Support mxcl # Support mxcl
@@ -40,6 +49,12 @@ 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>
[Other donation/tipping options](http://mxcl.github.io/donate/)
# 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:
@@ -79,6 +94,18 @@ decoder.userInfo[.relativePath] = Path.home
decoder.decode(from: data) decoder.decode(from: data)
``` ```
## Dynamic members
We support `@dynamicMemberLookup`:
```swift
let ls = Path.root.usr.bin.ls // => /usr/bin/ls
```
This is less commonly useful than you would think, hence our documentation
does not use it. Usually you are joining variables or other `String` arguments.
However when you need it, its *lovely*.
## Initializing from user-input ## Initializing from user-input
The `Path` initializer returns `nil` unless fed an absolute path; thus to The `Path` initializer returns `nil` unless fed an absolute path; thus to
@@ -92,6 +119,9 @@ 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 to be consistent with the equivalent operation for
converting strings to `Int`, `Float` etc. in the standard library.
## Extensions ## Extensions
We have some extensions to Apple APIs: We have some extensions to Apple APIs:
@@ -134,18 +164,82 @@ let dirs = Path.home.ls().directories().filter {
let swiftFiles = Path.home.ls().files(withExtension: "swift") let swiftFiles = Path.home.ls().files(withExtension: "swift")
``` ```
# Installation # Rules & Caveats
SwiftPM only: Paths are just string representations, there *might not* be a real file there.
```swift ```swift
package.append(.package(url: "https://github.com/mxcl/Path.swift", from: "0.0.0")) 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/mxcl/foo
// this does not work though
Path("~foo") // => nil
``` ```
*Path.swift* has the general policty that if the desired end result preexists,
then its 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 dont check
for that as the check was deemed too expensive to be worthwhile.
# Installation
SwiftPM:
```swift
package.append(.package(url: "https://github.com/mxcl/Path.swift", from: "0.5.0"))
```
CocoaPods:
```ruby
pod 'Path.swift' ~> '0.5.0'
```
Carthage:
> Waiting on: [@Carthage#1945](https://github.com/Carthage/Carthage/pull/1945).
## Please note
We are pre 1.0, thus we can change the API as we like, and we will (to the
pursuit of getting it *right*)! We will tag 1.0 as soon as possible.
### 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-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 [badge-languages]: https://img.shields.io/badge/swift-4.2-orange.svg
[online API documentation]: https://mxcl.github.io/Path.swift/Structs/Path.html

View File

@@ -1,26 +1,33 @@
import Foundation import Foundation
/// Extensions on Foundations `Bundle` so you get `Path` rather than `String` or `URL`.
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)
} }
public var sharedFrameworks: Path? { /// Returns the path for the shared-frameworks directory in this bundle.
var sharedFrameworks: Path? {
return sharedFrameworksPath.flatMap(Path.init) return sharedFrameworksPath.flatMap(Path.init)
} }
public var resources: Path? { /// Returns the path for the resources directory in this bundle.
var resources: Path? {
return resourcePath.flatMap(Path.init) return resourcePath.flatMap(Path.init)
} }
public var path: Path { /// Returns the path for this bundle.
var path: Path {
return Path(string: bundlePath) return Path(string: bundlePath)
} }
} }
/// Extensions on `String` that work with `Path` rather than `String` or `URL`
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)
@@ -35,7 +42,9 @@ public extension String {
} }
} }
/// Extensions on `Data` that work with `Path` rather than `String` or `URL`
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)

View File

@@ -1,10 +1,52 @@
import Foundation import Foundation
public extension Path { public extension Path {
//MARK: Filesystem Attributes
/**
Returns the creation-time of the file.
- Note: Returns UNIX-time-zero if there is no creation-time, this should only happen if the file doesnt exist.
*/
var ctime: Date {
do {
let attrs = try FileManager.default.attributesOfItem(atPath: string)
return attrs[.creationDate] as? Date ?? Date(timeIntervalSince1970: 0)
} catch {
//TODO log error
return Date(timeIntervalSince1970: 0)
}
}
/**
Returns the modification-time of the file.
- Note: Returns the creation time if there is no modification time.
- Note: Returns UNIX-time-zero if neither are available, this should only happen if the file doesnt exist.
*/
var mtime: Date {
do {
let attrs = try FileManager.default.attributesOfItem(atPath: string)
return attrs[.modificationDate] as? Date ?? ctime
} 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
public func lock() throws -> Path { func lock() throws -> Path {
var attrs = try FileManager.default.attributesOfItem(atPath: string) var attrs = try FileManager.default.attributesOfItem(atPath: string)
let b = attrs[.immutable] as? Bool ?? false let b = attrs[.immutable] as? Bool ?? false
if !b { if !b {
@@ -17,7 +59,7 @@ public extension Path {
/// - Note: If file isnt locked, does nothing /// - Note: If file isnt locked, does nothing
/// - Note: If file doesnt exist, does nothing /// - Note: If file doesnt exist, does nothing
@discardableResult @discardableResult
public func unlock() throws -> Path { func unlock() throws -> Path {
var attrs: [FileAttributeKey: Any] var attrs: [FileAttributeKey: Any]
do { do {
attrs = try FileManager.default.attributesOfItem(atPath: string) attrs = try FileManager.default.attributesOfItem(atPath: string)
@@ -31,26 +73,4 @@ public extension Path {
} }
return self return self
} }
/**
Sets the files 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()
}
}
} }

View File

@@ -1,10 +1,31 @@
import Foundation import Foundation
/**
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 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](https://github.com/mxcl/Path.swift/#codable).
*/
extension Path: 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("/") {
@@ -17,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

@@ -0,0 +1,92 @@
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)
}
/// Returns a `Path` representing the root path.
public static var root: Path {
return Path(string: "/")
}
/// Returns a `Path` representing the users home directory
public static var home: Path {
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)
}
/// Helper to allow search path and domain mask to be passed in.
private static func path(for searchPath: FileManager.SearchPathDirectory) -> Path {
#if os(Linux)
// the urls(for:in:) function is not implemented on Linux
//TODO strictly we should first try to use the provided binary tool
let foo = { ProcessInfo.processInfo.environment[$0].flatMap(Path.init) ?? $1 }
switch searchPath {
case .documentDirectory:
return Path.home/"Documents"
case .applicationSupportDirectory:
return foo("XDG_DATA_HOME", Path.home/".local/share")
case .cachesDirectory:
return foo("XDG_CACHE_HOME", Path.home/".cache")
default:
fatalError()
}
#else
guard let pathString = FileManager.default.urls(for: searchPath, in: .userDomainMask).first?.path else {
switch searchPath {
case .documentDirectory:
return Path.home/"Documents"
case .applicationSupportDirectory:
return Path.home/"Library/Application Support"
case .cachesDirectory:
return Path.home/"Library/Caches"
default:
fatalError()
}
}
return Path(string: pathString)
#endif
}
/**
The root for user documents.
- Note: There is no standard location for documents on Linux, thus we return `~/Documents`.
- Note: You should create a subdirectory before creating any files.
*/
public static var documents: Path {
return path(for: .documentDirectory)
}
/**
The root for cache files.
- Note: On Linux this is `XDG_CACHE_HOME`.
- Note: You should create a subdirectory before creating any files.
*/
public static var caches: Path {
return path(for: .cachesDirectory)
}
/**
For data that supports your running application.
- Note: On Linux is `XDG_DATA_HOME`.
- Note: You should create a subdirectory before creating any files.
*/
public static var applicationSupport: Path {
return path(for: .applicationSupportDirectory)
}
}

View File

@@ -1,17 +1,28 @@
import Foundation import Foundation
public extension Path { public extension Path {
//MARK: File Management
/** /**
Copies a file. Copies a file.
try Path.root.join("bar").copy(to: Path.home/"foo")
// => "/Users/mxcl/foo"
- Note: `throws` if `to` is a directory. - Note: `throws` if `to` is a directory.
- Parameter to: Destination filename. - Parameter to: Destination filename.
- Parameter overwrite: If true overwrites any file that already exists at `to`. - 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.swifts* 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 - Returns: `to` to allow chaining
- SeeAlso: copy(into:overwrite:) - SeeAlso: `copy(into:overwrite:)`
*/ */
@discardableResult @discardableResult
public func copy(to: Path, overwrite: Bool = false) throws -> Path { func copy(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.exists { if overwrite, to.isFile, isFile {
try FileManager.default.removeItem(at: to.url) try FileManager.default.removeItem(at: to.url)
} }
try FileManager.default.copyItem(atPath: string, toPath: to.string) try FileManager.default.copyItem(atPath: string, toPath: to.string)
@@ -21,28 +32,64 @@ public extension Path {
/** /**
Copies a file into a directory Copies a file into a directory
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. If the destination does not exist, this function creates the directory first.
- Note: `throws` if `into` is a file.
- Parameter into: Destination directory - 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`.
- Returns: The `Path` of the newly copied file. - Returns: The `Path` of the newly copied file.
- SeeAlso: copy(into:overwrite:) - Note: `throws` if `into` is a file.
- Note: Throws if `overwrite` is `false` yet `to` is *already* identical to
`self` because even though *Path.swifts* 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 @discardableResult
public func copy(into: Path, overwrite: Bool = false) throws -> Path { func copy(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists { if !into.exists {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
} else if overwrite, !into.isDirectory {
try into.delete()
} }
let rv = into/basename() let rv = into/basename()
if overwrite, rv.isFile {
try rv.delete()
}
#if os(Linux)
#if swift(>=5.1)
// check if fixed
#else
if !overwrite, rv.isFile {
throw CocoaError.error(.fileWriteFileExists)
}
#endif
#endif
try FileManager.default.copyItem(at: url, to: rv.url) try FileManager.default.copyItem(at: url, to: rv.url)
return rv return rv
} }
/**
Moves a file.
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.swifts* 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 @discardableResult
public func move(to: Path, overwrite: Bool = false) throws -> Path { func move(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.exists { if overwrite, to.exists {
try FileManager.default.removeItem(at: to.url) try FileManager.default.removeItem(at: to.url)
} }
@@ -50,10 +97,24 @@ public extension Path {
return to return to
} }
/**
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.
- Parameter into: Destination directory
- 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 @discardableResult
public func move(into: Path) throws -> Path { func move(into: Path) throws -> Path {
if !into.exists { if !into.exists {
try into.mkpath() try into.mkdir(.p)
} else if !into.isDirectory { } else if !into.isDirectory {
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
@@ -62,71 +123,58 @@ public extension Path {
return rv return rv
} }
/**
Deletes the path, recursively if a directory.
- Note: noop: if the path doesnt exist
*Path.swift* doesnt error if desired end result preexists.
*/
@inlinable @inlinable
public func delete() throws { func delete() throws {
try FileManager.default.removeItem(at: url) if exists {
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)
} }
private func _foo(go: () throws -> Void) throws { /**
#if !os(Linux) Creates the directory at this path.
- 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 { do {
try go() let wid = options == .p
try FileManager.default.createDirectory(at: self.url, withIntermediateDirectories: wid, attributes: nil)
} catch CocoaError.Code.fileWriteFileExists { } catch CocoaError.Code.fileWriteFileExists {
// noop //noop (fails to trigger on Linux)
}
#else
do {
try go()
} catch { } catch {
#if os(Linux)
let error = error as NSError let error = error as NSError
guard error.domain == NSCocoaErrorDomain, error.code == CocoaError.Code.fileWriteFileExists.rawValue else { guard error.domain == NSCocoaErrorDomain, error.code == CocoaError.Code.fileWriteFileExists.rawValue else {
throw error throw error
} }
#else
throw error
#endif
} }
#endif
}
@discardableResult
public func mkdir() throws -> Path {
try _foo {
try FileManager.default.createDirectory(at: self.url, withIntermediateDirectories: false, attributes: nil)
}
return self
}
@discardableResult
public func mkpath() throws -> Path {
try _foo {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
}
return self
}
/// - Note: If file doesnt 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 return self
} }
} }
/// Options for `Path.mkdir(_:)`
public enum MakeDirectoryOptions {
/// Creates intermediary directories; works the same as `mkdir -p`.
case p
}

View File

@@ -1,20 +1,14 @@
import class Foundation.NSString 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 { 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

@@ -1,24 +1,57 @@
import Foundation import Foundation
/**
A file entry from a directory listing.
- SeeAlso: `ls()`
*/
public struct Entry {
/// The kind of this directory entry.
public enum Kind {
/// The path is a file.
case file
/// The path is a directory.
case directory
}
/// The kind of this entry.
public let kind: Kind
/// The path of this entry.
public let path: Path
}
public extension Path { public extension Path {
/// same as the `ls` command is shallow //MARK: Directory Listings
func ls() throws -> [Entry] {
let relativePaths = try FileManager.default.contentsOfDirectory(atPath: string) /**
func convert(relativePath: String) -> Entry { Same as the `ls -a` command output is shallow and unsorted.
let path = self/relativePath - Parameter includeHiddenFiles: If `true`, hidden files are included in the results. Defaults to `true`.
- Important: `includeHiddenFiles` does not work on Linux
*/
func ls(includeHiddenFiles: Bool = true) throws -> [Entry] {
var opts = FileManager.DirectoryEnumerationOptions()
#if !os(Linux)
if !includeHiddenFiles {
opts.insert(.skipsHiddenFiles)
}
#endif
let paths = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: opts)
func convert(url: URL) -> Entry? {
guard let path = Path(url.path) else { return nil }
return Entry(kind: path.isDirectory ? .directory : .file, path: path) return Entry(kind: path.isDirectory ? .directory : .file, path: path)
} }
return relativePaths.map(convert) return paths.compactMap(convert)
} }
} }
public extension Array where Element == Path.Entry { /// Convenience functions for the array return value of `Path.ls()`
public extension Array where Element == 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,25 +1,42 @@
import Foundation import Foundation
public extension Path { public extension Path {
var isWritable: Bool { //MARK: Filesystem Properties
return FileManager.default.isWritableFile(atPath: string)
} /// Returns true if the path represents an actual filesystem entry.
var exists: Bool {
var isDirectory: Bool { return FileManager.default.fileExists(atPath: string)
var isDir: ObjCBool = false
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 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 { var isExecutable: Bool {
return FileManager.default.isExecutableFile(atPath: string) return FileManager.default.isExecutableFile(atPath: string)
} }
var exists: Bool {
return FileManager.default.fileExists(atPath: string)
}
} }

View File

@@ -1,50 +1,124 @@
import Foundation import Foundation
/**
Represents a filesystem absolute path.
`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.
All functions on `Path` are chainable and short to facilitate doing sequences
of file operations in a concise manner.
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. The underlying
representation for `Path` is `String`.
*/
@dynamicMemberLookup
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
public let string: String public let string: String
public static var cwd: Path { /// Returns a `URL` representing this file path.
return Path(string: FileManager.default.currentDirectoryPath) public var url: URL {
return URL(fileURLWithPath: string)
} }
public static var root: Path { /// Facilitates constructing paths for static strings
return Path(string: "/") public subscript(dynamicMember pathComponent: String) -> Path {
let str = (string as NSString).appendingPathComponent(pathComponent)
return Path(string: str)
} }
public static var home: Path { /**
return Path(string: NSHomeDirectory()) 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 {
return Path(string: (string as NSString).deletingLastPathComponent)
} }
/**
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` //MARK: Pathing
public var parent: Path {
return Path(string: (string as NSString).deletingLastPathComponent) /**
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)
} }
/**
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: `join(_:)`
*/
@inlinable @inlinable
public var url: URL { public static func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol {
return URL(fileURLWithPath: string) return lhs.join(rhs)
} }
public func basename(dropExtension: Bool = false) -> String { /**
let str = string as NSString Returns a string representing the relative path to `base`.
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 `..` - 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,28 +149,33 @@ public struct Path: Equatable, Hashable, Comparable {
} }
} }
public func join<S>(_ part: S) -> Path where S: StringProtocol { /**
//TODO standardizingPath does more than we want really (eg tilde expansion) The basename for the provided file, optionally dropping the file extension.
let str = (string as NSString).appendingPathComponent(String(part))
return Path(string: (str as NSString).standardizingPath) 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 {
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 @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
} }
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)
} }

View File

@@ -14,9 +14,10 @@ class PathTests: XCTestCase {
func testEnumeration() throws { func testEnumeration() throws {
let tmpdir_ = try TemporaryDirectory() let tmpdir_ = try TemporaryDirectory()
let tmpdir = tmpdir_.path let tmpdir = tmpdir_.path
try tmpdir.join("a").mkdir().join("c").touch() try tmpdir.a.mkdir().c.touch()
try tmpdir.join("b").touch() try tmpdir.b.touch()
try tmpdir.join("c").touch() try tmpdir.c.touch()
try tmpdir.join(".d").mkdir().e.touch()
var paths = Set<String>() var paths = Set<String>()
var dirs = 0 var dirs = 0
@@ -26,8 +27,31 @@ class PathTests: XCTestCase {
} }
paths.insert(entry.path.basename()) paths.insert(entry.path.basename())
} }
XCTAssertEqual(dirs, 2)
XCTAssertEqual(paths, ["a", "b", "c", ".d"])
}
func testEnumerationSkippingHiddenFiles() throws {
#if !os(Linux)
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()
try tmpdir.join(".d").mkdir().join("e").touch()
var paths = Set<String>()
var dirs = 0
for entry in try tmpdir.ls(includeHiddenFiles: false) {
if entry.kind == .directory {
dirs += 1
}
paths.insert(entry.path.basename())
}
XCTAssertEqual(dirs, 1) XCTAssertEqual(dirs, 1)
XCTAssertEqual(paths, ["a", "b", "c"]) XCTAssertEqual(paths, ["a", "b", "c"])
#endif
} }
func testRelativeTo() { func testRelativeTo() {
@@ -59,7 +83,7 @@ class PathTests: XCTestCase {
try Path.mktemp { try Path.mktemp {
for _ in 0...1 { for _ in 0...1 {
try $0.join("a").mkdir() try $0.join("a").mkdir()
try $0.join("b/c").mkpath() try $0.join("b/c").mkdir(.p)
} }
} }
} }
@@ -93,4 +117,43 @@ 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"))
XCTAssertEqual(Path.root/"a/foo"/"../bar", Path.root/"a/bar")
XCTAssertEqual(Path.root/"a/foo"/"/../bar", Path.root/"a/bar")
XCTAssertEqual(Path.root/"a/foo"/"../../bar", Path.root/"bar")
XCTAssertEqual(Path.root/"a/foo"/"../../../bar", Path.root/"bar")
}
func testDynamicMember() {
XCTAssertEqual(Path.root.Documents, Path.root/"Documents")
let a = Path.home.foo
XCTAssertEqual(a.Documents, Path.home/"foo/Documents")
}
func testCopyInto() throws {
try Path.mktemp { root in
let bar = try root.join("bar").touch()
try Path.mktemp { root in
try root.join("bar").touch()
XCTAssertThrowsError(try bar.copy(into: root))
try bar.copy(into: root, overwrite: true)
}
}
}
} }

View File

@@ -1,8 +1,9 @@
@testable import Path
import Foundation import Foundation
public class TemporaryDirectory { class TemporaryDirectory {
public let url: URL let url: URL
public var path: Path { return Path(string: url.path) } var path: Path { return Path(string: url.path) }
/** /**
Creates a new temporary directory. Creates a new temporary directory.
@@ -19,7 +20,7 @@ public class TemporaryDirectory {
- Parameter appropriateFor: The temporary directory will be located on this - Parameter appropriateFor: The temporary directory will be located on this
volume. volume.
*/ */
public init(appropriateFor: URL? = nil) throws { init(appropriateFor: URL? = nil) throws {
#if !os(Linux) #if !os(Linux)
let appropriate: URL let appropriate: URL
if let appropriateFor = appropriateFor { if let appropriateFor = appropriateFor {
@@ -45,9 +46,26 @@ public class TemporaryDirectory {
} }
} }
public extension Path { extension Path {
static func mktemp<T>(body: (Path) throws -> T) throws -> T { static func mktemp<T>(body: (Path) throws -> T) throws -> T {
let tmp = try TemporaryDirectory() let tmp = try TemporaryDirectory()
return try body(tmp.path) return try body(tmp.path)
} }
} }
#if !os(macOS) && !os(Linux)
import XCTest
// SwiftPM generates code that is improperly escaped thus we require this to
// compile on iOS & tvOS.
public typealias XCTestCaseEntry = (testCaseClass: XCTestCase.Type, allTests: [(String, (XCTestCase) throws -> Void)])
public func testCase<T: XCTestCase>(_ allTests: [(String, (T) -> () throws -> Void)]) -> XCTestCaseEntry {
fatalError()
}
public func testCase<T: XCTestCase>(_ allTests: [(String, (T) -> () -> Void)]) -> XCTestCaseEntry {
fatalError()
}
#endif

View File

@@ -5,9 +5,13 @@ extension PathTests {
("testBasename", testBasename), ("testBasename", testBasename),
("testCodable", testCodable), ("testCodable", testCodable),
("testConcatenation", testConcatenation), ("testConcatenation", testConcatenation),
("testCopyInto", testCopyInto),
("testDynamicMember", testDynamicMember),
("testEnumeration", testEnumeration), ("testEnumeration", testEnumeration),
("testEnumerationSkippingHiddenFiles", testEnumerationSkippingHiddenFiles),
("testExists", testExists), ("testExists", testExists),
("testIsDirectory", testIsDirectory), ("testIsDirectory", testIsDirectory),
("testJoin", testJoin),
("testMkpathIfExists", testMkpathIfExists), ("testMkpathIfExists", testMkpathIfExists),
("testMktemp", testMktemp), ("testMktemp", testMktemp),
("testRelativePathCodable", testRelativePathCodable), ("testRelativePathCodable", testRelativePathCodable),
@@ -15,7 +19,7 @@ extension PathTests {
] ]
} }
#if os(Linux) #if !os(macOS)
public func __allTests() -> [XCTestCaseEntry] { public func __allTests() -> [XCTestCaseEntry] {
return [ return [
testCase(PathTests.__allTests), testCase(PathTests.__allTests),