Path.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/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,我写过很多开源软件——通常是我的很大一部分空闲时间 👨💻。
用户手册
我们的在线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.home
或Bundle.path
。这是因为我们发现实际上很容易编写错误的代码,因为如果我们允许任意变量以任何命名的属性有效语法,则所有内容都会进行编译。这是你大部分时候想要的东西,但(潜在地)在运行时更少(危险)。
Pathish
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()`》不会抛出异常,而是在无法列出目录时向控制台输出警告。这种做法的理由不足,请创建工单进行讨论。
我们为递归列表提供了《find()`》。
for path in Path.home.find() {
// descends all directories, and includes hidden files
// so it behaves the same as the terminal command `find`
}
它是可配置的
for path in Path.home.find().depth(max: 1).extension("swift").type(.file) {
//…
}
可以用闭包语法来控制它
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
的一般策略是,如果期望的结果已经存在,则不执行任何操作。
- 如果您尝试删除一个文件,但文件不存在,则我们不执行任何操作。
- 如果您尝试创建一个目录,但它已经存在,则我们不执行任何操作。
- 如果您对非符号链接调用
readlink
,我们将返回self
。
然而,如果您尝试不指定 overwrite
选项而复制或移动文件,并且目标位置的文件已经存在且内容相同,我们不会进行检查,因为这被认为成本太高而得不偿失。
符号链接
- 两个路径可能表示相同的(已解析的)路径,但由于符号链接而内容不等。在这种情况下,您应该在需要比较相等时,先对两个路径使用
realpath
。 - 在 Mac 上有几个符号链接路径通常是 Foundation 自动解析的,例如
/private
,我们尝试在您期望的函数上做同样的事情(特别是realpath
),对于Path.init
我们也这样做,但如果您正在拼接的结果最终是这些路径之一(例如Path.root.join("var/private')
),则我们不这样做。
如果路径是符号链接但链接的目标不存在,则exists
返回false
。这似乎是正确的行为,因为符号链接旨在作为文件系统的抽象。要验证那里根本没有任何文件系统条目,请检查type
是否为nil
。
我们不提供更改目录的功能
更改目录很危险,您应该始终尝试避免它,因此我们甚至不提供此方法。如果您正在执行子进程,请在执行时使用Process.currentDirectoryURL
更改其工作目录。
如果您必须更改目录,请在进程的尽可能早的时候使用FileManager.changeCurrentDirectory
。更改应用程序环境的全局状态本质上很危险,可能会创建难以调试的问题,这些问题的出现可能要过几年。
URL
?
我以为我应该只使用苹果公司推荐这样做,因为它们为由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"))
CocoaPods
pod 'Path.swift', '~> 1.0.0'
Carthage
等待: @Carthage#1945。