Path.swift badge-platforms badge-languages badge-ci badge-jazzy badge-codecov badge-version

一个专注于开发者体验和稳健最终结果的文件系统路径库。

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/mxcl/Library/Preferences

// a practical example: installing a helper executable
try Bundle.resources.helper.copy(into: Path.root.usr.local.bin).chmod(0o500)

我们像 Swift 一样强调安全性和正确性,并且(再次像 Swift 一样),我们提供经过深思熟虑且全面(但简洁)的 API。

赞助者 @mxcl

大家好,我是 Max Howell,我编写了很多开源软件——通常占用我大量的空闲时间 👨🏻‍💻。赞助帮助我证明创建和维护新的开源软件是值得的。谢谢。

赞助者 @mxcl.

手册

我们的在线 API 文档涵盖了我们 100% 的公共 API,并为新版本自动更新。

Codable

我们支持您期望的 Codable

try JSONEncoder().encode([Path.home, Path.home/"foo"])
[
    "/Users/mxcl",
    "/Users/mxcl/foo",
]

尽管我们建议编码相对路径‡

let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = Path.home
encoder.encode([Path.home, Path.home/"foo", Path.home/"../baz"])
[
    "",
    "foo",
    "../baz"
]

注意 如果您使用此键集进行编码,则必须也使用该键集进行解码

let decoder = JSONDecoder()
decoder.userInfo[.relativePath] = Path.home
try decoder.decode(from: data)  // would throw if `.relativePath` not set

‡ 如果您要将文件保存到系统提供的位置,例如“文档”,那么目录可能会在 Apple 的选择下更改,或者如果用户更改了他们的用户名。使用相对路径还为您提供了未来的灵活性,可以轻松更改文件的存储位置。

动态成员

我们支持 @dynamicMemberLookup

let ls = Path.root.usr.bin.ls  // => /usr/bin/ls

我们仅为“起始”函数提供此功能,例如 Path.homeBundle.path。这是因为我们发现在实践中很容易编写不正确的代码,因为如果我们允许任意变量将任何命名属性作为有效语法,那么所有内容都会编译。我们现在拥有的功能是您最需要的功能,但(在运行时)危险性要低得多。

Pathish

PathDynamicPath(例如 Path.root 的结果)都符合 Pathish 协议,该协议包含所有路径函数。因此,如果您从两者混合创建对象,则需要创建泛型函数或首先将任何 DynamicPath 转换为 Path

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

这很不方便,但就 Swift 目前的情况而言,我们想不到任何可以提供帮助的方法。

从用户输入初始化

除非馈送绝对路径,否则 Path 初始化器返回 nil;因此,要从可能包含相对路径的用户输入进行初始化,请使用此形式

let path = Path(userInput) ?? Path.cwd/userInput

这是显式的,没有隐藏代码审查可能会遗漏的任何内容,并防止常见的错误,例如意外地从您不希望是相对路径的字符串创建 Path 对象。

我们的初始化器是无名的,以便与标准库中将字符串转换为 IntFloat 等的等效操作保持一致。

从已知字符串初始化

如果您有需要作为路径的已知字符串,则通常无需使用可选的初始化器

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!

扩展

我们对 Apple API 进行了一些扩展

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)

目录列表

我们提供 ls(),之所以这样称呼是因为它的行为类似于终端 ls 函数,因此名称暗示了它的行为,即它不是递归的,并且不列出隐藏文件。

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)

注意 ls() 不会抛出错误,而是在无法列出目录时向控制台输出警告。此理由很薄弱,请打开 issue 进行讨论。

我们提供 find() 用于递归列表

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`
}

它是可配置的

for path in Path.home.find().depth(max: 1).extension("swift").type(.file).hidden(false) {
    //…
}

它可以通过闭包语法控制

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
}

或者一次性获取所有内容作为数组

let paths = Path.home.find().map(\.self)

Path.swift 是健壮的

FileManager 的某些部分并非完全符合习惯用法。例如,即使那里没有文件,isExecutableFile 也会返回 true,它实际上是在告诉您,如果您在那里创建了一个文件,它可能是可执行的。因此,在返回 isExecutableFile 的结果之前,我们会先检查文件的 POSIX 权限。 Path.swift 已经为您完成了这些基础工作,因此您可以继续进行工作,而无需担心。

Foundation 的文件系统 API 中也有一些魔力,我们会寻找这些魔力并确保我们的 API 是确定性的,例如此测试

Path.swift 是真正的跨平台

Linux 上的 FileManager 漏洞百出。我们已经发现了这些漏洞,并在必要时进行了规避。

规则和注意事项

路径只是(规范化的)字符串表示形式,那里可能没有真实的文件。

Path.home/"b"      // => /Users/mxcl/b

// joining multiple strings works as you’d expect
Path.home/"b"/"c"  // => /Users/mxcl/b/c

// joining multiple parts simultaneously is fine
Path.home/"b/c"    // => /Users/mxcl/b/c

// joining with absolute paths omits prefixed slash
Path.home/"/b"     // => /Users/mxcl/b

// joining with .. or . works as expected
Path.home.foo.bar.join("..")  // => /Users/mxcl/foo
Path.home.foo.bar.join(".")   // => /Users/mxcl/foo/bar

// though note that we provide `.parent`:
Path.home.foo.bar.parent      // => /Users/mxcl/foo

// 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/mxcl/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

Path.swift 的一般策略是,如果所需的最终结果已经存在,那么它就是空操作

但值得注意的是,如果您尝试复制或移动文件,但未指定 overwrite 且目标位置已存在相同的文件,我们不会检查,因为该检查被认为太昂贵而不值得。

符号链接

如果 Path 是符号链接,但链接的目标不存在,则 exists 返回 false。这似乎是正确的做法,因为符号链接旨在作为文件系统的抽象。要改为验证那里根本没有文件系统条目,请检查 type 是否为 nil

我们不提供更改目录功能

更改目录是危险的,您应该始终尝试避免它,因此我们甚至不提供该方法。如果您要执行子进程,请使用 Process.currentDirectoryURL 在执行时更改工作目录。

如果您必须更改目录,请在您的进程中尽可能早地使用 FileManager.changeCurrentDirectory。更改应用程序环境的全局状态从根本上来说是危险的,会造成难以调试的问题,这些问题可能在多年后才会被发现。

我以为我应该只使用 URL

Apple 建议这样做是因为他们为 URL 体现的文件引用 提供了神奇的转换,这为您提供了如下 URL

file:///.file/id=6571367.15106761

因此,如果您不使用此功能,则可以正常使用。如果您有 URL,则获取 Path 的正确方法是

if let path = Path(url: url) {
    /*…*/
}

我们的初始化器在 URL 上调用 path,这会解析对实际文件系统路径的任何引用,但我们也会首先检查 URL 是否具有 file 方案。

为我们的命名方案辩护

链式语法要求方法名称简短,因此我们采用了终端的命名方案,当涉及到他们如何设计其 API 时,这绝对不是很“Apple”的方式,但是对于终端用户(肯定是大多数开发人员),它简洁明了且很熟悉。

安装

SwiftPM

package.append(
    .package(url: "https://github.com/mxcl/Path.swift.git", from: "1.0.0")
)

package.targets.append(
    .target(name: "Foo", dependencies: [
        .product(name: "Path", package: "Path.swift")
    ])
)

CocoaPods

pod 'Path.swift', '~> 1.0.0'

Carthage

等待中:@Carthage#1945

SwiftUI.Path 等的命名冲突。

我们有一个您可以改用的 PathStruct 类型别名。

替代方案