.some
This commit is contained in:
53
Sources/Extensions.swift
Normal file
53
Sources/Extensions.swift
Normal 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
|
||||
}
|
||||
}
|
||||
56
Sources/Path+Attributes.swift
Normal file
56
Sources/Path+Attributes.swift
Normal file
@@ -0,0 +1,56 @@
|
||||
import Foundation
|
||||
|
||||
public extension Path {
|
||||
/// - Note: If file is already locked, does nothing
|
||||
/// - Note: If file doesn’t 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 isn‘t locked, does nothing
|
||||
/// - Note: If file doesn’t 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 file’s 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Sources/Path+Codable.swift
Normal file
28
Sources/Path+Codable.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
109
Sources/Path+FileManager.swift
Normal file
109
Sources/Path+FileManager.swift
Normal 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 doesn’t 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
|
||||
}
|
||||
}
|
||||
21
Sources/Path+StringConvertibles.swift
Normal file
21
Sources/Path+StringConvertibles.swift
Normal 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
27
Sources/Path+ls.swift
Normal 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
21
Sources/Path->Bool.swift
Normal 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
102
Sources/Path.swift
Normal 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)
|
||||
}
|
||||
21
Sources/TemporaryDirectory.swift
Normal file
21
Sources/TemporaryDirectory.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user