429 lines
13 KiB
Markdown
429 lines
13 KiB
Markdown
# Swift PathKit ![badge-platforms][] ![badge-languages][]
|
||
|
||
A file-system pathing library focused on developer experience and robust end
|
||
results.
|
||
|
||
> [!NOTE]
|
||
> This repository is a fork of [mxcl/Path.swift][] by [mxcl](), which due to inactivity does not receive new features.
|
||
|
||
[mxcl/Path.swift]: https://github.com/mxcl/Path.swift
|
||
[mxcl]: https://github.com/mxcl
|
||
|
||
## Examples
|
||
|
||
```swift
|
||
import Path
|
||
|
||
// convenient static members
|
||
let home = Path.home
|
||
|
||
// pleasant joining syntax
|
||
let docs = Path.home/"Documents"
|
||
|
||
// paths are *always* absolute thus avoiding common bugs
|
||
let path = Path(userInput) ?? Path.cwd/userInput
|
||
|
||
// elegant, chainable syntax
|
||
try Path.home.join("foo").mkdir().join("bar").touch().chmod(0o555)
|
||
|
||
// sensible considerations
|
||
try Path.home.join("bar").mkdir()
|
||
try Path.home.join("bar").mkdir() // doesn’t throw ∵ we already have the desired result
|
||
|
||
// easy file-management
|
||
let bar = try Path.root.join("foo").copy(to: Path.root/"bar")
|
||
print(bar) // => /bar
|
||
print(bar.isFile) // => true
|
||
|
||
// careful API considerations so as to avoid common bugs
|
||
let foo = try Path.root.join("foo").copy(into: Path.root.join("bar").mkdir())
|
||
print(foo) // => /bar/foo
|
||
print(foo.isFile) // => true
|
||
// ^^ the `into:` version will only copy *into* a directory, the `to:` version copies
|
||
// to a file at that path, thus you will not accidentally copy into directories you
|
||
// may not have realized existed.
|
||
|
||
// we support dynamic-member-syntax when joining named static members, eg:
|
||
let prefs = Path.home.Library.Preferences // => /Users/someuser/Library/Preferences
|
||
|
||
// a practical example: installing a helper executable
|
||
try Bundle.resources.helper.copy(into: Path.root.usr.local.bin).chmod(0o500)
|
||
```
|
||
|
||
This repository emphasizes safety and correctness, just like Swift, and also (again like
|
||
Swift), we provide a thoughtful and comprehensive (yet concise) API.
|
||
|
||
# Handbook
|
||
|
||
The [online API documentation][docs] covers 100% of the public API and is
|
||
automatically updated for new releases.
|
||
|
||
## Codable
|
||
|
||
`Path` conforms to `Codable`:
|
||
|
||
```swift
|
||
try JSONEncoder().encode([Path.home, Path.home/"foo"])
|
||
```
|
||
|
||
```json
|
||
[
|
||
"/Users/someuser",
|
||
"/Users/someuser/foo",
|
||
]
|
||
```
|
||
|
||
`Paths` can be encoded as *relative* paths‡:
|
||
|
||
```swift
|
||
let encoder = JSONEncoder()
|
||
encoder.userInfo[.relativePath] = Path.home
|
||
encoder.encode([Path.home, Path.home/"foo", Path.home/"../baz"])
|
||
```
|
||
|
||
```json
|
||
[
|
||
"",
|
||
"foo",
|
||
"../baz"
|
||
]
|
||
```
|
||
|
||
**Note** if you encode with this key set you *must* decode with the key
|
||
set also:
|
||
|
||
```swift
|
||
let decoder = JSONDecoder()
|
||
decoder.userInfo[.relativePath] = Path.home
|
||
try decoder.decode(from: data) // would throw if `.relativePath` not set
|
||
```
|
||
|
||
> ‡ If you are saving files to a system provided location, eg. Documents then
|
||
> the directory could change at Apple’s choice, or if say the user changes their
|
||
> username. Using relative paths also provides you with the flexibility in
|
||
> future to change where you are storing your files without hassle.
|
||
|
||
## Dynamic members
|
||
|
||
`Path` supports `@dynamicMemberLookup`:
|
||
|
||
```swift
|
||
let ls = Path.root.usr.bin.ls // => /usr/bin/ls
|
||
```
|
||
|
||
This is provided for “starting” functions only, eg. `Path.home` or `Bundle.path`.
|
||
Allowing arbitrary property access only in special cases is a precaution.
|
||
|
||
### Pathish
|
||
|
||
`Path`, and `DynamicPath` (the result of eg. `Path.root`) both conform to
|
||
`Pathish` which is a protocol that contains all pathing functions. Thus if
|
||
you create objects from a mixture of both you need to create generic
|
||
functions or convert any `DynamicPath`s to `Path` first:
|
||
|
||
```swift
|
||
let path1 = Path("/usr/lib")!
|
||
let path2 = Path.root.usr.bin
|
||
var paths = [Path]()
|
||
paths.append(path1) // fine
|
||
paths.append(path2) // error
|
||
paths.append(Path(path2)) // ok
|
||
```
|
||
|
||
This is inconvenient but as Swift stands there’s nothing we can think of
|
||
that would help.
|
||
|
||
## Initializing from user-input
|
||
|
||
The `Path` initializer returns `nil` unless fed an absolute path; thus to
|
||
initialize from user-input that may contain a relative path use this form:
|
||
|
||
```swift
|
||
let path = Path(userInput) ?? Path.cwd/userInput
|
||
```
|
||
|
||
This is explicit, not hiding anything that code-review may miss and preventing
|
||
common bugs like accidentally creating `Path` objects from strings you did not
|
||
expect to be relative.
|
||
|
||
The initializer is nameless to be consistent with the equivalent operation for
|
||
converting strings to `Int`, `Float` etc. in the standard library.
|
||
|
||
## Initializing from known strings
|
||
|
||
There’s no need to use the optional initializer in general if you have known
|
||
strings that you need to be paths:
|
||
|
||
```swift
|
||
let absolutePath = "/known/path"
|
||
let path1 = Path.root/absolutePath
|
||
|
||
let pathWithoutInitialSlash = "known/path"
|
||
let path2 = Path.root/pathWithoutInitialSlash
|
||
|
||
assert(path1 == path2)
|
||
|
||
let path3 = Path(absolutePath)! // at your options
|
||
|
||
assert(path2 == path3)
|
||
|
||
// be cautious:
|
||
let path4 = Path(pathWithoutInitialSlash)! // CRASH!
|
||
```
|
||
|
||
## Extensions
|
||
|
||
We have some extensions to Apple APIs:
|
||
|
||
```swift
|
||
let bashProfile = try String(contentsOf: Path.home/".bash_profile")
|
||
let history = try Data(contentsOf: Path.home/".history")
|
||
|
||
bashProfile += "\n\nfoo"
|
||
|
||
try bashProfile.write(to: Path.home/".bash_profile")
|
||
|
||
try Bundle.main.resources.join("foo").copy(to: .home)
|
||
```
|
||
|
||
## Directory listings
|
||
|
||
`Path` provides `ls()` to list files. Like `ls` it is not recursive and doesn’t
|
||
list hidden files.
|
||
|
||
```swift
|
||
for path in Path.home.ls() {
|
||
//…
|
||
}
|
||
|
||
for path in Path.home.ls() where path.isFile {
|
||
//…
|
||
}
|
||
|
||
for path in Path.home.ls() where path.mtime > yesterday {
|
||
//…
|
||
}
|
||
|
||
let dirs = Path.home.ls().directories
|
||
// ^^ directories that *exist*
|
||
|
||
let files = Path.home.ls().files
|
||
// ^^ files that both *exist* and are *not* directories
|
||
|
||
let swiftFiles = Path.home.ls().files.filter{ $0.extension == "swift" }
|
||
|
||
let includingHiddenFiles = Path.home.ls(.a)
|
||
```
|
||
|
||
**Note** `ls()` does not throw, instead outputing a warning to the console if it
|
||
fails to list the directory. The rationale for this is weak, please open a
|
||
ticket for discussion.
|
||
|
||
`Path` provides `find()` for recursive listing:
|
||
|
||
```swift
|
||
for path in Path.home.find() {
|
||
// descends all directories, and includes hidden files by default
|
||
// so it behaves the same as the terminal command `find`
|
||
}
|
||
```
|
||
|
||
It is configurable:
|
||
|
||
```swift
|
||
for path in Path.home.find().depth(max: 1).extension("swift").type(.file).hidden(false) {
|
||
//…
|
||
}
|
||
```
|
||
|
||
It can be controlled with a closure syntax:
|
||
|
||
```swift
|
||
Path.home.find().depth(2...3).execute { path in
|
||
guard path.basename() != "foo.lock" else { return .abort }
|
||
if path.basename() == ".build", path.isDirectory { return .skip }
|
||
//…
|
||
return .continue
|
||
}
|
||
```
|
||
|
||
Or get everything at once as an array:
|
||
|
||
```swift
|
||
let paths = Path.home.find().map(\.self)
|
||
```
|
||
|
||
# `Path.swift` is robust
|
||
|
||
Some parts of `FileManager` are not exactly idiomatic. For example
|
||
`isExecutableFile` returns `true` even if there is no file there, it is instead
|
||
telling you that *if* you made a file there it *could* be executable. Thus we
|
||
check the POSIX permissions of the file first, before returning the result of
|
||
`isExecutableFile`. `Path.swift` has done the leg-work for you so you can just
|
||
get on with it and not have to worry.
|
||
|
||
There is also some magic going on in Foundation’s filesystem APIs, which we look
|
||
for and ensure our API is deterministic, eg. [this test].
|
||
|
||
[this test]: https://github.com/astzweig/swiftpm-pathkit/blob/master/Tests/PathTests/PathTests.swift#L539-L554
|
||
|
||
# `PathKit` is properly cross-platform
|
||
|
||
`FileManager` on Linux has a lot of pitfalls, which this library works around on.
|
||
|
||
# Rules & Caveats
|
||
|
||
`Paths` are just (normalized) string representations, there *might not* be a real
|
||
file there.
|
||
|
||
```swift
|
||
Path.home/"b" // => /Users/someuser/b
|
||
|
||
// joining multiple strings works as you’d expect
|
||
Path.home/"b"/"c" // => /Users/someuser/b/c
|
||
|
||
// joining multiple parts simultaneously is fine
|
||
Path.home/"b/c" // => /Users/someuser/b/c
|
||
|
||
// joining with absolute paths omits prefixed slash
|
||
Path.home/"/b" // => /Users/someuser/b
|
||
|
||
// joining with .. or . works as expected
|
||
Path.home.foo.bar.join("..") // => /Users/someuser/foo
|
||
Path.home.foo.bar.join(".") // => /Users/someuser/foo/bar
|
||
|
||
// though note that we provide `.parent`:
|
||
Path.home.foo.bar.parent // => /Users/someuser/foo
|
||
|
||
// of course, feel free to join variables:
|
||
let b = "b"
|
||
let c = "c"
|
||
Path.home/b/c // => /Users/someuser/b/c
|
||
|
||
// tilde is not special here
|
||
Path.root/"~b" // => /~b
|
||
Path.root/"~/b" // => /~/b
|
||
|
||
// but is here
|
||
Path("~/foo")! // => /Users/someuser/foo
|
||
|
||
// this works provided the user `Guest` exists
|
||
Path("~Guest") // => /Users/Guest
|
||
|
||
// but if the user does not exist
|
||
Path("~foo") // => nil
|
||
|
||
// paths with .. or . are resolved
|
||
Path("/foo/bar/../baz") // => /foo/baz
|
||
|
||
// symlinks are not resolved
|
||
Path.root.bar.symlink(as: "foo")
|
||
Path("/foo") // => /foo
|
||
Path.root.foo // => /foo
|
||
|
||
// unless you do it explicitly
|
||
try Path.root.foo.readlink() // => /bar
|
||
// `readlink` only resolves the *final* path component,
|
||
// thus use `realpath` if there are multiple symlinks
|
||
```
|
||
|
||
*PathKit* has the general policy that if the desired end result preexists,
|
||
then it’s a noop:
|
||
|
||
* If you try to delete a file, but the file doesn't exist, nothing happens.
|
||
* If you try to make a directory and it already exists, nothing happens.
|
||
* If you call `readlink` on a non-symlink, we return `self`
|
||
|
||
However notably if you try to copy or move a file without specifying `overwrite`
|
||
and the file already exists at the destination and is identical, *PathKit* doesn't check
|
||
for that as the check was deemed too expensive to be worthwhile.
|
||
|
||
## Symbolic links
|
||
|
||
* Two paths may represent the same *resolved* path yet not be equal due to
|
||
symlinks in such cases you should use `realpath` on both first if an
|
||
equality check is required.
|
||
* There are several symlink paths on Mac that are typically automatically
|
||
resolved by Foundation, eg. `/private`, we attempt to do the same for
|
||
functions that you would expect it (notably `realpath`), we *do* the same
|
||
for `Path.init`, but *do not* if you are joining a path that ends up being
|
||
one of these paths, (eg. `Path.root.join("var/private')`).
|
||
|
||
If a `Path` is a symlink but the destination of the link does not exist `exists`
|
||
returns `false`. This seems to be the correct thing to do since symlinks are
|
||
meant to be an abstraction for filesystems. To instead verify that there is
|
||
no filesystem entry there at all check if `type` is `nil`.
|
||
|
||
|
||
## There is no change directory functionality
|
||
|
||
Changing directory is dangerous, you should *always* try to avoid it and thus
|
||
we don’t even provide the method. If you are executing a sub-process then
|
||
use `Process.currentDirectoryURL` to change *its* working directory when it
|
||
executes.
|
||
|
||
If you must change directory then use `FileManager.changeCurrentDirectory` as
|
||
early in your process as *possible*. Altering the global state of your app’s
|
||
environment is fundamentally dangerous creating hard to debug issues that
|
||
you won‘t find for potentially *years*.
|
||
|
||
# I thought I should only use `URL`s?
|
||
|
||
Apple recommend this because they provide a magic translation for
|
||
[file-references embodied by URLs][file-refs], which gives you URLs like so:
|
||
|
||
file:///.file/id=6571367.15106761
|
||
|
||
Therefore, if you are not using this feature you are fine. If you have URLs the
|
||
correct way to get a `Path` is:
|
||
|
||
```swift
|
||
if let path = Path(url: url) {
|
||
/*…*/
|
||
}
|
||
```
|
||
|
||
Our initializer calls `path` on the URL which resolves any reference to an
|
||
actual filesystem path, however we also check the URL has a `file` scheme first.
|
||
|
||
[file-refs]: https://developer.apple.com/documentation/foundation/nsurl/1408631-filereferenceurl
|
||
|
||
# In defense of our naming scheme
|
||
|
||
Chainable syntax demands short method names, thus we adopted the naming scheme
|
||
of the terminal, which is absolutely not very “Apple” when it comes to how they
|
||
design their APIs, however for users of the terminal (which *surely* is most
|
||
developers) it is snappy and familiar.
|
||
|
||
# Installation
|
||
|
||
SwiftPM:
|
||
|
||
```swift
|
||
package.append(
|
||
.package(url: "https://github.com/astzweig/swiftpm-pathkit.git", from: "1.0.0")
|
||
)
|
||
|
||
package.targets.append(
|
||
.target(name: "Foo", dependencies: [
|
||
.product(name: "Path", package: "swiftpm-pathkit")
|
||
])
|
||
)
|
||
```
|
||
|
||
# Naming Conflicts with `SwiftUI.Path`, etc.
|
||
|
||
We have a typealias of `PathStruct` you can use instead.
|
||
|
||
# Alternatives
|
||
|
||
* [Pathos](https://github.com/dduan/Pathos) by Daniel Duan
|
||
* [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-languages]: https://img.shields.io/badge/swift-4.2%20%7C%205.x-orange.svg
|
||
[docs]: https://mxcl.dev/Path.swift/Structs/Path.html
|