Compare commits

...

11 Commits
0.6.0 ... 0.9.0

Author SHA1 Message Date
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
10 changed files with 159 additions and 56 deletions

View File

@@ -89,11 +89,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 +124,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,11 +28,14 @@ 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_):
let prefs = Path.home.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) try Bundle.resources.join("helper").copy(into: Path.home.join(".local/bin").mkdir(.p)).chmod(0o500)
``` ```
@@ -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,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
@@ -122,8 +138,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
@@ -229,5 +244,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

@@ -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

@@ -147,7 +147,6 @@ public extension Path {
/** /**
Creates the directory at this path. Creates the directory at this path.
- Note: Does not create any intermediary directories.
- Parameter options: Specify `mkdir(.p)` to create intermediary directories. - Parameter options: Specify `mkdir(.p)` to create intermediary directories.
- Note: We do not error if the directory already exists (even without `.p`) - Note: We do not error if the directory already exists (even without `.p`)
because *Path.swift* noops if the desired end result preexists. because *Path.swift* noops if the desired end result preexists.
@@ -174,8 +173,8 @@ public extension Path {
} }
} }
/// Options for `Path.mkdir` /// Options for `Path.mkdir(_:)`
public enum MakeDirectoryOptions { public enum MakeDirectoryOptions {
/// Creates intermediary directories. Works the same as mkdir -p. /// Creates intermediary directories; works the same as `mkdir -p`.
case 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 {

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 `Path`s 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,9 +20,16 @@ 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
If you are constructing Paths from static-strings we provide support for
dynamic members:
let p1 = Path.root.usr.bin.ls // => /usr/bin/ls
- Note: There may not be an actual filesystem entry at the path. The underlying - Note: There may not be an actual filesystem entry at the path. The underlying
representation for `Path` is `String`. representation for `Path` is `String`.
*/ */
@dynamicMemberLookup
public struct Path: Equatable, Hashable, Comparable { public struct Path: Equatable, Hashable, Comparable {
init(string: String) { init(string: String) {
@@ -33,6 +42,12 @@ public struct Path: Equatable, Hashable, Comparable {
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
@@ -168,24 +183,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,10 +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().join("e").touch() try tmpdir.join(".d").mkdir().e.touch()
var paths = Set<String>() var paths = Set<String>()
var dirs = 0 var dirs = 0
@@ -139,6 +139,13 @@ 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 { root in try Path.mktemp { root in
let bar = try root.join("bar").touch() let bar = try root.join("bar").touch()

View File

@@ -6,6 +6,7 @@ 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),