一个专注于开发者体验和稳健最终结果的文件系统路径库。
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。
大家好,我是 Max Howell,我编写了很多开源软件——通常占用我大量的空闲时间 👨🏻💻。赞助帮助我证明创建和维护新的开源软件是值得的。谢谢。
我们的在线 API 文档涵盖了我们 100% 的公共 API,并为新版本自动更新。
我们支持您期望的 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.home
或 Bundle.path
。这是因为我们发现在实践中很容易编写不正确的代码,因为如果我们允许任意变量将任何命名属性作为有效语法,那么所有内容都会编译。我们现在拥有的功能是您最需要的功能,但(在运行时)危险性要低得多。
Path
和 DynamicPath
(例如 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
对象。
我们的初始化器是无名的,以便与标准库中将字符串转换为 Int
、Float
等的等效操作保持一致。
如果您有需要作为路径的已知字符串,则通常无需使用可选的初始化器
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)
FileManager
的某些部分并非完全符合习惯用法。例如,即使那里没有文件,isExecutableFile
也会返回 true
,它实际上是在告诉您,如果您在那里创建了一个文件,它可能是可执行的。因此,在返回 isExecutableFile
的结果之前,我们会先检查文件的 POSIX 权限。 Path.swift
已经为您完成了这些基础工作,因此您可以继续进行工作,而无需担心。
Foundation 的文件系统 API 中也有一些魔力,我们会寻找这些魔力并确保我们的 API 是确定性的,例如此测试。
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 的一般策略是,如果所需的最终结果已经存在,那么它就是空操作
readlink
,我们将返回 self
但值得注意的是,如果您尝试复制或移动文件,但未指定 overwrite
且目标位置已存在相同的文件,我们不会检查,因为该检查被认为太昂贵而不值得。
realpath
。/private
,我们尝试对您期望它的函数执行相同的操作(特别是 realpath
),我们对 Path.init
执行相同的操作,但如果您要连接最终成为这些路径之一的路径(例如 Path.root.join("var/private')
),则不执行相同的操作。如果 Path
是符号链接,但链接的目标不存在,则 exists
返回 false
。这似乎是正确的做法,因为符号链接旨在作为文件系统的抽象。要改为验证那里根本没有文件系统条目,请检查 type
是否为 nil
。
更改目录是危险的,您应该始终尝试避免它,因此我们甚至不提供该方法。如果您要执行子进程,请使用 Process.currentDirectoryURL
在执行时更改其工作目录。
如果您必须更改目录,请在您的进程中尽可能早地使用 FileManager.changeCurrentDirectory
。更改应用程序环境的全局状态从根本上来说是危险的,会造成难以调试的问题,这些问题可能在多年后才会被发现。
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。
我们有一个您可以改用的 PathStruct
类型别名。