{"id":13872141,"url":"https://github.com/Ponyboy47/Pathman","last_synced_at":"2025-07-16T01:33:09.271Z","repository":{"id":63920338,"uuid":"136391044","full_name":"Ponyboy47/Pathman","owner":"Ponyboy47","description":"Swift type-safe path, file, and directory library using POSIX C APIs","archived":false,"fork":false,"pushed_at":"2020-01-02T18:48:16.000Z","size":635,"stargazers_count":10,"open_issues_count":2,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-04-14T12:10:24.877Z","etag":null,"topics":["directory","file","files","filesystem","path","path-manipulation","swift","swift-package-manager","type-safety"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Ponyboy47.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-06-06T22:05:50.000Z","updated_at":"2024-04-14T12:10:24.878Z","dependencies_parsed_at":"2023-01-14T14:00:30.224Z","dependency_job_id":null,"html_url":"https://github.com/Ponyboy47/Pathman","commit_stats":null,"previous_names":[],"tags_count":31,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ponyboy47%2FPathman","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ponyboy47%2FPathman/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ponyboy47%2FPathman/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ponyboy47%2FPathman/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Ponyboy47","download_url":"https://codeload.github.com/Ponyboy47/Pathman/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225834368,"owners_count":17531470,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["directory","file","files","filesystem","path","path-manipulation","swift","swift-package-manager","type-safety"],"created_at":"2024-08-05T23:00:34.966Z","updated_at":"2024-11-23T19:31:39.467Z","avatar_url":"https://github.com/Ponyboy47.png","language":"Swift","funding_links":[],"categories":["Swift"],"sub_categories":[],"readme":"# Pathman\n\n[![Build Status](https://travis-ci.org/Ponyboy47/Pathman.svg?branch=master)](https://travis-ci.org/Ponyboy47/Pathman) [![codecov](https://codecov.io/gh/Ponyboy47/Pathman/branch/master/graph/badge.svg)](https://codecov.io/gh/Ponyboy47/Pathman) [![Maintainability](https://api.codeclimate.com/v1/badges/4bafeb6b0d65f0c57fa6/maintainability)](https://codeclimate.com/github/Ponyboy47/Pathman/maintainability) [![Current Version](https://img.shields.io/badge/version-0.20.1-blue.svg)](https://github.com/Ponyboy47/Pathman/releases/tag/0.20.1) ![Supported Platforms](https://img.shields.io/badge/platform-macOS%20%7C%20linux-lightgrey.svg) [![Language](https://img.shields.io/badge/language-swift-orange.svg)](https://swift.org) [![Language Version](https://img.shields.io/badge/swift%20version-5.0-blue.svg)](https://swift.org/download/) [![License](https://img.shields.io/badge/license-MIT-black.svg)](https://github.com/Ponyboy47/Pathman/blob/master/LICENSE)\u003cbr\u003e\nA type-safe path library for Apple's Swift language.\n\n## Motivation\nI 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?).\n\nSo 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).\n\n## Goals\n- Type safety\n  - File paths are different that directory paths and should be treated as such\n- Extensibility\n  - Everything is based around protocols or extensible classes so that others can create new path types (ie: sockets)\n- Error Handling\n  - 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)\n    - No more dealing with obscure `NSError`s when `FileManager` throws\n- Minimal Foundation\n  - 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\n  - Currently, I only use Foundation for the `Data`, `Date`, and `URL` types\n- Ease of Use\n  - No clunky interface just to get attributes of a path\n    - Was anyone ever a fan of `FileAttributeKey`s?\n- Expose low-level control with high-level safety built-in\n\n## Installation\n### Compatibility:\n- Swift 5.0\n- Ubuntu\n- macOS\n\n### Swift Package Manager:\nAdd this to your Package.swift dependencies:\n```swift\n.package(url: \"https://github.com/Ponyboy47/Pathman.git\", from: \"0.20.1\")\n```\n\n## Usage\n\n### Paths\nThere are 3 different Path types right now:\nGenericPath, FilePath, and DirectoryPath\n```swift\n// Paths can be initialized from Strings, Arrays, or Slices\nlet genericString = GenericPath(\"/tmp\")\nlet genericArray = GenericPath([\"/\", \"tmp\"])\nlet genericSlice = GenericPath([\"/\", \"tmp\", \"test\"].dropLast())\n\n// FilePaths and DirectoryPaths can be initialized the same as a GenericPath\n\n// Beware that you do your own validation that the path matches it's type.\n// Things like this are possible and will lead to errors:\nlet file = FilePath(\"/tmp/\")\n\nlet directory = DirectoryPath(\"/tmp/\")\n```\n\n### Path Information\n```swift\n// Paths conform to the StatDelegate protocol, which means that they use the\n// `stat` utility to gather information about the file (ie: size, ownership,\n// modify time, etc)\n// NOTE: Certain properties are only available for paths that exist\n\n/// The system id of the path\nvar id: DeviceID\n\n/// The inode of the path\nvar inode: Inode\n\n/// The type of the path, if it exists\nvar type: PathType\n\n/// Whether the path exists\nvar exists: Bool\n\n/// Whether the path exists and is a file\nvar isFile: Bool\n\n/// Whether the path exists and is a directory\nvar isDirectory: Bool\n\n/// Whether the path exists and is a link\nvar isLink: Bool\n\n/// The URL representation of the path\nvar url: URL\n\n/// The permissions of the path\nvar permissions: FileMode\n\n/// The user id of the user that owns the path\nvar owner: UID\n\n// The name of the user that owns the path\nvar ownerName: String?\n\n/// The group id of the user that owns the path\nvar group: GID\n\n/// The name of the group that owns the path\nvar groupName: String?\n\n/// The device id (if special file)\nvar device: DeviceID\n\n/// The total size, in bytes\nvar size: OSOffsetInt\n// macOS -\u003e Int64\n// Linux -\u003e Int\n\n/// The blocksize for filesystem I/O\nvar blockSize: BlockSize\n\n/// The number of 512B block allocated\nvar blocks: OSOffsetInt\n// macOS -\u003e Int64\n// Linux -\u003e Int\n\n/// The parent directory of the path\nvar parent: DirectoryPath\n\n/// The pieces that make up the path\nvar components: [String]\n\n/// The final piece of the path (filename or directory name)\nvar lastComponent: String?\n\n/// The final piece of the path with the extension stripped off\nvar lastComponentWithoutExtension: String?\n\n/// The extension of the path\nvar extension: String?\n\n/// The last time the path was accessed\nvar lastAccess: Date\n\n/// The last time the path was modified\nvar lastModified: Date\n\n/// The last time the path had a status change\nvar lastAttributeChange: Date\n\n/// The time when the path was created (macOS only)\nvar creation: Date\n```\n\n### Opening Paths\n\n#### FilePath:\n```swift\nlet file = FilePath(\"/tmp/test\")\nlet openFile: OpenFile = try file.open(mode: \"r+\")\n\n// Open files can be written to or read from (depending on the permissions used above)\nlet content: String = try openFile.read()\ntry openFile.write(content)\n```\n\n#### DirectoryPath:\n```swift\nlet dir = DirectoryPath(\"/tmp\")\nlet openDir: OpenDirectory = try dir.open()\n\n// Open directories can be traversed\nlet children = openDir.children()\n\n// Recursively traversing directories requires opening sub-directories and may throw errors\nlet recursiveChildren = try openDir.recursiveChildren()\n```\n\n#### With Closure:\n\nPaths may also be opened for the duration of a provided closure:\n```swift\nlet dir = DirectoryPath(\"/tmp\")\n\ntry dir.open() { openDirectory in\n    let children = openDirectory.children()\n    print(children)\n}\n```\n\n### Creating Paths\n\n#### Any Path conforming to Openable:\n```swift\nvar file = FilePath(\"/tmp/test\")\n\n// Creates a file with the write permissions and returns the opened file\nlet openFile: OpenFile = try file.create(mode: FileMode(owner: .readWriteExecute, group: .readWrite, other: .none))\n```\n\n#### Creating Intermediate Directories:\n\nIn the event you need to create the intermediate paths as well:\n```swift\nvar file = FilePath(\"/tmp/testdir/test\")\n\nlet openFile: OpenFile = try file.create(options: .createIntermediates)\n```\n\n#### With Contents:\n\nPaths whose Open\u003c...\u003e variation conforms to Writable can be created with predetermined contents:\n```swift\nvar file = FilePath(\"/tmp/test\")\n\ntry file.create(contents: \"Hello World\")\nprint(try file.read()) // \"Hello World\"\n```\n\n#### With Closure:\n\nPaths may also be opened for the duration of a provided closure:\n```swift\nvar file = FilePath(\"/tmp/test\")\n\ntry file.create() { openFile in\n    try openFile.write(\"Hello world\")\n    let contents: String = try openFile.read(from: .beginning)\n    print(contents) // Hello World\n}\n```\n\n### Deleting Paths\n\n#### The current path only:\nThis is the same for all paths\n```swift\nvar file = FilePath(\"/tmp/test\")\n\ntry file.delete()\n```\n\n#### Recursively delete directories:\n```swift\nvar dir = DirectoryPath(\"/tmp/test\")\n\ntry dir.recursiveDelete()\n```\nNOTE: Be VERY cautious with this as it cannot be undone (just like `rm -rf`).\n\n### Reading Files\n\n```swift\nlet file = FilePath(\"/tmp/test\")\n\n// All of the following operations are available on both a FilePath and an OpenFile\n\n// Read the whole file\nlet contents: String = try file.read()\n\n// Read up to 1024 bytes\nlet contents: String = try file.read(bytes: 1024)\n\n// Read content as ascii characters instead of utf8\nlet contents: String = try file.read(encoding: .ascii)\n\n// Read to the end, but starting at 1024 bytes from the beginning of the file\nlet contents: String = try file.read(from: Offset(from: .beginning, bytes: 1024))\n\n// Read the last 1024 bytes from of the file using the ascii encoding\nlet contents: String = try file.read(from: Offset(from: .end, bytes: -1024), bytes: 1024, encoding: .ascii)\n```\n\nNOTES:\u003cbr /\u003e\nReading 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.\u003cbr /\u003e\nThe 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)`.\u003cbr /\u003e\nIf 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.\u003cbr /\u003e\nEach 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.\n\n### Writing Files\n\n```swift\nlet file = FilePath(\"/tmp/test\")\n\n// All of the following operations are available on both a FilePath and an OpenFile\n\n// Write a string at the current file position\ntry file.write(\"Hello world\")\n\n// Write an ascii string at the end of the file\ntry file.write(\"Goodbye\", at: Offset(from: .end, bytes: 0), using: .ascii)\n```\nNOTE: You can also pass a `Data` instance to the write function instead of a `String` with an encoding.\n\n### Buffered File Writing\n\n```\n// Writing files is buffered by default. If you expect to use a file\n// immediately after writing to it then be sure to flush the buffer\nlet file = FilePath(\"/tmp/test\")\n\nlet openFile = try file.open(mode: \"w+\")\ntry openFile.write(\"Hello world!\")\ntry openFile.flush()\ntry openFile.rewind()\nlet contents = openFile.read()\n\n// You may also change the buffering mode for the file\ntry openFile.setBuffer(mode: .line) // Flushes after each newline\ntry openFile.setBuffer(mode: .none) // Flushes immediately\ntry openFile.setBuffer(mode: .full(size: 1024)) // Flushes after 1024 bytes are written\n```\nNOTE: The default buffering is full buffering based on your OS's `BUFSIZ` variable\n\n\n### Getting Directory Contents:\n\n#### Immediate children:\n```swift\nlet dir = DirectoryPath(\"/tmp\")\n\nlet children = try dir.children()\n\n// This same operation is safe, assuming you've already opened the directory\nlet openDir = try dir.open()\nlet children = openDir.children()\n\nprint(children.files)\nprint(children.directories)\nprint(children.other)\n```\n\n#### Recursive children:\n```swift\nlet dir = DirectoryPath(\"/tmp\")\n\nlet children = try dir.recursiveChildren()\n\n// This operation is still unsafe, even if the directory is already opened (Because you still might have to open sub-directories, which is unsafe)\nlet openDir = try dir.open()\nlet children = try openDir.recursiveChildren()\n\nprint(children.files)\nprint(children.directories)\nprint(children.other)\n\n// You can optionally specify a depth to only get so many directories\n// This will go no more than 5 directories deep before returning\nlet children = try dir.recursiveChildren(depth: 5)\n```\n\n#### Hidden Files:\n```swift\n// Both .children() and .recursiveChildren() support getting hidden files/directories (files that begin with a '.')\nlet children = try dir.children(options: .includeHidden)\nlet recursiveChildren = try dir.recursiveChildren(depth: 5, options: .includeHidden)\n```\n\n### Changing Path Metadata:\n\n#### Ownership:\n```swift\nvar path = GenericPath(\"/tmp\")\n\n// Owner/Group can be changed separately or together\ntry path.change(owner: \"ponyboy47\")\ntry path.change(group: \"ponyboy47\")\ntry path.change(owner: \"ponyboy47\", group: \"ponyboy47\")\n\n// You can also set them through the corresponding properties:\n// NOTE: Setting them this way is NOT guarenteed to succeed and any errors\n// thrown are ignored. If you need a reliant way to set path ownership then you\n// should call the `change` method directly\npath.owner = 0\npath.group = 1000\npath.ownerName = \"root\"\npath.groupName = \"wheel\"\n\n// If you have a DirectoryPath, then changes can be made recursively:\nvar dir = DirectoryPath(path)\n\ntry dir.recursiveChange(owner: \"ponyboy47\")\n```\n\n#### Permissions:\n```swift\nvar path = GenericPath(\"/tmp\")\n\n// Owner/Group/Others permissions can each be changed separately or in any combination (permissions that are not specified are not changed)\ntry path.change(owner: [.read, .write, .execute]) // Only changes the owner's permissions\ntry path.change(group: .readWrite) // Only changes the group's permissions\ntry path.change(others: .none) // Only changes other's permissions\ntry path.change(ownerGroup: .all) // Only changes owner's and group's permissions\ntry path.change(groupOthers: .read) // Only changes group's and other's permissions\ntry path.change(ownerOthers: .writeExecute) // Only changes owner's and other's permissions\ntry path.change(ownerGroupOthers: .all) // Changes all permissions\n\n// You can also change the uid, gid, and sticky bits\ntry path.change(bits: .uid)\ntry path.change(bits: .gid)\ntry path.change(bits: .sticky)\ntry path.change(bits: [.uid, .sticky])\ntry path.change(bits: .all)\n\n// You can also set them through the permissions property:\n// NOTE: Setting them this way is NOT guarenteed to succeed and any errors\n// thrown are ignored. If you need a reliant way to set path ownership then you\n// should call the `change` method directly\npath.permissions = FileMode(owner: .readWriteExecute, group: .readWrite, others: .read)\npath.permissions.owner = .readWriteExecute\npath.permissions.group = .readWrite\npath.permissions.others = .read\npath.permissions.bits = .none\n\n// If you have a DirectoryPath, then changes can be made recursively:\nvar dir = DirectoryPath(path)\n\ntry dir.recursiveChange(owner: .readWriteExecute, group: .readWrite, others: .read)\n```\n\n### Moving Paths:\n\n```swift\nvar path = GenericPath(\"/tmp/testFile\")\n\n// Both of these things will move testFile from /tmp/testFile to ~/testFile\ntry path.move(to: DirectoryPath.home! + \"testFile\")\ntry path.move(into: DirectoryPath.home!)\n\n// This renames a file in place\ntry path.rename(to: \"newTestFile\")\n```\n\n### Globbing:\n\n```swift\nlet globData = try glob(pattern: \"/tmp/*\")\n\n// Just like getting a directories children:\nprint(globData.files)\nprint(globData.directories)\nprint(globData.other)\n\n// You can also glob from a DirectoryPath\nlet home = DirectoryPath.home\n\nlet globData = try home.glob(\"*.swift\")\n\nprint(globData.files)\nprint(globData.directories)\nprint(globData.other)\n```\n\n### Temporary Paths:\n\n#### Creating Temporary Paths:\n```swift\nlet tmpFile = try FilePath.temporary()\n// /tmp/vDjKM1C\n\nlet tmpDir = try DirectoryPath.temporary()\n// /tmp/rYcznHQ\n\n// You can optionally specify a prefix for the path name\nlet tmpFile = try FilePath.temporary(prefix: \"com.pathman.\")\n// /tmp/com.pathman.gHyiZq\n\n// You can optionally specify a base directory where the temporary path will be stored\nlet tmpDirectory = try DirectoryPath.temporary(base: DirectoryPath(\"/path/to/my/tmp\")!, prefix: \"com.pathman.\")\n// /path/to/my/tmp/com.pathman.2eH4iB\n```\n\n#### With Closure:\n```swift\n// When creating a temporary path with a closure, the path of the temporary\n// file is returned instead of an Opened path\nlet tmpFile: FilePath = try FilePath.temporary() { openFile in\n    try openFile.write(\"Hello World\")\n}\n\n// You can also pass the .deleteOnCompletion option to the .temporary()\n// function in order to delete the temporary path after the closure exits\n// NOTE: This will recursively delete the temporary path if it is a DirectoryPath\ntry FilePath.temporary(options: .deleteOnCompletion) { openFile in\n    try openFile.write(\"Hello World\")\n}\n```\n\n### Links:\n\n#### Target to Destination:\n```swift\n// You can link to an existing path\nlet dir = DirectoryPath(\"/tmp\")\n\n// Creates a soft/symbolic link to dir at the specified path\n// All 3 of the following lines produce the same type of link\nlet link = try dir.link(at: \"~/tmpDir.link\")\nlet link = try dir.link(at: \"~/tmpDir.symbolic\", type: .symbolic)\nlet link = try dir.link(at: \"~/tmpDir.soft\", type: .soft)\n\n// Creates a hard link to dir at the specified path\nlet link = try dir.link(at: \"~/tmpDir.hard\", type: .hard)\n```\n\n#### Destination from Target:\n```swift\nlet linkedFile = FilePath(\"/path/to/link/location\")\n\n// Creates a soft/symbolic link to dir at the specified path\n// All 3 of the following lines produce the same type of link\nlet link = try linkedFile.link(from: \"/path/to/link/target\")\nlet link = try linkedFile.link(from: \"/path/to/link/target\", type: .symbolic)\nlet link = try linkedFile.link(from: \"/path/to/link/target\", type: .soft)\n\n// Creates a hard link to dir at the specified path\nlet link = try linkedFile.link(from: \"/path/to/link/target\", type: .hard)\n```\n\n#### Changing the Default Link Type:\n\nPathman uses .symbolic/.soft links as the default, but this may be changed.\n```swift\nPathman.defaultLinkType = .hard\n```\n\n### Copy Paths:\n\n#### FilePath:\n```swift\nlet file = FilePath(\"/path/to/file\")\n\nlet copyPath = FilePath(\"/path/to/copy\")\n\n// Both these lines would result in the same thing\ntry file.copy(to: copyPath)\ntry file.copy(to: \"/path/to/copy\")\n```\n\n#### DirectoryPath:\n```swift\nlet dir = DirectoryPath(\"/path/to/directory\")\n\nlet copyPath = DirectoryPath(\"/path/to/copy\")\n\n// Both these lines would result in the same thing\ntry dir.copy(to: copyPath)\ntry dir.copy(to: \"/path/to/copy\")\n\n// NOTE: Copying directories will fail if the directory is not empty, so pass\n// the recursive option to the copy call in order to sucessfully copy non empty\n// directories\ntry dir.copy(to: copyPath, options: .recursive)\n\n// NOTE: You may also include hidden files with the includeHidden option\ntry dir.copy(to: copyPath, options: [.recursive, .includeHidden])\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FPonyboy47%2FPathman","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FPonyboy47%2FPathman","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FPonyboy47%2FPathman/lists"}