Compare commits

..

26 Commits

Author SHA1 Message Date
repo-ranger[bot]
ab9a70e947 Merge pull request #25 from mxcl/rename
Refactor rename -> rename(to:)
2019-01-31 15:32:59 +00:00
Max Howell
49ef073e34 Refactor rename -> rename(to:) 2019-01-31 10:15:39 -05:00
Max Howell
889d825b3a Merge pull request #24 from mxcl/rename
Rename
2019-01-31 10:08:02 -05:00
Max Howell
f1cd06fdff Add CI post success hook yamls 2019-01-31 09:58:46 -05:00
Max Howell
c6e840b9b6 Add rename 2019-01-31 08:39:54 -05:00
Max Howell
eb34ac4af8 Add overwrite parameter to move(into:) 2019-01-31 08:37:32 -05:00
Max Howell
66ae86c986 Enable codecov.io 2019-01-31 08:37:14 -05:00
Max Howell
c432f710eb Merge pull request #22 from mxcl/files()
Entry.files defaults to all files
2019-01-28 12:17:08 -05:00
Max Howell
19c0c19bb6 Entry.files defaults to all files 2019-01-28 12:04:47 -05:00
Max Howell
ee1f46954c Fixes #20
[skip ci]
2019-01-28 11:05:51 -05:00
Max Howell
2394cc1c85 Merge pull request #19 from JaapWijnen/filehandle-extensions
added extension to initialize filehandle from path
2019-01-27 16:59:44 -05:00
Jaap Wijnen
50bb319619 added extension to initialize filehandle from path 2019-01-27 22:57:19 +01:00
Max Howell
9f40068833 Badge for Swift 5 support 2019-01-26 16:15:45 -05:00
Max Howell
67f4e5f41a Merge pull request #16 from mxcl/bundle-non-optional
Bundle extensions don’t return optional Paths
2019-01-26 15:46:03 -05:00
Max Howell
83c83dcaba Bundle extensions don’t return optional Paths
Rationale: Paths are not guaranteed to exist, the Bundle functions return optional if the path doesn't exist. So we'll provide a sensible default instead and you need to check the result exists at some point instead.

This makes more elegant chains, the chain will fail when you operate on it, but you don’t have to do a check for optional first. Or risk a bang.
2019-01-26 15:20:32 -05:00
Max Howell
93e2701950 Docs tweaks
[ci skip]
2019-01-26 15:10:52 -05:00
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
13 changed files with 340 additions and 119 deletions

2
.github/codecov.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
ignore:
- Tests

3
.github/ranger.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
merges:
- action: delete_branch
- action: tag

View File

@@ -19,9 +19,10 @@ jobs:
name: macOS / Swift 4.2.1 name: macOS / Swift 4.2.1
- &xcodebuild - &xcodebuild
before_install: swift package generate-xcodeproj before_install: swift package generate-xcodeproj --enable-code-coverage
xcode_destination: platform=iOS Simulator,OS=latest,name=iPhone XS xcode_destination: platform=iOS Simulator,OS=latest,name=iPhone XS
name: iOS / Swift 4.2.1 name: iOS / Swift 4.2.1
after_success: bash <(curl -s https://codecov.io/bash)
- <<: *xcodebuild - <<: *xcodebuild
xcode_destination: platform=tvOS Simulator,OS=latest,name=Apple TV xcode_destination: platform=tvOS Simulator,OS=latest,name=Apple TV
name: tvOS / Swift 4.2.1 name: tvOS / Swift 4.2.1
@@ -34,6 +35,7 @@ jobs:
-scheme Path.swift-Package \ -scheme Path.swift-Package \
-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
after_success: false
- &linux - &linux
env: SWIFT_VERSION=4.2.1 env: SWIFT_VERSION=4.2.1
@@ -89,11 +91,28 @@ jobs:
- name: CocoaPods - name: CocoaPods
before_install: | 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 cat <<\ \ EOF> Path.swift.podspec
Pod::Spec.new do |s| Pod::Spec.new do |s|
s.name = 'Path.swift' s.name = 'Path.swift'
s.version = 'TRAVIS_TAG' s.version = ENV['TRAVIS_TAG']
s.summary = 'Delightful, robust file-pathing functions' s.summary = ENV['DESCRIPTION']
s.homepage = 'https://github.com/mxcl/Path.swift' s.homepage = 'https://github.com/mxcl/Path.swift'
s.license = { :type => 'Unlicense', :file => 'LICENSE.md' } s.license = { :type => 'Unlicense', :file => 'LICENSE.md' }
s.author = { 'mxcl' => 'mxcl@me.com' } s.author = { 'mxcl' => 'mxcl@me.com' }
@@ -107,7 +126,5 @@ jobs:
s.swift_version = '4.2' s.swift_version = '4.2'
end end
EOF EOF
sed -i '' "s/TRAVIS_TAG/$TRAVIS_TAG/" Path.swift.podspec
# ^^ see the Jazzy deployment for explanation
install: gem install cocoapods --pre install: gem install cocoapods --pre
script: pod trunk push script: pod trunk push

View File

@@ -1,7 +1,7 @@
# Path.swift ![badge-platforms] ![badge-languages] [![Build Status](https://travis-ci.com/mxcl/Path.swift.svg)](https://travis-ci.com/mxcl/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 end
endresults. results.
```swift ```swift
import Path import Path
@@ -28,12 +28,15 @@ print(bar) // => /bar
print(bar.isFile) // => true print(bar.isFile) // => true
// careful API considerations so as to avoid common bugs // careful API considerations so as to avoid common bugs
let foo = 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())
print(foo) // => /bar/foo print(foo) // => /bar/foo
print(foo.isFile) // => true print(foo.isFile) // => true
// A practical example: installing a helper executable // we support dynamic members (_use_sparingly_):
try Bundle.resources.join("helper").copy(into: Path.home.join(".local/bin").mkpath()).chmod(0o500) let prefs = Path.home.Library.Preferences // => /Users/mxcl/Library/Preferences
// a practical example: installing a helper executable
try Bundle.resources.join("helper").copy(into: Path.home.join(".local/bin").mkdir(.p)).chmod(0o500)
``` ```
We emphasize safety and correctness, just like Swift, and also (again like We emphasize safety and correctness, just like Swift, and also (again like
@@ -42,8 +45,9 @@ Swift), we provide a thoughtful and comprehensive (yet concise) API.
# Support mxcl # Support mxcl
Hi, Im Max Howell and I have written a lot of open source software, and Hi, Im 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 probably you already use some of it (Homebrew anyone?). I work full-time on
can continue to make tools and software you need and love. I appreciate it x. open source and its hard; currently I earn *less* than minimum wage. Please
help me continue my work, I appreciate it x
<a href="https://www.patreon.com/mxcl"> <a href="https://www.patreon.com/mxcl">
<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">
@@ -94,6 +98,19 @@ 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
or trying to describe files (and files usually have extensions). 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
@@ -122,8 +139,7 @@ bashProfile += "\n\nfoo"
try bashProfile.write(to: Path.home/".bash_profile") try bashProfile.write(to: Path.home/".bash_profile")
try Bundle.main.resources!.join("foo").copy(to: .home) try Bundle.main.resources.join("foo").copy(to: .home)
// ^^ `-> Path?` because the underlying `Bundle` function is `-> String?`
``` ```
## Directory listings ## Directory listings
@@ -145,9 +161,9 @@ for entry in Path.home.ls() where entry.path.mtime > yesterday {
// //
} }
let dirs = Path.home.ls().directories().filter { let dirs = Path.home.ls().directories
//
} let files = Path.home.ls().files
let swiftFiles = Path.home.ls().files(withExtension: "swift") let swiftFiles = Path.home.ls().files(withExtension: "swift")
``` ```
@@ -184,6 +200,16 @@ Path("~/foo")! // => /Users/mxcl/foo
Path("~foo") // => nil 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 # Installation
SwiftPM: SwiftPM:
@@ -195,7 +221,7 @@ package.append(.package(url: "https://github.com/mxcl/Path.swift", from: "0.5.0"
CocoaPods: CocoaPods:
```ruby ```ruby
pod 'Path.swift' ~> '0.5.0' pod 'Path.swift', '~> 0.5'
``` ```
Carthage: Carthage:
@@ -219,5 +245,5 @@ https://codebasesaga.com/canopy/
[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%20%7C%205.0-orange.svg
[online API documentation]: https://mxcl.github.io/Path.swift/Structs/Path.html [online API documentation]: https://mxcl.github.io/Path.swift/Structs/Path.html

View File

@@ -10,13 +10,31 @@ public extension Bundle {
} }
/// Returns the path for the shared-frameworks directory in this bundle. /// Returns the path for the shared-frameworks directory in this bundle.
var sharedFrameworks: Path? { var sharedFrameworks: Path {
return sharedFrameworksPath.flatMap(Path.init) var `default`: Path {
#if os(macOS)
return path.join("Contents/Frameworks")
#elseif os(Linux)
return path.join("lib")
#else
return path.join("Frameworks")
#endif
}
return sharedFrameworksPath.flatMap(Path.init) ?? `default`
} }
/// Returns the path for the resources directory in this bundle. /// Returns the path for the resources directory in this bundle.
var resources: Path? { var resources: Path {
return resourcePath.flatMap(Path.init) var `default`: Path {
#if os(macOS)
return path.join("Contents/Resources")
#elseif os(Linux)
return path.join("share")
#else
return path
#endif
}
return resourcePath.flatMap(Path.init) ?? `default`
} }
/// Returns the path for this bundle. /// Returns the path for this bundle.
@@ -68,3 +86,24 @@ public extension Data {
return to return to
} }
} }
/// Extensions on `FileHandle` that work with `Path` rather than `String` or `URL`
public extension FileHandle {
/// Initializes this `FileHandle` for reading at the location of the provided path.
@inlinable
convenience init(forReadingAt path: Path) throws {
try self.init(forReadingFrom: path.url)
}
/// Initializes this `FileHandle` for writing at the location of the provided path.
@inlinable
convenience init(forWritingAt path: Path) throws {
try self.init(forWritingTo: path.url)
}
/// Initializes this `FileHandle` for reading and writing at the location of the provided path.
@inlinable
convenience init(forUpdatingAt path: Path) throws {
try self.init(forUpdating: path.url)
}
}

View File

@@ -4,14 +4,28 @@ public extension Path {
//MARK: Filesystem Attributes //MARK: Filesystem Attributes
/** /**
Returns the modification-time. 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 the creation time if there is no modification time.
- Note: Returns UNIX-time-zero if neither are available, though this *should* be impossible. - Note: Returns UNIX-time-zero if neither are available, this should only happen if the file doesnt exist.
*/ */
var mtime: Date { 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(timeIntervalSince1970: 0) return attrs[.modificationDate] as? Date ?? ctime
} catch { } catch {
//TODO log error //TODO log error
return Date(timeIntervalSince1970: 0) return Date(timeIntervalSince1970: 0)

View File

@@ -25,7 +25,7 @@ public extension CodingUserInfoKey {
*/ */
extension Path: Codable { extension Path: Codable {
/// - SeeAlso: `CodingUserInfoKey.relativePath` /// - SeeAlso: `CodingUserInfoKey.relativePath`
// :nodoc: /// :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("/") {
@@ -39,7 +39,7 @@ extension Path: Codable {
} }
/// - SeeAlso: `CodingUserInfoKey.relativePath` /// - SeeAlso: `CodingUserInfoKey.relativePath`
// :nodoc: /// :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

@@ -3,7 +3,7 @@ import Foundation
extension Path { extension Path {
//MARK: Common Directories //MARK: Common Directories
/// Returns a `Path` containing ``FileManager.default.currentDirectoryPath`. /// Returns a `Path` containing `FileManager.default.currentDirectoryPath`.
public static var cwd: Path { public static var cwd: Path {
return Path(string: FileManager.default.currentDirectoryPath) return Path(string: FileManager.default.currentDirectoryPath)
} }
@@ -74,7 +74,7 @@ extension Path {
/** /**
The root for cache files. The root for cache files.
- Note: On Linux this is 'XDG_CACHE_HOME'. - Note: On Linux this is `XDG_CACHE_HOME`.
- Note: You should create a subdirectory before creating any files. - Note: You should create a subdirectory before creating any files.
*/ */
public static var caches: Path { public static var caches: Path {

View File

@@ -5,10 +5,18 @@ public extension Path {
/** /**
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` and both `self` and `to` are files, overwrites `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: 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:)`
*/ */
@@ -24,15 +32,22 @@ 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.
// Create ~/.local/bin, copy `ls` there and make the new copy executable
try Path.root.join("bin/ls").copy(into: Path.home.join(".local/bin").mkpath()).chmod(0o500)
- 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.
- 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:)` - SeeAlso: `copy(into:overwrite:)`
*/ */
@discardableResult @discardableResult
@@ -59,15 +74,23 @@ public extension Path {
/** /**
Moves a file. Moves a file.
- Note: `throws` if `to` is a directory.
try Path.root.join("bar").move(to: Path.home/"foo")
// => "/Users/mxcl/foo"
- Parameter to: Destination filename. - Parameter to: Destination filename.
- Parameter overwrite: If true overwrites any file that already exists at `to`. - Parameter overwrite: If true overwrites any file that already exists at `to`.
- Returns: `to` to allow chaining - 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:) - SeeAlso: move(into:overwrite:)
*/ */
@discardableResult @discardableResult
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.isFile {
try FileManager.default.removeItem(at: to.url) try FileManager.default.removeItem(at: to.url)
} }
try FileManager.default.moveItem(at: url, to: to.url) try FileManager.default.moveItem(at: url, to: to.url)
@@ -77,30 +100,42 @@ public extension Path {
/** /**
Moves a file into a directory 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. 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`.
- Note: `throws` if `into` is a file.
- Returns: The `Path` of destination filename. - Returns: The `Path` of destination filename.
- SeeAlso: move(into:overwrite:) - SeeAlso: move(into:overwrite:)
*/ */
@discardableResult @discardableResult
func move(into: Path) throws -> Path { func move(into: Path, overwrite: Bool = false) 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)
} }
let rv = into/basename() let rv = into/basename()
if overwrite, rv.isFile {
try FileManager.default.removeItem(at: rv.url)
}
try FileManager.default.moveItem(at: url, to: rv.url) try FileManager.default.moveItem(at: url, to: rv.url)
return rv return rv
} }
/// Deletes the path, recursively if a directory. /**
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
func delete() throws { func delete() throws {
try FileManager.default.removeItem(at: url) if exists {
try FileManager.default.removeItem(at: url)
}
} }
/** /**
@@ -113,49 +148,51 @@ public extension Path {
return try "".write(to: self) return try "".write(to: self)
} }
/// Helper due to Linux Swift being incomplete. /**
private func _foo(go: () throws -> Void) throws { Creates the directory at this path.
#if !os(Linux) - 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
#endif throw error
} #endif
/**
Creates the directory at this path.
- Note: Does not create any intermediary directories.
- Returns: `self` to allow chaining.
*/
@discardableResult
func mkdir() throws -> Path {
try _foo {
try FileManager.default.createDirectory(at: self.url, withIntermediateDirectories: false, attributes: nil)
} }
return self return self
} }
/** /**
Creates the directory at this path. Renames the file at path.
- Note: Creates any intermediary directories, if required.
- Returns: `self` to allow chaining. Path.root.foo.bar.rename(to: "baz") // => /foo/baz
- Parameter to: the new basename for the file
- Returns: The renamed path.
*/ */
@discardableResult @discardableResult
func mkpath() throws -> Path { func rename(to newname: String) throws -> Path {
try _foo { let newpath = parent/newname
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) try FileManager.default.moveItem(atPath: string, toPath: newpath.string)
} return newpath
return self
} }
} }
/// Options for `Path.mkdir(_:)`
public enum MakeDirectoryOptions {
/// Creates intermediary directories; works the same as `mkdir -p`.
case p
}

View File

@@ -1,5 +1,23 @@
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 {
//MARK: Directory Listings //MARK: Directory Listings
@@ -25,7 +43,7 @@ public extension Path {
} }
/// Convenience functions for the array return value of `Path.ls()` /// Convenience functions for the array return value of `Path.ls()`
public extension Array where Element == Path.Entry { public extension Array where Element == Entry {
/// Filters the list of entries to be a list of Paths that are directories. /// Filters the list of entries to be a list of Paths that are directories.
var directories: [Path] { var directories: [Path] {
return compactMap { return compactMap {
@@ -33,7 +51,14 @@ public extension Array where Element == Path.Entry {
} }
} }
/// Filters the list of entries to be a list of Paths that are files with the specified extension /// Filters the list of entries to be a list of Paths that are files.
var files: [Path] {
return compactMap {
$0.kind == .file ? $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,14 +1,16 @@
import Foundation import Foundation
/** /**
Represents a platform filesystem absolute path. A `Path` represents an absolute path on a filesystem.
All functions on `Path` are chainable and short to facilitate doing sequences
of file operations in a concise manner.
`Path` supports `Codable`, and can be configured to `Path` supports `Codable`, and can be configured to
[encode paths *relatively*](https://github.com/mxcl/Path.swift/#codable). [encode paths *relatively*](https://github.com/mxcl/Path.swift/#codable).
Sorting a `Sequence` of `Path`s will return the locale-aware sort order, which Sorting a `Sequence` of paths will return the locale-aware sort order, which
will give you the same order as Finder, (though folders will not be sorted will give you the same order as Finder.
first).
Converting from a `String` is a common first step, here are the recommended Converting from a `String` is a common first step, here are the recommended
ways to do that: ways to do that:
@@ -18,21 +20,33 @@ import Foundation
let p3 = Path.cwd/relativePathString let p3 = Path.cwd/relativePathString
let p4 = Path(userInput) ?? Path.cwd/userInput let p4 = Path(userInput) ?? Path.cwd/userInput
- Note: There may not be an actual filesystem entry at the path. The underlying If you are constructing paths from static-strings we provide support for
representation for `Path` is `String`. dynamic members:
let p1 = Path.root.usr.bin.ls // => /usr/bin/ls
- Note: A `Path` does not necessarily represent an actual filesystem entry.
*/ */
@dynamicMemberLookup
public struct Path: Equatable, Hashable, Comparable { public struct Path: Equatable, Hashable, Comparable {
init(string: String) { init(string: String) {
self.string = string self.string = string
} }
/// Returns `nil` unless fed an absolute path /// Returns `nil` unless fed an absolute path.
public init?(_ description: String) { public init?(_ description: String) {
guard description.starts(with: "/") || description.starts(with: "~/") else { return nil } guard description.starts(with: "/") || description.starts(with: "~/") else { return nil }
self.init(string: (description as NSString).standardizingPath) self.init(string: (description as NSString).standardizingPath)
} }
/// :nodoc:
public subscript(dynamicMember pathComponent: String) -> Path {
let str = (string as NSString).appendingPathComponent(pathComponent)
return Path(string: str)
}
//MARK: Properties //MARK: Properties
/// The underlying filesystem path /// The underlying filesystem path
@@ -58,10 +72,15 @@ public struct Path: Equatable, Hashable, Comparable {
/** /**
Returns the filename extension of this path. Returns the filename extension of this path.
- Remark: Implemented via `NSString.pathExtension`. - Remark: Implemented via `NSString.pathExtension`.
- Note: We special case eg. `foo.tar.gz`.
*/ */
@inlinable @inlinable
public var `extension`: String { public var `extension`: String {
return (string as NSString).pathExtension if string.hasSuffix(".tar.gz") {
return "tar.gz"
} else {
return (string as NSString).pathExtension
}
} }
//MARK: Pathing //MARK: Pathing
@@ -76,7 +95,7 @@ public struct Path: Equatable, Hashable, Comparable {
- Parameter pathComponent: The string to join with this path. - Parameter pathComponent: The string to join with this path.
- Returns: A new joined path. - Returns: A new joined path.
- SeeAlso: `Path./(_:, _:)` - SeeAlso: `Path./(_:_:)`
*/ */
public func join<S>(_ pathComponent: S) -> Path where S: StringProtocol { 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)
@@ -168,24 +187,4 @@ public struct Path: Equatable, Hashable, Comparable {
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
} }
//MARK: Entry
/**
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
}
} }

View File

@@ -14,21 +14,27 @@ 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.join("b.swift").touch()
try tmpdir.join("c").touch() try tmpdir.c.touch()
try tmpdir.join(".d").mkdir().join("e").touch() try tmpdir.join(".d").mkdir().e.touch()
var paths = Set<String>() var paths = Set<String>()
let lsrv = try tmpdir.ls()
var dirs = 0 var dirs = 0
for entry in try tmpdir.ls() { for entry in lsrv {
if entry.kind == .directory { if entry.kind == .directory {
dirs += 1 dirs += 1
} }
paths.insert(entry.path.basename()) paths.insert(entry.path.basename())
} }
XCTAssertEqual(dirs, 2) XCTAssertEqual(dirs, 2)
XCTAssertEqual(paths, ["a", "b", "c", ".d"]) XCTAssertEqual(dirs, lsrv.directories.count)
XCTAssertEqual(["a", ".d"], Set(lsrv.directories.map{ $0.relative(to: tmpdir) }))
XCTAssertEqual(["b.swift", "c"], Set(lsrv.files.map{ $0.relative(to: tmpdir) }))
XCTAssertEqual(["b.swift"], Set(lsrv.files(withExtension: "swift").map{ $0.relative(to: tmpdir) }))
XCTAssertEqual(["c"], Set(lsrv.files(withExtension: "").map{ $0.relative(to: tmpdir) }))
XCTAssertEqual(paths, ["a", "b.swift", "c", ".d"])
} }
@@ -69,6 +75,18 @@ class PathTests: XCTestCase {
XCTAssert((Path.root/"bin").isDirectory) XCTAssert((Path.root/"bin").isDirectory)
} }
func testExtension() {
XCTAssertEqual(Path.root.join("a.swift").extension, "swift")
XCTAssertEqual(Path.root.join("a").extension, "")
XCTAssertEqual(Path.root.join("a.").extension, "")
XCTAssertEqual(Path.root.join("a..").extension, "")
XCTAssertEqual(Path.root.join("a..swift").extension, "swift")
XCTAssertEqual(Path.root.join("a..swift.").extension, "")
XCTAssertEqual(Path.root.join("a.tar.gz").extension, "tar.gz")
XCTAssertEqual(Path.root.join("a..tar.gz").extension, "tar.gz")
XCTAssertEqual(Path.root.join("a..tar..gz").extension, "gz")
}
func testMktemp() throws { func testMktemp() throws {
var path: Path! var path: Path!
try Path.mktemp { try Path.mktemp {
@@ -83,7 +101,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)
} }
} }
} }
@@ -139,13 +157,50 @@ class PathTests: XCTestCase {
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 { func testCopyInto() throws {
try Path.mktemp { root1 in
let bar1 = try root1.join("bar").touch()
try Path.mktemp { root2 in
let bar2 = try root2.join("bar").touch()
XCTAssertThrowsError(try bar1.copy(into: root2))
try bar1.copy(into: root2, overwrite: true)
XCTAssertTrue(bar1.exists)
XCTAssertTrue(bar2.exists)
}
}
}
func testMoveInto() throws {
try Path.mktemp { root1 in
let bar1 = try root1.join("bar").touch()
try Path.mktemp { root2 in
let bar2 = try root2.join("bar").touch()
XCTAssertThrowsError(try bar1.move(into: root2))
try bar1.move(into: root2, overwrite: true)
XCTAssertFalse(bar1.exists)
XCTAssertTrue(bar2.exists)
}
}
}
func testRename() throws {
try Path.mktemp { root in try Path.mktemp { root in
let bar = try root.join("bar").touch() do {
try Path.mktemp { root in let file = try root.bar.touch()
try root.join("bar").touch() let foo = try file.rename(to: "foo")
XCTAssertThrowsError(try bar.copy(into: root)) XCTAssertFalse(file.exists)
try bar.copy(into: root, overwrite: true) XCTAssertTrue(foo.isFile)
}
do {
let file = try root.bar.touch()
XCTAssertThrowsError(try file.rename(to: "foo"))
} }
} }
} }

View File

@@ -6,15 +6,19 @@ extension PathTests {
("testCodable", testCodable), ("testCodable", testCodable),
("testConcatenation", testConcatenation), ("testConcatenation", testConcatenation),
("testCopyInto", testCopyInto), ("testCopyInto", testCopyInto),
("testDynamicMember", testDynamicMember),
("testEnumeration", testEnumeration), ("testEnumeration", testEnumeration),
("testEnumerationSkippingHiddenFiles", testEnumerationSkippingHiddenFiles), ("testEnumerationSkippingHiddenFiles", testEnumerationSkippingHiddenFiles),
("testExists", testExists), ("testExists", testExists),
("testExtension", testExtension),
("testIsDirectory", testIsDirectory), ("testIsDirectory", testIsDirectory),
("testJoin", testJoin), ("testJoin", testJoin),
("testMkpathIfExists", testMkpathIfExists), ("testMkpathIfExists", testMkpathIfExists),
("testMktemp", testMktemp), ("testMktemp", testMktemp),
("testMoveInto", testMoveInto),
("testRelativePathCodable", testRelativePathCodable), ("testRelativePathCodable", testRelativePathCodable),
("testRelativeTo", testRelativeTo), ("testRelativeTo", testRelativeTo),
("testRename", testRename),
] ]
} }