Skip to content

Commit 97e9cba

Browse files
committed
.some
1 parent 8e0d766 commit 97e9cba

16 files changed

+754
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.DS_Store
2+
/.build
3+
/*.xcodeproj

LICENSE.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Unlicense (Public Domain)
2+
============================
3+
4+
This is free and unencumbered software released into the public domain.
5+
6+
Anyone is free to copy, modify, publish, use, compile, sell, or
7+
distribute this software, either in source code form or as a compiled
8+
binary, for any purpose, commercial or non-commercial, and by any
9+
means.
10+
11+
In jurisdictions that recognize copyright laws, the author or authors
12+
of this software dedicate any and all copyright interest in the
13+
software to the public domain. We make this dedication for the benefit
14+
of the public at large and to the detriment of our heirs and
15+
successors. We intend this dedication to be an overt act of
16+
relinquishment in perpetuity of all present and future rights to this
17+
software under copyright law.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
22+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
23+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
24+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25+
OTHER DEALINGS IN THE SOFTWARE.
26+
27+
For more information, please refer to &lt;<http://unlicense.org/>&gt;

Package.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// swift-tools-version:4.2
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "Path.swift",
6+
products: [
7+
.library(name: "Path", targets: ["Path"]),
8+
],
9+
targets: [
10+
.target(name: "Path", path: "Sources"),
11+
.testTarget(name: "PathTests", dependencies: ["Path"]),
12+
]
13+
)

README.md

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Path.swift
2+
3+
A file-system pathing library focused on developer experience and robust
4+
end‐results.
5+
6+
```swift
7+
// convenient static members
8+
let home = Path.home
9+
10+
// pleasant joining syntax
11+
let docs = Path.home/"Documents"
12+
13+
// paths are *always* absolute thus avoiding common bugs
14+
let path = Path(userInput) ?? Path.cwd/userInput
15+
16+
// chainable syntax so you have less boilerplate
17+
try Path.home.join("foo").mkpath().join("bar").chmod(0o555)
18+
19+
// easy file-management
20+
try Path.root.join("foo").copy(to: Path.root.join("bar"))
21+
22+
// careful API to avoid common bugs
23+
try Path.root.join("foo").copy(into: Path.root.mkdir("bar"))
24+
// ^^ other libraries would make the `to:` form handle both these cases
25+
// but that can easily lead to bugs where you accidentally write files that
26+
// were meant to be directory destinations
27+
```
28+
29+
Paths are just string representations, there *may not* be a real file there.
30+
31+
# Support mxcl
32+
33+
Hi, I’m Max Howell and I have written a lot of open source software, and
34+
probably you already use some of it (Homebrew anyone?). Please help me so I
35+
can continue to make tools and software you need and love. I appreciate it x.
36+
37+
<a href="https://www.patreon.com/mxcl">
38+
<img src="https://c5.patreon.com/external/logo/[email protected]" width="160">
39+
</a>
40+
41+
## Codable
42+
43+
We support `Codable` as you would expect:
44+
45+
```swift
46+
try JSONEncoder().encode([Path.home, Path.home/"foo"])
47+
```
48+
49+
```json
50+
[
51+
"/Users/mxcl",
52+
"/Users/mxcl/foo",
53+
]
54+
```
55+
56+
However, often you want to encode relative paths:
57+
58+
```swift
59+
let encoder = JSONEncoder()
60+
encoder.userInfo[.relativePath] = Path.home
61+
encoder.encode([Path.home, Path.home/"foo"])
62+
```
63+
64+
```json
65+
[
66+
"",
67+
"foo",
68+
]
69+
```
70+
71+
**Note** make sure you decode with this key set *also*, otherwise we `fatal`
72+
(unless the paths are absolute obv.)
73+
74+
```swift
75+
let decoder = JSONDecoder()
76+
decoder.userInfo[.relativePath] = Path.home
77+
decoder.decode(from: data)
78+
```
79+
80+
## Initializing from user-input
81+
82+
The `Path` initializer returns `nil` unless fed an absolute path; thus to
83+
initialize from user-input that may contain a relative path use this form:
84+
85+
```swift
86+
let path = Path(userInput) ?? Path.cwd/userInput
87+
```
88+
89+
This is explicit, not hiding anything that code-review may miss and preventing
90+
common bugs like accidentally creating `Path` objects from strings you did not
91+
expect to be relative.
92+
93+
## Extensions
94+
95+
We have some extensions to Apple APIs:
96+
97+
```swift
98+
let bashProfile = try String(contentsOf: Path.home/".bash_profile")
99+
let history = try Data(contentsOf: Path.home/".history")
100+
101+
bashProfile += "\n\nfoo"
102+
103+
try bashProfile.write(to: Path.home/".bash_profile")
104+
105+
try Bundle.main.resources!.join("foo").copy(to: .home)
106+
// ^^ `-> Path?` because the underlying `Bundle` function is `-> String?`
107+
```
108+
109+
## Directory listings
110+
111+
We provide `ls()`, called because it behaves like the Terminal `ls` function,
112+
the name thus implies its behavior, ie. that it is not recursive.
113+
114+
```swift
115+
for path in Path.home.ls() {
116+
print(path.path)
117+
print(path.kind) // .directory or .file
118+
}
119+
120+
for path in Path.home.ls() where path.kind == .file {
121+
//
122+
}
123+
124+
for path in Path.home.ls() where path.mtime > yesterday {
125+
//
126+
}
127+
128+
let dirs = Path.home.ls().directories().filter {
129+
//
130+
}
131+
132+
let swiftFiles = Path.home.ls().files(withExtension: "swift")
133+
```
134+
135+
# Installation
136+
137+
SwiftPM only:
138+
139+
```swift
140+
package.append(.package(url: "https://github.com/mxcl/Path.swift", from: "0.0.0"))
141+
```
142+
143+
### Get push notifications for new releases
144+
145+
https://codebasesaga.com/canopy/

Sources/Extensions.swift

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import Foundation
2+
3+
public extension Bundle {
4+
func path(forResource: String, ofType: String?) -> Path? {
5+
let f: (String?, String?) -> String? = path(forResource:ofType:)
6+
let str = f(forResource, ofType)
7+
return str.flatMap(Path.init)
8+
}
9+
10+
public var sharedFrameworks: Path? {
11+
return sharedFrameworksPath.flatMap(Path.init)
12+
}
13+
14+
public var resources: Path? {
15+
return resourcePath.flatMap(Path.init)
16+
}
17+
}
18+
19+
public extension String {
20+
@inlinable
21+
init(contentsOf path: Path) throws {
22+
try self.init(contentsOfFile: path.string)
23+
}
24+
25+
/// - Returns: `to` to allow chaining
26+
@inlinable
27+
@discardableResult
28+
func write(to: Path, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> Path {
29+
try write(toFile: to.string, atomically: atomically, encoding: encoding)
30+
return to
31+
}
32+
}
33+
34+
public extension Data {
35+
@inlinable
36+
init(contentsOf path: Path) throws {
37+
try self.init(contentsOf: path.url)
38+
}
39+
40+
/// - Returns: `to` to allow chaining
41+
@inlinable
42+
@discardableResult
43+
func write(to: Path, atomically: Bool = false) throws -> Path {
44+
let opts: NSData.WritingOptions
45+
if atomically {
46+
opts = .atomicWrite
47+
} else {
48+
opts = []
49+
}
50+
try write(to: to.url, options: opts)
51+
return to
52+
}
53+
}

Sources/Path+Attributes.swift

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import Foundation
2+
3+
public extension Path {
4+
/// - Note: If file is already locked, does nothing
5+
/// - Note: If file doesn’t exist, throws
6+
@discardableResult
7+
public func lock() throws -> Path {
8+
var attrs = try FileManager.default.attributesOfItem(atPath: string)
9+
let b = attrs[.immutable] as? Bool ?? false
10+
if !b {
11+
attrs[.immutable] = true
12+
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
13+
}
14+
return self
15+
}
16+
17+
/// - Note: If file isn‘t locked, does nothing
18+
/// - Note: If file doesn’t exist, does nothing
19+
@discardableResult
20+
public func unlock() throws -> Path {
21+
var attrs: [FileAttributeKey: Any]
22+
do {
23+
attrs = try FileManager.default.attributesOfItem(atPath: string)
24+
} catch CocoaError.fileReadNoSuchFile {
25+
return self
26+
}
27+
let b = attrs[.immutable] as? Bool ?? false
28+
if b {
29+
attrs[.immutable] = false
30+
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
31+
}
32+
return self
33+
}
34+
35+
/**
36+
Sets the file’s attributes using UNIX octal notation.
37+
38+
Path.home.join("foo").chmod(0o555)
39+
*/
40+
@discardableResult
41+
public func chmod(_ octal: Int) throws -> Path {
42+
try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string)
43+
return self
44+
}
45+
46+
/// - Returns: modification-time or creation-time if none
47+
public var mtime: Date {
48+
do {
49+
let attrs = try FileManager.default.attributesOfItem(atPath: string)
50+
return attrs[.modificationDate] as? Date ?? attrs[.creationDate] as? Date ?? Date()
51+
} catch {
52+
//TODO print(error)
53+
return Date()
54+
}
55+
}
56+
}

Sources/Path+Codable.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Foundation
2+
3+
public extension CodingUserInfoKey {
4+
static let relativePath = CodingUserInfoKey(rawValue: "dev.mxcl.Path.relative")!
5+
}
6+
7+
extension Path: Codable {
8+
public init(from decoder: Decoder) throws {
9+
let value = try decoder.singleValueContainer().decode(String.self)
10+
if value.hasPrefix("/") {
11+
string = value
12+
} else {
13+
guard let root = decoder.userInfo[.relativePath] as? Path else {
14+
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Path cannot decode a relative path if `userInfo[.relativePath]` not set to a Path object."))
15+
}
16+
string = (root/value).string
17+
}
18+
}
19+
20+
public func encode(to encoder: Encoder) throws {
21+
var container = encoder.singleValueContainer()
22+
if let root = encoder.userInfo[.relativePath] as? Path {
23+
try container.encode(relative(to: root))
24+
} else {
25+
try container.encode(string)
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)