A type-safe path library for Apple's Swift language.
I have never been a big fan of Foundation's FileManager
. Foundation in general has inconsistent results when used cross-platform (Linux support/stability is important for most of the things for which I use Swift) and FileManager
itself lacks the type-safety and ease-of-use that most Swift API's are expected to have (FileAttributeKey
anyone?).
So I built Pathman! The first type-safe swift path library built around the lower level C API's (everything else out there is just a wrapper around FileManager
to make it nicer to use in Swift).
- Type safety
- File paths are different that directory paths and should be treated as such
- Extensibility
- Everything is based around protocols or extensible classes so that others can create new path types (ie: sockets)
- Error Handling
- There are an extensive number of errors so that when something goes wrong you can get the most relevant error message possible (see Errors.swift)
- No more dealing with obscure
NSError
s whenFileManager
throws
- No more dealing with obscure
- There are an extensive number of errors so that when something goes wrong you can get the most relevant error message possible (see Errors.swift)
- Minimal Foundation
- I avoid using Foundation as much as possible, because it is not as stable on Linux as it is on Apple platforms (yet) and the results for some APIs are inconsistent between Linux and macOS
- Currently, I only use Foundation for the
Data
,Date
, andURL
types
- Ease of Use
- No clunky interface just to get attributes of a path
- Was anyone ever a fan of
FileAttributeKey
s?
- Was anyone ever a fan of
- No clunky interface just to get attributes of a path
- Expose low-level control with high-level safety built-in
- Swift 5.0
- Ubuntu
- macOS
Add this to your Package.swift dependencies:
.package(url: "https://github.com/Ponyboy47/Pathman.git", from: "0.20.1")
There are 3 different Path types right now: GenericPath, FilePath, and 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()
Paths may also be opened for the duration of a provided closure:
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))
In the event you need to create the intermediate paths as well:
var file = FilePath("/tmp/testdir/test")
let openFile: OpenFile = try file.create(options: .createIntermediates)
Paths whose Open<...> variation conforms to Writable can be created with predetermined contents:
var file = FilePath("/tmp/test")
try file.create(contents: "Hello World")
print(try file.read()) // "Hello World"
Paths may also be opened for the duration of a provided closure:
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
}
This is the same for all paths
var file = FilePath("/tmp/test")
try file.delete()
var dir = DirectoryPath("/tmp/test")
try dir.recursiveDelete()
NOTE: Be VERY cautious with this as it cannot be undone (just like 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)
NOTES:
Reading from a FilePath
is only intended to be used when performing a single read operation on a file since it will open the file, read from the file, and close the file. If you're going to read a file multiple times, then it would be best to open it (with try file.open(permissions: .read)
and then read it as much as you want.
The file offset is updated after each read. If you wish to read from the beginning again then pass an offset of Offset(from: .beginning, bytes: 0)
.
If the file was opened using the .append
flag then any offsets passed will be ignored and the file offset is moved to the end of the file before any write operations.
Each of the read operations may either return String
or Data
, so be sure the object you're storing into is explicitly typed, otherwise, you will have an ambiguous use-case.
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)
NOTE: You can also pass a Data
instance to the write function instead of a String
with an encoding.
// 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
NOTE: The default buffering is full buffering based on your OS's BUFSIZ
variable
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)
Hidden Files:
// 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 uses .symbolic/.soft links as the default, but this may be changed.
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])