Compare commits

...

11 Commits
0.2.2 ... 0.3.0

Author SHA1 Message Date
Max Howell
6c24de4875 Fix actually deploying 2019-01-19 15:08:11 -05:00
Max Howell
6df12b3bb9 Refer to alternative packages 2019-01-19 15:07:47 -05:00
Max Howell
d52cdf96c4 Merge pull request #4 from mxcl/more-docs
More docs
2019-01-19 14:46:46 -05:00
Max Howell
3eda9a9741 100% documentation please 2019-01-19 14:36:27 -05:00
Max Howell
29149da72b Use FileManager.homeDirectory where possible 2019-01-19 14:35:42 -05:00
Max Howell
cd30e89808 Remove TemporaryDirectory
This violates the responsibility of this framework.
2019-01-19 14:20:12 -05:00
Max Howell
9f76eeb507 Link to Jazzy docs 2019-01-19 12:55:18 -05:00
Max Howell
b5bdaa6ceb Fix Jazzy root-url 2019-01-19 12:52:47 -05:00
Max Howell
17dd706115 Merge pull request #3 from mxcl/jazzy
Generate Jazzy docs in Travis
2019-01-19 12:40:21 -05:00
Max Howell
903038ae80 Generate Jazzy docs in Travis 2019-01-19 12:34:35 -05:00
Max Howell
f56811d64f Document various rules and caveats 2019-01-18 17:47:04 -05:00
12 changed files with 254 additions and 20 deletions

View File

@@ -1,6 +1,11 @@
# 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: test
- name: deploy
if: branch =~ ^\d+\.\d+\.\d+$
os: osx os: osx
language: swift language: swift
osx_image: xcode10.1 osx_image: xcode10.1
@@ -11,7 +16,7 @@ jobs:
include: include:
- script: swift test - script: swift test
name: macOS name: macOS
- &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
@@ -29,7 +34,7 @@ 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
- env: SWIFT_VERSION=4.2.1 - env: SWIFT_VERSION=4.2.1
os: linux os: linux
name: Linux name: Linux
@@ -38,3 +43,24 @@ jobs:
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
- <<: *xcodebuild
stage: deploy
name: Jazzy
install: gem install jazzy
script: |
jazzy \
--min-acl internal \
--no-hide-documentation-coverage \
--theme fullwidth \
--output output \
--readme README.md \
--root-url https://mxcl.github.io/Path.swift/ \
--github_url https://github.com/mxcl/Path.swift
deploy:
provider: pages
skip-cleanup: true
github-token: $GITHUB_TOKEN
local-dir: output
on:
tags: true

View File

@@ -16,10 +16,10 @@ let docs = Path.home/"Documents"
let path = Path(userInput) ?? Path.cwd/userInput let path = Path(userInput) ?? Path.cwd/userInput
// chainable syntax so you have less boilerplate // chainable syntax so you have less boilerplate
try Path.home.join("foo").mkpath().join("bar").chmod(0o555) try Path.home.join("foo").mkpath().join("bar").touch().chmod(0o555)
// easy file-management // easy file-management
try Path.root.join("foo").copy(to: Path.root.join("bar")) try Path.root.join("foo").copy(to: Path.root/"bar")
// careful API to avoid common bugs // careful API to avoid common bugs
try Path.root.join("foo").copy(into: Path.root.mkdir("bar")) try Path.root.join("foo").copy(into: Path.root.mkdir("bar"))
@@ -28,7 +28,8 @@ try Path.root.join("foo").copy(into: Path.root.mkdir("bar"))
// were meant to be directory destinations // were meant to be directory destinations
``` ```
Paths are just string representations, there *might not* be a real file there. We emphasize safety and correctness, just like Swift, and also just
like Swift, we provide a thoughtful and comprehensive (yet concise) API.
# Support mxcl # Support mxcl
@@ -40,6 +41,10 @@ can continue to make tools and software you need and love. I appreciate it x.
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160"> <img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160">
</a> </a>
# Handbook
Our [online API documentation] is automatically updated for new releases.
## Codable ## Codable
We support `Codable` as you would expect: We support `Codable` as you would expect:
@@ -134,6 +139,38 @@ let dirs = Path.home.ls().directories().filter {
let swiftFiles = Path.home.ls().files(withExtension: "swift") let swiftFiles = Path.home.ls().files(withExtension: "swift")
``` ```
# Rules & Caveats
Paths are just string representations, there *might not* be a real file there.
```swift
Path.home/"b" // => /Users/mxcl/b
// joining multiple strings works as youd expect
Path.home/"b"/"c" // => /Users/mxcl/b/c
// joining multiple parts at a time is fine
Path.home/"b/c" // => /Users/mxcl/b/c
// joining with absolute paths omits prefixed slash
Path.home/"/b" // => /Users/mxcl/b
// of course, feel free to join variables:
let b = "b"
let c = "c"
Path.home/b/c // => /Users/mxcl/b/c
// tilde is not special here
Path.root/"~b" // => /~b
Path.root/"~/b" // => /~/b
// but is here
Path("~/foo")! // => /Users/foo
// this does not work though
Path("~foo") // => nil
```
# Installation # Installation
SwiftPM only: SwiftPM only:
@@ -146,6 +183,13 @@ package.append(.package(url: "https://github.com/mxcl/Path.swift", from: "0.0.0"
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/

View File

@@ -1,26 +1,31 @@
import Foundation import Foundation
public extension Bundle { public extension Bundle {
/// Returns the path for requested resource in this bundle.
func path(forResource: String, ofType: String?) -> Path? { func path(forResource: String, ofType: String?) -> Path? {
let f: (String?, String?) -> String? = path(forResource:ofType:) let f: (String?, String?) -> String? = path(forResource:ofType:)
let str = f(forResource, ofType) let str = f(forResource, ofType)
return str.flatMap(Path.init) return str.flatMap(Path.init)
} }
/// Returns the path for the shared-frameworks directory in this bundle.
public var sharedFrameworks: Path? { public var sharedFrameworks: Path? {
return sharedFrameworksPath.flatMap(Path.init) return sharedFrameworksPath.flatMap(Path.init)
} }
/// Returns the path for the resources directory in this bundle.
public var resources: Path? { public var resources: Path? {
return resourcePath.flatMap(Path.init) return resourcePath.flatMap(Path.init)
} }
/// Returns the path for this bundle.
public var path: Path { public var path: Path {
return Path(string: bundlePath) return Path(string: bundlePath)
} }
} }
public extension String { public extension String {
/// Initializes this `String` with the contents of the provided path.
@inlinable @inlinable
init(contentsOf path: Path) throws { init(contentsOf path: Path) throws {
try self.init(contentsOfFile: path.string) try self.init(contentsOfFile: path.string)
@@ -36,6 +41,7 @@ public extension String {
} }
public extension Data { public extension Data {
/// Initializes this `Data` with the contents of the provided path.
@inlinable @inlinable
init(contentsOf path: Path) throws { init(contentsOf path: Path) throws {
try self.init(contentsOf: path.url) try self.init(contentsOf: path.url)

View File

@@ -43,14 +43,18 @@ public extension Path {
return self return self
} }
/// - Returns: modification-time or creation-time if none /**
Returns the modification-time.
- Note: Returns the creation time if there is no modification time.
- Note: Returns UNIX-time-zero if neither are available, though this *should* be impossible.
*/
public var mtime: Date { public var mtime: Date {
do { do {
let attrs = try FileManager.default.attributesOfItem(atPath: string) let attrs = try FileManager.default.attributesOfItem(atPath: string)
return attrs[.modificationDate] as? Date ?? attrs[.creationDate] as? Date ?? Date() return attrs[.modificationDate] as? Date ?? attrs[.creationDate] as? Date ?? Date()
} catch { } catch {
//TODO print(error) //TODO log error
return Date() return Date(timeIntervalSince1970: 0)
} }
} }
} }

View File

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

View File

@@ -41,6 +41,14 @@ public extension Path {
return rv return rv
} }
/**
Moves a file.
- Note: `throws` if `to` is a directory.
- Parameter to: Destination filename.
- Parameter overwrite: If true overwrites any file that already exists at `to`.
- Returns: `to` to allow chaining
- SeeAlso: move(into:overwrite:)
*/
@discardableResult @discardableResult
public func move(to: Path, overwrite: Bool = false) throws -> Path { public func move(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.exists { if overwrite, to.exists {
@@ -50,6 +58,17 @@ public extension Path {
return to return to
} }
/**
Moves a file into a directory
If the destination does not exist, this function creates the directory first.
- Note: `throws` if `into` is a file.
- Parameter into: Destination directory
- Parameter overwrite: If true overwrites any file that already exists at `into`.
- Returns: The `Path` of destination filename.
- SeeAlso: move(into:overwrite:)
*/
@discardableResult @discardableResult
public func move(into: Path) throws -> Path { public func move(into: Path) throws -> Path {
if !into.exists { if !into.exists {
@@ -62,17 +81,23 @@ public extension Path {
return rv return rv
} }
/// Deletes the path, recursively if a directory.
@inlinable @inlinable
public func delete() throws { public func delete() throws {
try FileManager.default.removeItem(at: url) try FileManager.default.removeItem(at: url)
} }
/**
Creates an empty file at this path.
- Returns: `self` to allow chaining.
*/
@inlinable @inlinable
@discardableResult @discardableResult
func touch() throws -> Path { func touch() throws -> Path {
return try "".write(to: self) return try "".write(to: self)
} }
/// Helper due to Linux Swift being incomplete.
private func _foo(go: () throws -> Void) throws { private func _foo(go: () throws -> Void) throws {
#if !os(Linux) #if !os(Linux)
do { do {
@@ -92,6 +117,11 @@ public extension Path {
#endif #endif
} }
/**
Creates the directory at this path.
- Note: Does not create any intermediary directories.
- Returns: `self` to allow chaining.
*/
@discardableResult @discardableResult
public func mkdir() throws -> Path { public func mkdir() throws -> Path {
try _foo { try _foo {
@@ -100,6 +130,11 @@ public extension Path {
return self return self
} }
/**
Creates the directory at this path.
- Note: Creates any intermediary directories, if required.
- Returns: `self` to allow chaining.
*/
@discardableResult @discardableResult
public func mkpath() throws -> Path { public func mkpath() throws -> Path {
try _foo { try _foo {
@@ -108,8 +143,15 @@ public extension Path {
return self return self
} }
/// - Note: If file doesnt exist, creates file /**
/// - Note: If file is not writable, makes writable first, resetting permissions after the write Replaces the contents of the file at this path with the provided string.
- Note: If file doesnt exist, creates file
- Note: If file is not writable, makes writable first, resetting permissions after the write
- Parameter contents: The string that will become the contents of this file.
- Parameter atomically: If `true` the operation will be performed atomically.
- Parameter encoding: The string encoding to use.
- Returns: `self` to allow chaining.
*/
@discardableResult @discardableResult
public func replaceContents(with contents: String, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> Path { public func replaceContents(with contents: String, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> Path {
let resetPerms: Int? let resetPerms: Int?

View File

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

View File

@@ -13,12 +13,14 @@ public extension Path {
} }
public extension Array where Element == Path.Entry { public extension Array where Element == Path.Entry {
/// Filters the list of entries to be a list of Paths that are directories.
var directories: [Path] { var directories: [Path] {
return compactMap { return compactMap {
$0.kind == .directory ? $0.path : nil $0.kind == .directory ? $0.path : nil
} }
} }
/// Filters the list of entries to be a list of Paths that are files with the specified extension
func files(withExtension ext: String) -> [Path] { func files(withExtension ext: String) -> [Path] {
return compactMap { return compactMap {
$0.kind == .file && $0.path.extension == ext ? $0.path : nil $0.kind == .file && $0.path.extension == ext ? $0.path : nil

View File

@@ -1,24 +1,29 @@
import Foundation import Foundation
public extension Path { public extension Path {
/// Returns true if the path represents an actual file that is also writable by the current user.
var isWritable: Bool { var isWritable: Bool {
return FileManager.default.isWritableFile(atPath: string) return FileManager.default.isWritableFile(atPath: string)
} }
/// Returns true if the path represents an actual directory.
var isDirectory: Bool { var isDirectory: Bool {
var isDir: ObjCBool = false var isDir: ObjCBool = false
return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && isDir.boolValue return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && isDir.boolValue
} }
/// Returns true if the path represents an actual filesystem entry that is *not* a directory.
var isFile: Bool { var isFile: Bool {
var isDir: ObjCBool = true var isDir: ObjCBool = true
return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && !isDir.boolValue return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && !isDir.boolValue
} }
/// Returns true if the path represents an actual file that is also executable by the current user.
var isExecutable: Bool { var isExecutable: Bool {
return FileManager.default.isExecutableFile(atPath: string) return FileManager.default.isExecutableFile(atPath: string)
} }
/// Returns true if the path represents an actual filesystem entry.
var exists: Bool { var exists: Bool {
return FileManager.default.fileExists(atPath: string) return FileManager.default.fileExists(atPath: string)
} }

View File

@@ -1,35 +1,82 @@
import Foundation import Foundation
/**
Represents a platform filesystem absolute path.
The recommended conversions from string are:
let p1 = Path.root/pathString
let p2 = Path.root/url.path
let p3 = Path.cwd/relativePathString
let p4 = Path(userInput) ?? Path.cwd/userInput
- Note: There may not be an actual filename at the path.
*/
public struct Path: Equatable, Hashable, Comparable { public struct Path: Equatable, Hashable, Comparable {
/// The underlying filesystem path
public let string: String public let string: String
/// Returns a `Path` containing ``FileManager.default.currentDirectoryPath`.
public static var cwd: Path { public static var cwd: Path {
return Path(string: FileManager.default.currentDirectoryPath) return Path(string: FileManager.default.currentDirectoryPath)
} }
/// Returns a `Path` representing the root path.
public static var root: Path { public static var root: Path {
return Path(string: "/") return Path(string: "/")
} }
/// Returns a `Path` representing the users home directory
public static var home: Path { public static var home: Path {
return Path(string: NSHomeDirectory()) let string: String
#if os(macOS)
if #available(OSX 10.12, *) {
string = FileManager.default.homeDirectoryForCurrentUser.path
} else {
string = NSHomeDirectory()
}
#else
string = NSHomeDirectory()
#endif
return Path(string: string)
} }
/**
Returns the filename extension of this path.
- Remark: Implemented via `NSString.pathExtension`.
*/
@inlinable @inlinable
public var `extension`: String { public var `extension`: String {
return (string as NSString).pathExtension return (string as NSString).pathExtension
} }
/// - Note: always returns a valid path, `Path.root.parent` *is* `Path.root` /**
Returns the parent directory for this path.
Path is not aware of the nature of the underlying file, but this is
irrlevant since the operation is the same irrespective of this fact.
- Note: always returns a valid path, `Path.root.parent` *is* `Path.root`.
*/
public var parent: Path { public var parent: Path {
return Path(string: (string as NSString).deletingLastPathComponent) return Path(string: (string as NSString).deletingLastPathComponent)
} }
/// Returns a `URL` representing this file path.
@inlinable @inlinable
public var url: URL { public var url: URL {
return URL(fileURLWithPath: string) return URL(fileURLWithPath: string)
} }
/**
The basename for the provided file, optionally dropping the file extension.
Path.root.join("foo.swift").basename() // => "foo.swift"
Path.root.join("foo.swift").basename(dropExtension: true) // => "foo"
- Returns: A string that is the filenames basename.
- Parameter dropExtension: If `true` returns the basename without its file extension.
*/
public func basename(dropExtension: Bool = false) -> String { public func basename(dropExtension: Bool = false) -> String {
let str = string as NSString let str = string as NSString
if !dropExtension { if !dropExtension {
@@ -44,7 +91,13 @@ public struct Path: Equatable, Hashable, Comparable {
} }
} }
//TODO another variant that returns `nil` if result would start with `..` /**
Returns a string representing the relative path to `base`.
- Note: If `base` is not a logical prefix for `self` your result will be prefixed some number of `../` components.
- Parameter base: The base to which we calculate the relative path.
- ToDo: Another variant that returns `nil` if result would start with `..`
*/
public func relative(to base: Path) -> String { public func relative(to base: Path) -> String {
// Split the two paths into their components. // Split the two paths into their components.
// FIXME: The is needs to be optimized to avoid unncessary copying. // FIXME: The is needs to be optimized to avoid unncessary copying.
@@ -75,27 +128,59 @@ public struct Path: Equatable, Hashable, Comparable {
} }
} }
public func join<S>(_ part: S) -> Path where S: StringProtocol { /**
Joins a path and a string to produce a new path.
Path.root.join("a") // => /a
Path.root.join("a/b") // => /a/b
Path.root.join("a").join("b") // => /a/b
Path.root.join("a").join("/b") // => /a/b
- Parameter pathComponent: The string to join with this path.
- Returns: A new joined path.
- SeeAlso: /(:Path,:String)
*/
public func join<S>(_ pathComponent: S) -> Path where S: StringProtocol {
//TODO standardizingPath does more than we want really (eg tilde expansion) //TODO standardizingPath does more than we want really (eg tilde expansion)
let str = (string as NSString).appendingPathComponent(String(part)) let str = (string as NSString).appendingPathComponent(String(pathComponent))
return Path(string: (str as NSString).standardizingPath) return Path(string: (str as NSString).standardizingPath)
} }
/// Returns the locale-aware sort order for the two paths.
@inlinable @inlinable
public static func <(lhs: Path, rhs: Path) -> Bool { public static func <(lhs: Path, rhs: Path) -> Bool {
return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending
} }
/// A file entry from a directory listing.
public struct Entry { public struct Entry {
/// The kind of this directory entry.
public enum Kind { public enum Kind {
/// The path is a file.
case file case file
/// The path is a directory.
case directory case directory
} }
/// The kind of this entry.
public let kind: Kind public let kind: Kind
/// The path of this entry.
public let path: Path public let path: Path
} }
} }
/**
Joins a path and a string to produce a new path.
Path.root/"a" // => /a
Path.root/"a/b" // => /a/b
Path.root/"a"/"b" // => /a/b
Path.root/"a"/"/b" // => /a/b
- Parameter lhs: The base path to join with `rhs`.
- Parameter rhs: The string to join with this `lhs`.
- Returns: A new joined path.
- SeeAlso: Path.join(_:)
*/
@inlinable @inlinable
public func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol { public func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol {
return lhs.join(rhs) return lhs.join(rhs)

View File

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

View File

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