一个为 Apple 的 Swift 语言设计的类型安全的路径库。
我一直不太喜欢 Foundation 的 FileManager
。一般来说,Foundation 在跨平台使用时结果并不一致(Linux 支持/稳定性对于我使用 Swift 的大多数场景来说很重要),并且 FileManager
本身缺乏类型安全性和易用性,而这些正是大多数 Swift API 应该具备的(FileAttributeKey
有人喜欢吗?)。
所以我构建了 Pathman! 这是第一个围绕底层 C API 构建的类型安全的 Swift 路径库(其他所有库都只是 FileManager
的包装器,使其在 Swift 中使用起来更方便)。
FileManager
抛出的晦涩的 NSError
sData
、Date
和 URL
类型FileAttributeKey
s 吗?将其添加到您的 Package.swift 依赖项中
.package(url: "https://github.com/Ponyboy47/Pathman.git", from: "0.20.1")
目前有 3 种不同的路径类型:GenericPath、FilePath 和 DirectoryPath
// Paths can be initialized from Strings, Arrays, or Slices
let genericString = GenericPath("/tmp")
let genericArray = GenericPath(["/", "tmp"])
let genericSlice = GenericPath(["/", "tmp", "test"].dropLast())
// FilePaths and DirectoryPaths can be initialized the same as a GenericPath
// Beware that you do your own validation that the path matches it's type.
// Things like this are possible and will lead to errors:
let file = FilePath("/tmp/")
let directory = DirectoryPath("/tmp/")
// Paths conform to the StatDelegate protocol, which means that they use the
// `stat` utility to gather information about the file (ie: size, ownership,
// modify time, etc)
// NOTE: Certain properties are only available for paths that exist
/// The system id of the path
var id: DeviceID
/// The inode of the path
var inode: Inode
/// The type of the path, if it exists
var type: PathType
/// Whether the path exists
var exists: Bool
/// Whether the path exists and is a file
var isFile: Bool
/// Whether the path exists and is a directory
var isDirectory: Bool
/// Whether the path exists and is a link
var isLink: Bool
/// The URL representation of the path
var url: URL
/// The permissions of the path
var permissions: FileMode
/// The user id of the user that owns the path
var owner: UID
// The name of the user that owns the path
var ownerName: String?
/// The group id of the user that owns the path
var group: GID
/// The name of the group that owns the path
var groupName: String?
/// The device id (if special file)
var device: DeviceID
/// The total size, in bytes
var size: OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The blocksize for filesystem I/O
var blockSize: BlockSize
/// The number of 512B block allocated
var blocks: OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The parent directory of the path
var parent: DirectoryPath
/// The pieces that make up the path
var components: [String]
/// The final piece of the path (filename or directory name)
var lastComponent: String?
/// The final piece of the path with the extension stripped off
var lastComponentWithoutExtension: String?
/// The extension of the path
var extension: String?
/// The last time the path was accessed
var lastAccess: Date
/// The last time the path was modified
var lastModified: Date
/// The last time the path had a status change
var lastAttributeChange: Date
/// The time when the path was created (macOS only)
var creation: Date
let file = FilePath("/tmp/test")
let openFile: OpenFile = try file.open(mode: "r+")
// Open files can be written to or read from (depending on the permissions used above)
let content: String = try openFile.read()
try openFile.write(content)
let dir = DirectoryPath("/tmp")
let openDir: OpenDirectory = try dir.open()
// Open directories can be traversed
let children = openDir.children()
// Recursively traversing directories requires opening sub-directories and may throw errors
let recursiveChildren = try openDir.recursiveChildren()
路径也可以在提供的闭包的持续时间内打开
let dir = DirectoryPath("/tmp")
try dir.open() { openDirectory in
let children = openDirectory.children()
print(children)
}
var file = FilePath("/tmp/test")
// Creates a file with the write permissions and returns the opened file
let openFile: OpenFile = try file.create(mode: FileMode(owner: .readWriteExecute, group: .readWrite, other: .none))
如果您还需要创建中间路径
var file = FilePath("/tmp/testdir/test")
let openFile: OpenFile = try file.create(options: .createIntermediates)
其 Open<...> 变体符合 Writable 的路径可以使用预定的内容创建
var file = FilePath("/tmp/test")
try file.create(contents: "Hello World")
print(try file.read()) // "Hello World"
路径也可以在提供的闭包的持续时间内打开
var file = FilePath("/tmp/test")
try file.create() { openFile in
try openFile.write("Hello world")
let contents: String = try openFile.read(from: .beginning)
print(contents) // Hello World
}
这对于所有路径都是一样的
var file = FilePath("/tmp/test")
try file.delete()
var dir = DirectoryPath("/tmp/test")
try dir.recursiveDelete()
注意:对此要非常小心,因为它无法撤消(就像 rm -rf
一样)。
let file = FilePath("/tmp/test")
// All of the following operations are available on both a FilePath and an OpenFile
// Read the whole file
let contents: String = try file.read()
// Read up to 1024 bytes
let contents: String = try file.read(bytes: 1024)
// Read content as ascii characters instead of utf8
let contents: String = try file.read(encoding: .ascii)
// Read to the end, but starting at 1024 bytes from the beginning of the file
let contents: String = try file.read(from: Offset(from: .beginning, bytes: 1024))
// Read the last 1024 bytes from of the file using the ascii encoding
let contents: String = try file.read(from: Offset(from: .end, bytes: -1024), bytes: 1024, encoding: .ascii)
注意
从 FilePath
读取仅用于对文件执行单个读取操作,因为它会打开文件,从中读取,然后关闭文件。如果您要多次读取文件,最好使用 try file.open(permissions: .read)
打开它,然后根据需要多次读取。
每次读取后,文件偏移量都会更新。如果您希望再次从头开始读取,请传递偏移量 Offset(from: .beginning, bytes: 0)
。
如果文件是使用 .append
标志打开的,那么任何传递的偏移量都将被忽略,并且文件偏移量在任何写入操作之前都会移动到文件末尾。
每个读取操作都可能返回 String
或 Data
,因此请确保您要存储到的对象是显式类型的,否则,您将遇到歧义的用例。
let file = FilePath("/tmp/test")
// All of the following operations are available on both a FilePath and an OpenFile
// Write a string at the current file position
try file.write("Hello world")
// Write an ascii string at the end of the file
try file.write("Goodbye", at: Offset(from: .end, bytes: 0), using: .ascii)
注意:您也可以将 Data
实例传递给 write 函数,而不是带有编码的 String
。
// Writing files is buffered by default. If you expect to use a file
// immediately after writing to it then be sure to flush the buffer
let file = FilePath("/tmp/test")
let openFile = try file.open(mode: "w+")
try openFile.write("Hello world!")
try openFile.flush()
try openFile.rewind()
let contents = openFile.read()
// You may also change the buffering mode for the file
try openFile.setBuffer(mode: .line) // Flushes after each newline
try openFile.setBuffer(mode: .none) // Flushes immediately
try openFile.setBuffer(mode: .full(size: 1024)) // Flushes after 1024 bytes are written
注意:默认缓冲是基于您操作系统 BUFSIZ
变量的完全缓冲
let dir = DirectoryPath("/tmp")
let children = try dir.children()
// This same operation is safe, assuming you've already opened the directory
let openDir = try dir.open()
let children = openDir.children()
print(children.files)
print(children.directories)
print(children.other)
let dir = DirectoryPath("/tmp")
let children = try dir.recursiveChildren()
// This operation is still unsafe, even if the directory is already opened (Because you still might have to open sub-directories, which is unsafe)
let openDir = try dir.open()
let children = try openDir.recursiveChildren()
print(children.files)
print(children.directories)
print(children.other)
// You can optionally specify a depth to only get so many directories
// This will go no more than 5 directories deep before returning
let children = try dir.recursiveChildren(depth: 5)
// Both .children() and .recursiveChildren() support getting hidden files/directories (files that begin with a '.')
let children = try dir.children(options: .includeHidden)
let recursiveChildren = try dir.recursiveChildren(depth: 5, options: .includeHidden)
var path = GenericPath("/tmp")
// Owner/Group can be changed separately or together
try path.change(owner: "ponyboy47")
try path.change(group: "ponyboy47")
try path.change(owner: "ponyboy47", group: "ponyboy47")
// You can also set them through the corresponding properties:
// NOTE: Setting them this way is NOT guarenteed to succeed and any errors
// thrown are ignored. If you need a reliant way to set path ownership then you
// should call the `change` method directly
path.owner = 0
path.group = 1000
path.ownerName = "root"
path.groupName = "wheel"
// If you have a DirectoryPath, then changes can be made recursively:
var dir = DirectoryPath(path)
try dir.recursiveChange(owner: "ponyboy47")
var path = GenericPath("/tmp")
// Owner/Group/Others permissions can each be changed separately or in any combination (permissions that are not specified are not changed)
try path.change(owner: [.read, .write, .execute]) // Only changes the owner's permissions
try path.change(group: .readWrite) // Only changes the group's permissions
try path.change(others: .none) // Only changes other's permissions
try path.change(ownerGroup: .all) // Only changes owner's and group's permissions
try path.change(groupOthers: .read) // Only changes group's and other's permissions
try path.change(ownerOthers: .writeExecute) // Only changes owner's and other's permissions
try path.change(ownerGroupOthers: .all) // Changes all permissions
// You can also change the uid, gid, and sticky bits
try path.change(bits: .uid)
try path.change(bits: .gid)
try path.change(bits: .sticky)
try path.change(bits: [.uid, .sticky])
try path.change(bits: .all)
// You can also set them through the permissions property:
// NOTE: Setting them this way is NOT guarenteed to succeed and any errors
// thrown are ignored. If you need a reliant way to set path ownership then you
// should call the `change` method directly
path.permissions = FileMode(owner: .readWriteExecute, group: .readWrite, others: .read)
path.permissions.owner = .readWriteExecute
path.permissions.group = .readWrite
path.permissions.others = .read
path.permissions.bits = .none
// If you have a DirectoryPath, then changes can be made recursively:
var dir = DirectoryPath(path)
try dir.recursiveChange(owner: .readWriteExecute, group: .readWrite, others: .read)
var path = GenericPath("/tmp/testFile")
// Both of these things will move testFile from /tmp/testFile to ~/testFile
try path.move(to: DirectoryPath.home! + "testFile")
try path.move(into: DirectoryPath.home!)
// This renames a file in place
try path.rename(to: "newTestFile")
let globData = try glob(pattern: "/tmp/*")
// Just like getting a directories children:
print(globData.files)
print(globData.directories)
print(globData.other)
// You can also glob from a DirectoryPath
let home = DirectoryPath.home
let globData = try home.glob("*.swift")
print(globData.files)
print(globData.directories)
print(globData.other)
let tmpFile = try FilePath.temporary()
// /tmp/vDjKM1C
let tmpDir = try DirectoryPath.temporary()
// /tmp/rYcznHQ
// You can optionally specify a prefix for the path name
let tmpFile = try FilePath.temporary(prefix: "com.pathman.")
// /tmp/com.pathman.gHyiZq
// You can optionally specify a base directory where the temporary path will be stored
let tmpDirectory = try DirectoryPath.temporary(base: DirectoryPath("/path/to/my/tmp")!, prefix: "com.pathman.")
// /path/to/my/tmp/com.pathman.2eH4iB
// When creating a temporary path with a closure, the path of the temporary
// file is returned instead of an Opened path
let tmpFile: FilePath = try FilePath.temporary() { openFile in
try openFile.write("Hello World")
}
// You can also pass the .deleteOnCompletion option to the .temporary()
// function in order to delete the temporary path after the closure exits
// NOTE: This will recursively delete the temporary path if it is a DirectoryPath
try FilePath.temporary(options: .deleteOnCompletion) { openFile in
try openFile.write("Hello World")
}
// You can link to an existing path
let dir = DirectoryPath("/tmp")
// Creates a soft/symbolic link to dir at the specified path
// All 3 of the following lines produce the same type of link
let link = try dir.link(at: "~/tmpDir.link")
let link = try dir.link(at: "~/tmpDir.symbolic", type: .symbolic)
let link = try dir.link(at: "~/tmpDir.soft", type: .soft)
// Creates a hard link to dir at the specified path
let link = try dir.link(at: "~/tmpDir.hard", type: .hard)
let linkedFile = FilePath("/path/to/link/location")
// Creates a soft/symbolic link to dir at the specified path
// All 3 of the following lines produce the same type of link
let link = try linkedFile.link(from: "/path/to/link/target")
let link = try linkedFile.link(from: "/path/to/link/target", type: .symbolic)
let link = try linkedFile.link(from: "/path/to/link/target", type: .soft)
// Creates a hard link to dir at the specified path
let link = try linkedFile.link(from: "/path/to/link/target", type: .hard)
Pathman 使用 .symbolic/.soft 链接作为默认值,但可以更改此设置。
Pathman.defaultLinkType = .hard
let file = FilePath("/path/to/file")
let copyPath = FilePath("/path/to/copy")
// Both these lines would result in the same thing
try file.copy(to: copyPath)
try file.copy(to: "/path/to/copy")
let dir = DirectoryPath("/path/to/directory")
let copyPath = DirectoryPath("/path/to/copy")
// Both these lines would result in the same thing
try dir.copy(to: copyPath)
try dir.copy(to: "/path/to/copy")
// NOTE: Copying directories will fail if the directory is not empty, so pass
// the recursive option to the copy call in order to sucessfully copy non empty
// directories
try dir.copy(to: copyPath, options: .recursive)
// NOTE: You may also include hidden files with the includeHidden option
try dir.copy(to: copyPath, options: [.recursive, .includeHidden])