This commit is contained in:
Max Howell
2019-01-17 16:33:38 -05:00
parent 8e0d766924
commit 97e9cbaa8f
16 changed files with 754 additions and 0 deletions

53
Sources/Extensions.swift Normal file
View File

@@ -0,0 +1,53 @@
import Foundation
public extension Bundle {
func path(forResource: String, ofType: String?) -> Path? {
let f: (String?, String?) -> String? = path(forResource:ofType:)
let str = f(forResource, ofType)
return str.flatMap(Path.init)
}
public var sharedFrameworks: Path? {
return sharedFrameworksPath.flatMap(Path.init)
}
public var resources: Path? {
return resourcePath.flatMap(Path.init)
}
}
public extension String {
@inlinable
init(contentsOf path: Path) throws {
try self.init(contentsOfFile: path.string)
}
/// - Returns: `to` to allow chaining
@inlinable
@discardableResult
func write(to: Path, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> Path {
try write(toFile: to.string, atomically: atomically, encoding: encoding)
return to
}
}
public extension Data {
@inlinable
init(contentsOf path: Path) throws {
try self.init(contentsOf: path.url)
}
/// - Returns: `to` to allow chaining
@inlinable
@discardableResult
func write(to: Path, atomically: Bool = false) throws -> Path {
let opts: NSData.WritingOptions
if atomically {
opts = .atomicWrite
} else {
opts = []
}
try write(to: to.url, options: opts)
return to
}
}

View File

@@ -0,0 +1,56 @@
import Foundation
public extension Path {
/// - Note: If file is already locked, does nothing
/// - Note: If file doesnt exist, throws
@discardableResult
public func lock() throws -> Path {
var attrs = try FileManager.default.attributesOfItem(atPath: string)
let b = attrs[.immutable] as? Bool ?? false
if !b {
attrs[.immutable] = true
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
}
return self
}
/// - Note: If file isnt locked, does nothing
/// - Note: If file doesnt exist, does nothing
@discardableResult
public func unlock() throws -> Path {
var attrs: [FileAttributeKey: Any]
do {
attrs = try FileManager.default.attributesOfItem(atPath: string)
} catch CocoaError.fileReadNoSuchFile {
return self
}
let b = attrs[.immutable] as? Bool ?? false
if b {
attrs[.immutable] = false
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
}
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

@@ -0,0 +1,28 @@
import Foundation
public extension CodingUserInfoKey {
static let relativePath = CodingUserInfoKey(rawValue: "dev.mxcl.Path.relative")!
}
extension Path: Codable {
public init(from decoder: Decoder) throws {
let value = try decoder.singleValueContainer().decode(String.self)
if value.hasPrefix("/") {
string = value
} else {
guard let root = decoder.userInfo[.relativePath] as? Path else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Path cannot decode a relative path if `userInfo[.relativePath]` not set to a Path object."))
}
string = (root/value).string
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if let root = encoder.userInfo[.relativePath] as? Path {
try container.encode(relative(to: root))
} else {
try container.encode(string)
}
}
}

View File

@@ -0,0 +1,109 @@
import Foundation
public extension Path {
/**
Copies 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: copy(into:overwrite:)
*/
@discardableResult
public func copy(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.exists {
try FileManager.default.removeItem(at: to.url)
}
try FileManager.default.copyItem(atPath: string, toPath: to.string)
return to
}
/**
Copies 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 the newly copied file.
- SeeAlso: copy(into:overwrite:)
*/
@discardableResult
public func copy(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
} else if overwrite, !into.isDirectory {
try into.delete()
}
let rv = into/basename()
try FileManager.default.copyItem(at: url, to: rv.url)
return rv
}
@discardableResult
public func move(into: Path) throws -> Path {
if !into.exists {
try into.mkpath()
} else if !into.isDirectory {
throw CocoaError.error(.fileWriteFileExists)
}
try FileManager.default.moveItem(at: url, to: into.join(basename()).url)
return self
}
@inlinable
public func delete() throws {
try FileManager.default.removeItem(at: url)
}
@inlinable
@discardableResult
func touch() throws -> Path {
return try "".write(to: self)
}
@inlinable
@discardableResult
public func mkdir() throws -> Path {
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil)
} catch CocoaError.Code.fileWriteFileExists {
// noop
}
return self
}
@inlinable
@discardableResult
public func mkpath() throws -> Path {
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
} catch CocoaError.Code.fileWriteFileExists {
// noop
}
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
}
}

View File

@@ -0,0 +1,21 @@
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 {
public var description: String {
return string
}
}
extension Path: CustomDebugStringConvertible {
public var debugDescription: String {
return "Path(string: \(string))"
}
}

27
Sources/Path+ls.swift Normal file
View File

@@ -0,0 +1,27 @@
import Foundation
public extension Path {
/// same as the `ls` command is shallow
func ls() throws -> [Entry] {
let relativePaths = try FileManager.default.contentsOfDirectory(atPath: string)
func convert(relativePath: String) -> Entry {
let path = self/relativePath
return Entry(kind: path.isDirectory ? .directory : .file, path: path)
}
return relativePaths.map(convert)
}
}
public extension Array where Element == Path.Entry {
var directories: [Path] {
return compactMap {
$0.kind == .directory ? $0.path : nil
}
}
func files(withExtension ext: String) -> [Path] {
return compactMap {
$0.kind == .file && $0.path.extension == ext ? $0.path : nil
}
}
}

21
Sources/Path->Bool.swift Normal file
View File

@@ -0,0 +1,21 @@
import Foundation
public extension Path {
var isWritable: Bool {
return FileManager.default.isWritableFile(atPath: string)
}
var isDirectory: Bool {
var isDir: ObjCBool = false
return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && isDir.boolValue
}
var isFile: Bool {
var isDir: ObjCBool = true
return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && !isDir.boolValue
}
var exists: Bool {
return FileManager.default.fileExists(atPath: string)
}
}

102
Sources/Path.swift Normal file
View File

@@ -0,0 +1,102 @@
import Foundation
public struct Path: Equatable, Hashable, Comparable {
public let string: String
public static var cwd: Path {
return Path(string: FileManager.default.currentDirectoryPath)
}
public static var root: Path {
return Path(string: "/")
}
public static var home: Path {
return Path(string: NSHomeDirectory())
}
@inlinable
public var `extension`: String {
return (string as NSString).pathExtension
}
/// - Note: always returns a valid path, `Path.root.parent` *is* `Path.root`
public var parent: Path {
return Path(string: (string as NSString).deletingLastPathComponent)
}
@inlinable
public var url: URL {
return URL(fileURLWithPath: string)
}
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
}
}
}
//TODO another variant that returns `nil` if result would start with `..`
public func relative(to base: Path) -> String {
// Split the two paths into their components.
// FIXME: The is needs to be optimized to avoid unncessary copying.
let pathComps = (string as NSString).pathComponents
let baseComps = (base.string as NSString).pathComponents
// It's common for the base to be an ancestor, so try that first.
if pathComps.starts(with: baseComps) {
// Special case, which is a plain path without `..` components. It
// might be an empty path (when self and the base are equal).
let relComps = pathComps.dropFirst(baseComps.count)
return relComps.joined(separator: "/")
} else {
// General case, in which we might well need `..` components to go
// "up" before we can go "down" the directory tree.
var newPathComps = ArraySlice(pathComps)
var newBaseComps = ArraySlice(baseComps)
while newPathComps.prefix(1) == newBaseComps.prefix(1) {
// First component matches, so drop it.
newPathComps = newPathComps.dropFirst()
newBaseComps = newBaseComps.dropFirst()
}
// Now construct a path consisting of as many `..`s as are in the
// `newBaseComps` followed by what remains in `newPathComps`.
var relComps = Array(repeating: "..", count: newBaseComps.count)
relComps.append(contentsOf: newPathComps)
return relComps.joined(separator: "/")
}
}
public func join<S>(_ part: S) -> Path where S: StringProtocol {
//TODO standardizingPath does more than we want really (eg tilde expansion)
let str = (string as NSString).appendingPathComponent(String(part))
return Path(string: (str as NSString).standardizingPath)
}
@inlinable
public static func <(lhs: Path, rhs: Path) -> Bool {
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

@@ -0,0 +1,21 @@
import Foundation
public class TemporaryDirectory {
public let url: URL
public var path: Path { return Path(string: url.path) }
public init() throws {
url = try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: URL(fileURLWithPath: "/"), create: true)
}
deinit {
_ = try? FileManager.default.removeItem(at: url)
}
}
public extension Path {
static func mktemp<T>(body: (Path) throws -> T) throws -> T {
let tmp = try TemporaryDirectory()
return try body(tmp.path)
}
}