Repository files navigation

Pathman

Build StatuscodecovMaintainabilityCurrent VersionSupported PlatformsLanguageLanguage VersionLicense
A type-safe path library for Apple's Swift language.

Motivation

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).

Goals

  • 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 NSErrors when FileManager throws
  • 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, and URL types
  • Ease of Use
    • No clunky interface just to get attributes of a path
      • Was anyone ever a fan of FileAttributeKeys?
  • Expose low-level control with high-level safety built-in

Installation

Compatibility:

  • Swift 5.0
  • Ubuntu
  • macOS

Swift Package Manager:

Add this to your Package.swift dependencies:

.package(url:"https://github.com/Ponyboy47/Pathman.git", from:"0.20.1")

Usage

Paths

There are 3 different Path types right now: GenericPath, FilePath, and DirectoryPath

// Paths can be initialized from Strings, Arrays, or Slices
letgenericString=GenericPath("/tmp")letgenericArray=GenericPath(["/","tmp"])letgenericSlice=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:
letfile=FilePath("/tmp/")letdirectory=DirectoryPath("/tmp/")

Path Information

// 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
varid:DeviceID
/// The inode of the path
varinode:Inode
/// The type of the path, if it exists
vartype:PathType
/// Whether the path exists
varexists:Bool
/// Whether the path exists and is a file
varisFile:Bool
/// Whether the path exists and is a directory
varisDirectory:Bool
/// Whether the path exists and is a link
varisLink:Bool
/// The URL representation of the path
varurl:URL
/// The permissions of the path
varpermissions:FileMode
/// The user id of the user that owns the path
varowner:UID
// The name of the user that owns the path
varownerName:String?
/// The group id of the user that owns the path
vargroup:GID
/// The name of the group that owns the path
vargroupName:String?
/// The device id (if special file)
vardevice:DeviceID
/// The total size, in bytes
varsize:OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The blocksize for filesystem I/O
varblockSize:BlockSize
/// The number of 512B block allocated
varblocks:OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The parent directory of the path
varparent:DirectoryPath
/// The pieces that make up the path
varcomponents:[String]
/// The final piece of the path (filename or directory name)
varlastComponent:String?
/// The final piece of the path with the extension stripped off
varlastComponentWithoutExtension:String?
/// The extension of the path
varextension:String?
/// The last time the path was accessed
varlastAccess:Date
/// The last time the path was modified
varlastModified:Date
/// The last time the path had a status change
varlastAttributeChange:Date
/// The time when the path was created (macOS only)
varcreation:Date

Opening Paths

FilePath:

letfile=FilePath("/tmp/test")letopenFile:OpenFile=try file.open(mode:"r+")
// Open files can be written to or read from (depending on the permissions used above)
letcontent:String=try openFile.read()try openFile.write(content)

DirectoryPath:

letdir=DirectoryPath("/tmp")letopenDir:OpenDirectory=try dir.open()
// Open directories can be traversed
letchildren= openDir.children()
// Recursively traversing directories requires opening sub-directories and may throw errors
letrecursiveChildren=try openDir.recursiveChildren()

With Closure:

Paths may also be opened for the duration of a provided closure:

letdir=DirectoryPath("/tmp")try dir.open(){ openDirectory inletchildren= openDirectory.children()print(children)}

Creating Paths

Any Path conforming to Openable:

varfile=FilePath("/tmp/test")
// Creates a file with the write permissions and returns the opened file
letopenFile:OpenFile=try file.create(mode:FileMode(owner:.readWriteExecute, group:.readWrite, other:.none))

Creating Intermediate Directories:

In the event you need to create the intermediate paths as well:

varfile=FilePath("/tmp/testdir/test")letopenFile:OpenFile=try file.create(options:.createIntermediates)

With Contents:

Paths whose Open<...> variation conforms to Writable can be created with predetermined contents:

varfile=FilePath("/tmp/test")try file.create(contents:"Hello World")print(try file.read()) // "Hello World"

With Closure:

Paths may also be opened for the duration of a provided closure:

varfile=FilePath("/tmp/test")try file.create(){ openFile intry openFile.write("Hello world")letcontents:String=try openFile.read(from:.beginning)print(contents) // Hello World
}

Deleting Paths

The current path only:

This is the same for all paths

varfile=FilePath("/tmp/test")try file.delete()

Recursively delete directories:

vardir=DirectoryPath("/tmp/test")try dir.recursiveDelete()

NOTE: Be VERY cautious with this as it cannot be undone (just like rm -rf).

Reading Files

letfile=FilePath("/tmp/test")
// All of the following operations are available on both a FilePath and an OpenFile
// Read the whole file
letcontents:String=try file.read()
// Read up to 1024 bytes
letcontents:String=try file.read(bytes:1024)
// Read content as ascii characters instead of utf8
letcontents:String=try file.read(encoding:.ascii)
// Read to the end, but starting at 1024 bytes from the beginning of the file
letcontents:String=try file.read(from:Offset(from:.beginning, bytes:1024))
// Read the last 1024 bytes from of the file using the ascii encoding
letcontents: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.

Writing Files

letfile=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.

Buffered File Writing

// 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

Getting Directory Contents:

Immediate children:

letdir=DirectoryPath("/tmp")letchildren=try dir.children()
// This same operation is safe, assuming you've already opened the directory
letopenDir=try dir.open()letchildren= openDir.children()print(children.files)print(children.directories)print(children.other)

Recursive children:

letdir=DirectoryPath("/tmp")letchildren=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)
letopenDir=try dir.open()letchildren=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
letchildren=try dir.recursiveChildren(depth:5)

Hidden Files:

// Both .children() and .recursiveChildren() support getting hidden files/directories (files that begin with a '.')
letchildren=try dir.children(options:.includeHidden)letrecursiveChildren=try dir.recursiveChildren(depth:5, options:.includeHidden)

Changing Path Metadata:

Ownership:

varpath=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:
vardir=DirectoryPath(path)try dir.recursiveChange(owner:"ponyboy47")

Permissions:

varpath=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:
vardir=DirectoryPath(path)try dir.recursiveChange(owner:.readWriteExecute, group:.readWrite, others:.read)

Moving Paths:

varpath=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")

Globbing:

letglobData=tryglob(pattern:"/tmp/*")
// Just like getting a directories children:
print(globData.files)print(globData.directories)print(globData.other)
// You can also glob from a DirectoryPath
lethome=DirectoryPath.home
letglobData=try home.glob("*.swift")print(globData.files)print(globData.directories)print(globData.other)

Temporary Paths:

Creating Temporary Paths:

lettmpFile=tryFilePath.temporary()
// /tmp/vDjKM1C
lettmpDir=tryDirectoryPath.temporary()
// /tmp/rYcznHQ
// You can optionally specify a prefix for the path name
lettmpFile=tryFilePath.temporary(prefix:"com.pathman.")
// /tmp/com.pathman.gHyiZq
// You can optionally specify a base directory where the temporary path will be stored
lettmpDirectory=tryDirectoryPath.temporary(base:DirectoryPath("/path/to/my/tmp")!, prefix:"com.pathman.")
// /path/to/my/tmp/com.pathman.2eH4iB

With Closure:

// When creating a temporary path with a closure, the path of the temporary
// file is returned instead of an Opened path
lettmpFile:FilePath=tryFilePath.temporary(){ openFile intry 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
tryFilePath.temporary(options:.deleteOnCompletion){ openFile intry openFile.write("Hello World")}

Links:

Target to Destination:

// You can link to an existing path
letdir=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
letlink=try dir.link(at:"~/tmpDir.link")letlink=try dir.link(at:"~/tmpDir.symbolic", type:.symbolic)letlink=try dir.link(at:"~/tmpDir.soft", type:.soft)
// Creates a hard link to dir at the specified path
letlink=try dir.link(at:"~/tmpDir.hard", type:.hard)

Destination from Target:

letlinkedFile=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
letlink=try linkedFile.link(from:"/path/to/link/target")letlink=try linkedFile.link(from:"/path/to/link/target", type:.symbolic)letlink=try linkedFile.link(from:"/path/to/link/target", type:.soft)
// Creates a hard link to dir at the specified path
letlink=try linkedFile.link(from:"/path/to/link/target", type:.hard)

Changing the Default Link Type:

Pathman uses .symbolic/.soft links as the default, but this may be changed.

Pathman.defaultLinkType =.hard

Copy Paths:

FilePath:

letfile=FilePath("/path/to/file")letcopyPath=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")

DirectoryPath:

letdir=DirectoryPath("/path/to/directory")letcopyPath=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])

About

Swift type-safe path, file, and directory library using POSIX C APIs

Topics

Resources

Stars

14 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Pathman

Build StatuscodecovMaintainabilityCurrent VersionSupported PlatformsLanguageLanguage VersionLicense
A type-safe path library for Apple's Swift language.

Motivation

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).

Goals

  • 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 NSErrors when FileManager throws
  • 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, and URL types
  • Ease of Use
    • No clunky interface just to get attributes of a path
      • Was anyone ever a fan of FileAttributeKeys?
  • Expose low-level control with high-level safety built-in

Installation

Compatibility:

  • Swift 5.0
  • Ubuntu
  • macOS

Swift Package Manager:

Add this to your Package.swift dependencies:

.package(url:"https://github.com/Ponyboy47/Pathman.git", from:"0.20.1")

Usage

Paths

There are 3 different Path types right now: GenericPath, FilePath, and DirectoryPath

// Paths can be initialized from Strings, Arrays, or Slices
letgenericString=GenericPath("/tmp")letgenericArray=GenericPath(["/","tmp"])letgenericSlice=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:
letfile=FilePath("/tmp/")letdirectory=DirectoryPath("/tmp/")

Path Information

// 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
varid:DeviceID
/// The inode of the path
varinode:Inode
/// The type of the path, if it exists
vartype:PathType
/// Whether the path exists
varexists:Bool
/// Whether the path exists and is a file
varisFile:Bool
/// Whether the path exists and is a directory
varisDirectory:Bool
/// Whether the path exists and is a link
varisLink:Bool
/// The URL representation of the path
varurl:URL
/// The permissions of the path
varpermissions:FileMode
/// The user id of the user that owns the path
varowner:UID
// The name of the user that owns the path
varownerName:String?
/// The group id of the user that owns the path
vargroup:GID
/// The name of the group that owns the path
vargroupName:String?
/// The device id (if special file)
vardevice:DeviceID
/// The total size, in bytes
varsize:OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The blocksize for filesystem I/O
varblockSize:BlockSize
/// The number of 512B block allocated
varblocks:OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The parent directory of the path
varparent:DirectoryPath
/// The pieces that make up the path
varcomponents:[String]
/// The final piece of the path (filename or directory name)
varlastComponent:String?
/// The final piece of the path with the extension stripped off
varlastComponentWithoutExtension:String?
/// The extension of the path
varextension:String?
/// The last time the path was accessed
varlastAccess:Date
/// The last time the path was modified
varlastModified:Date
/// The last time the path had a status change
varlastAttributeChange:Date
/// The time when the path was created (macOS only)
varcreation:Date

Opening Paths

FilePath:

letfile=FilePath("/tmp/test")letopenFile:OpenFile=try file.open(mode:"r+")
// Open files can be written to or read from (depending on the permissions used above)
letcontent:String=try openFile.read()try openFile.write(content)

DirectoryPath:

letdir=DirectoryPath("/tmp")letopenDir:OpenDirectory=try dir.open()
// Open directories can be traversed
letchildren= openDir.children()
// Recursively traversing directories requires opening sub-directories and may throw errors
letrecursiveChildren=try openDir.recursiveChildren()

With Closure:

Paths may also be opened for the duration of a provided closure:

letdir=DirectoryPath("/tmp")try dir.open(){ openDirectory inletchildren= openDirectory.children()print(children)}

Creating Paths

Any Path conforming to Openable:

varfile=FilePath("/tmp/test")
// Creates a file with the write permissions and returns the opened file
letopenFile:OpenFile=try file.create(mode:FileMode(owner:.readWriteExecute, group:.readWrite, other:.none))

Creating Intermediate Directories:

In the event you need to create the intermediate paths as well:

varfile=FilePath("/tmp/testdir/test")letopenFile:OpenFile=try file.create(options:.createIntermediates)

With Contents:

Paths whose Open<...> variation conforms to Writable can be created with predetermined contents:

varfile=FilePath("/tmp/test")try file.create(contents:"Hello World")print(try file.read()) // "Hello World"

With Closure:

Paths may also be opened for the duration of a provided closure:

varfile=FilePath("/tmp/test")try file.create(){ openFile intry openFile.write("Hello world")letcontents:String=try openFile.read(from:.beginning)print(contents) // Hello World
}

Deleting Paths

The current path only:

This is the same for all paths

varfile=FilePath("/tmp/test")try file.delete()

Recursively delete directories:

vardir=DirectoryPath("/tmp/test")try dir.recursiveDelete()

NOTE: Be VERY cautious with this as it cannot be undone (just like rm -rf).

Reading Files

letfile=FilePath("/tmp/test")
// All of the following operations are available on both a FilePath and an OpenFile
// Read the whole file
letcontents:String=try file.read()
// Read up to 1024 bytes
letcontents:String=try file.read(bytes:1024)
// Read content as ascii characters instead of utf8
letcontents:String=try file.read(encoding:.ascii)
// Read to the end, but starting at 1024 bytes from the beginning of the file
letcontents:String=try file.read(from:Offset(from:.beginning, bytes:1024))
// Read the last 1024 bytes from of the file using the ascii encoding
letcontents: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.

Writing Files

letfile=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.

Buffered File Writing

// 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

Getting Directory Contents:

Immediate children:

letdir=DirectoryPath("/tmp")letchildren=try dir.children()
// This same operation is safe, assuming you've already opened the directory
letopenDir=try dir.open()letchildren= openDir.children()print(children.files)print(children.directories)print(children.other)

Recursive children:

letdir=DirectoryPath("/tmp")letchildren=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)
letopenDir=try dir.open()letchildren=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
letchildren=try dir.recursiveChildren(depth:5)

Hidden Files:

// Both .children() and .recursiveChildren() support getting hidden files/directories (files that begin with a '.')
letchildren=try dir.children(options:.includeHidden)letrecursiveChildren=try dir.recursiveChildren(depth:5, options:.includeHidden)

Changing Path Metadata:

Ownership:

varpath=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:
vardir=DirectoryPath(path)try dir.recursiveChange(owner:"ponyboy47")

Permissions:

varpath=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:
vardir=DirectoryPath(path)try dir.recursiveChange(owner:.readWriteExecute, group:.readWrite, others:.read)

Moving Paths:

varpath=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")

Globbing:

letglobData=tryglob(pattern:"/tmp/*")
// Just like getting a directories children:
print(globData.files)print(globData.directories)print(globData.other)
// You can also glob from a DirectoryPath
lethome=DirectoryPath.home
letglobData=try home.glob("*.swift")print(globData.files)print(globData.directories)print(globData.other)

Temporary Paths:

Creating Temporary Paths:

lettmpFile=tryFilePath.temporary()
// /tmp/vDjKM1C
lettmpDir=tryDirectoryPath.temporary()
// /tmp/rYcznHQ
// You can optionally specify a prefix for the path name
lettmpFile=tryFilePath.temporary(prefix:"com.pathman.")
// /tmp/com.pathman.gHyiZq
// You can optionally specify a base directory where the temporary path will be stored
lettmpDirectory=tryDirectoryPath.temporary(base:DirectoryPath("/path/to/my/tmp")!, prefix:"com.pathman.")
// /path/to/my/tmp/com.pathman.2eH4iB

With Closure:

// When creating a temporary path with a closure, the path of the temporary
// file is returned instead of an Opened path
lettmpFile:FilePath=tryFilePath.temporary(){ openFile intry 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
tryFilePath.temporary(options:.deleteOnCompletion){ openFile intry openFile.write("Hello World")}

Links:

Target to Destination:

// You can link to an existing path
letdir=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
letlink=try dir.link(at:"~/tmpDir.link")letlink=try dir.link(at:"~/tmpDir.symbolic", type:.symbolic)letlink=try dir.link(at:"~/tmpDir.soft", type:.soft)
// Creates a hard link to dir at the specified path
letlink=try dir.link(at:"~/tmpDir.hard", type:.hard)

Destination from Target:

letlinkedFile=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
letlink=try linkedFile.link(from:"/path/to/link/target")letlink=try linkedFile.link(from:"/path/to/link/target", type:.symbolic)letlink=try linkedFile.link(from:"/path/to/link/target", type:.soft)
// Creates a hard link to dir at the specified path
letlink=try linkedFile.link(from:"/path/to/link/target", type:.hard)

Changing the Default Link Type:

Pathman uses .symbolic/.soft links as the default, but this may be changed.

Pathman.defaultLinkType =.hard

Copy Paths:

FilePath:

letfile=FilePath("/path/to/file")letcopyPath=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")

DirectoryPath:

letdir=DirectoryPath("/path/to/directory")letcopyPath=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])

About

Swift type-safe path, file, and directory library using POSIX C APIs

Topics

Resources

Stars

14 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Pathman

Build StatuscodecovMaintainabilityCurrent VersionSupported PlatformsLanguageLanguage VersionLicense
A type-safe path library for Apple's Swift language.

Motivation

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).

Goals

  • 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 NSErrors when FileManager throws
  • 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, and URL types
  • Ease of Use
    • No clunky interface just to get attributes of a path
      • Was anyone ever a fan of FileAttributeKeys?
  • Expose low-level control with high-level safety built-in

Installation

Compatibility:

  • Swift 5.0
  • Ubuntu
  • macOS

Swift Package Manager:

Add this to your Package.swift dependencies:

.package(url:"https://github.com/Ponyboy47/Pathman.git", from:"0.20.1")

Usage

Paths

There are 3 different Path types right now: GenericPath, FilePath, and DirectoryPath

// Paths can be initialized from Strings, Arrays, or Slices
letgenericString=GenericPath("/tmp")letgenericArray=GenericPath(["/","tmp"])letgenericSlice=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:
letfile=FilePath("/tmp/")letdirectory=DirectoryPath("/tmp/")

Path Information

// 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
varid:DeviceID
/// The inode of the path
varinode:Inode
/// The type of the path, if it exists
vartype:PathType
/// Whether the path exists
varexists:Bool
/// Whether the path exists and is a file
varisFile:Bool
/// Whether the path exists and is a directory
varisDirectory:Bool
/// Whether the path exists and is a link
varisLink:Bool
/// The URL representation of the path
varurl:URL
/// The permissions of the path
varpermissions:FileMode
/// The user id of the user that owns the path
varowner:UID
// The name of the user that owns the path
varownerName:String?
/// The group id of the user that owns the path
vargroup:GID
/// The name of the group that owns the path
vargroupName:String?
/// The device id (if special file)
vardevice:DeviceID
/// The total size, in bytes
varsize:OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The blocksize for filesystem I/O
varblockSize:BlockSize
/// The number of 512B block allocated
varblocks:OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The parent directory of the path
varparent:DirectoryPath
/// The pieces that make up the path
varcomponents:[String]
/// The final piece of the path (filename or directory name)
varlastComponent:String?
/// The final piece of the path with the extension stripped off
varlastComponentWithoutExtension:String?
/// The extension of the path
varextension:String?
/// The last time the path was accessed
varlastAccess:Date
/// The last time the path was modified
varlastModified:Date
/// The last time the path had a status change
varlastAttributeChange:Date
/// The time when the path was created (macOS only)
varcreation:Date

Opening Paths

FilePath:

letfile=FilePath("/tmp/test")letopenFile:OpenFile=try file.open(mode:"r+")
// Open files can be written to or read from (depending on the permissions used above)
letcontent:String=try openFile.read()try openFile.write(content)

DirectoryPath:

letdir=DirectoryPath("/tmp")letopenDir:OpenDirectory=try dir.open()
// Open directories can be traversed
letchildren= openDir.children()
// Recursively traversing directories requires opening sub-directories and may throw errors
letrecursiveChildren=try openDir.recursiveChildren()

With Closure:

Paths may also be opened for the duration of a provided closure:

letdir=DirectoryPath("/tmp")try dir.open(){ openDirectory inletchildren= openDirectory.children()print(children)}

Creating Paths

Any Path conforming to Openable:

varfile=FilePath("/tmp/test")
// Creates a file with the write permissions and returns the opened file
letopenFile:OpenFile=try file.create(mode:FileMode(owner:.readWriteExecute, group:.readWrite, other:.none))

Creating Intermediate Directories:

In the event you need to create the intermediate paths as well:

varfile=FilePath("/tmp/testdir/test")letopenFile:OpenFile=try file.create(options:.createIntermediates)

With Contents:

Paths whose Open<...> variation conforms to Writable can be created with predetermined contents:

varfile=FilePath("/tmp/test")try file.create(contents:"Hello World")print(try file.read()) // "Hello World"

With Closure:

Paths may also be opened for the duration of a provided closure:

varfile=FilePath("/tmp/test")try file.create(){ openFile intry openFile.write("Hello world")letcontents:String=try openFile.read(from:.beginning)print(contents) // Hello World
}

Deleting Paths

The current path only:

This is the same for all paths

varfile=FilePath("/tmp/test")try file.delete()

Recursively delete directories:

vardir=DirectoryPath("/tmp/test")try dir.recursiveDelete()

NOTE: Be VERY cautious with this as it cannot be undone (just like rm -rf).

Reading Files

letfile=FilePath("/tmp/test")
// All of the following operations are available on both a FilePath and an OpenFile
// Read the whole file
letcontents:String=try file.read()
// Read up to 1024 bytes
letcontents:String=try file.read(bytes:1024)
// Read content as ascii characters instead of utf8
letcontents:String=try file.read(encoding:.ascii)
// Read to the end, but starting at 1024 bytes from the beginning of the file
letcontents:String=try file.read(from:Offset(from:.beginning, bytes:1024))
// Read the last 1024 bytes from of the file using the ascii encoding
letcontents: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.

Writing Files

letfile=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.

Buffered File Writing

// 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

Getting Directory Contents:

Immediate children:

letdir=DirectoryPath("/tmp")letchildren=try dir.children()
// This same operation is safe, assuming you've already opened the directory
letopenDir=try dir.open()letchildren= openDir.children()print(children.files)print(children.directories)print(children.other)

Recursive children:

letdir=DirectoryPath("/tmp")letchildren=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)
letopenDir=try dir.open()letchildren=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
letchildren=try dir.recursiveChildren(depth:5)

Hidden Files:

// Both .children() and .recursiveChildren() support getting hidden files/directories (files that begin with a '.')
letchildren=try dir.children(options:.includeHidden)letrecursiveChildren=try dir.recursiveChildren(depth:5, options:.includeHidden)

Changing Path Metadata:

Ownership:

varpath=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:
vardir=DirectoryPath(path)try dir.recursiveChange(owner:"ponyboy47")

Permissions:

varpath=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:
vardir=DirectoryPath(path)try dir.recursiveChange(owner:.readWriteExecute, group:.readWrite, others:.read)

Moving Paths:

varpath=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")

Globbing:

letglobData=tryglob(pattern:"/tmp/*")
// Just like getting a directories children:
print(globData.files)print(globData.directories)print(globData.other)
// You can also glob from a DirectoryPath
lethome=DirectoryPath.home
letglobData=try home.glob("*.swift")print(globData.files)print(globData.directories)print(globData.other)

Temporary Paths:

Creating Temporary Paths:

lettmpFile=tryFilePath.temporary()
// /tmp/vDjKM1C
lettmpDir=tryDirectoryPath.temporary()
// /tmp/rYcznHQ
// You can optionally specify a prefix for the path name
lettmpFile=tryFilePath.temporary(prefix:"com.pathman.")
// /tmp/com.pathman.gHyiZq
// You can optionally specify a base directory where the temporary path will be stored
lettmpDirectory=tryDirectoryPath.temporary(base:DirectoryPath("/path/to/my/tmp")!, prefix:"com.pathman.")
// /path/to/my/tmp/com.pathman.2eH4iB

With Closure:

// When creating a temporary path with a closure, the path of the temporary
// file is returned instead of an Opened path
lettmpFile:FilePath=tryFilePath.temporary(){ openFile intry 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
tryFilePath.temporary(options:.deleteOnCompletion){ openFile intry openFile.write("Hello World")}

Links:

Target to Destination:

// You can link to an existing path
letdir=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
letlink=try dir.link(at:"~/tmpDir.link")letlink=try dir.link(at:"~/tmpDir.symbolic", type:.symbolic)letlink=try dir.link(at:"~/tmpDir.soft", type:.soft)
// Creates a hard link to dir at the specified path
letlink=try dir.link(at:"~/tmpDir.hard", type:.hard)

Destination from Target:

letlinkedFile=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
letlink=try linkedFile.link(from:"/path/to/link/target")letlink=try linkedFile.link(from:"/path/to/link/target", type:.symbolic)letlink=try linkedFile.link(from:"/path/to/link/target", type:.soft)
// Creates a hard link to dir at the specified path
letlink=try linkedFile.link(from:"/path/to/link/target", type:.hard)

Changing the Default Link Type:

Pathman uses .symbolic/.soft links as the default, but this may be changed.

Pathman.defaultLinkType =.hard

Copy Paths:

FilePath:

letfile=FilePath("/path/to/file")letcopyPath=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")

DirectoryPath:

letdir=DirectoryPath("/path/to/directory")letcopyPath=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])

About

Swift type-safe path, file, and directory library using POSIX C APIs

Topics

Resources

Stars

14 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Pathman

Build StatuscodecovMaintainabilityCurrent VersionSupported PlatformsLanguageLanguage VersionLicense
A type-safe path library for Apple's Swift language.

Motivation

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).

Goals

  • 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 NSErrors when FileManager throws
  • 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, and URL types
  • Ease of Use
    • No clunky interface just to get attributes of a path
      • Was anyone ever a fan of FileAttributeKeys?
  • Expose low-level control with high-level safety built-in

Installation

Compatibility:

  • Swift 5.0
  • Ubuntu
  • macOS

Swift Package Manager:

Add this to your Package.swift dependencies:

.package(url:"https://github.com/Ponyboy47/Pathman.git", from:"0.20.1")

Usage

Paths

There are 3 different Path types right now: GenericPath, FilePath, and DirectoryPath

// Paths can be initialized from Strings, Arrays, or Slices
letgenericString=GenericPath("/tmp")letgenericArray=GenericPath(["/","tmp"])letgenericSlice=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:
letfile=FilePath("/tmp/")letdirectory=DirectoryPath("/tmp/")

Path Information

// 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
varid:DeviceID
/// The inode of the path
varinode:Inode
/// The type of the path, if it exists
vartype:PathType
/// Whether the path exists
varexists:Bool
/// Whether the path exists and is a file
varisFile:Bool
/// Whether the path exists and is a directory
varisDirectory:Bool
/// Whether the path exists and is a link
varisLink:Bool
/// The URL representation of the path
varurl:URL
/// The permissions of the path
varpermissions:FileMode
/// The user id of the user that owns the path
varowner:UID
// The name of the user that owns the path
varownerName:String?
/// The group id of the user that owns the path
vargroup:GID
/// The name of the group that owns the path
vargroupName:String?
/// The device id (if special file)
vardevice:DeviceID
/// The total size, in bytes
varsize:OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The blocksize for filesystem I/O
varblockSize:BlockSize
/// The number of 512B block allocated
varblocks:OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The parent directory of the path
varparent:DirectoryPath
/// The pieces that make up the path
varcomponents:[String]
/// The final piece of the path (filename or directory name)
varlastComponent:String?
/// The final piece of the path with the extension stripped off
varlastComponentWithoutExtension:String?
/// The extension of the path
varextension:String?
/// The last time the path was accessed
varlastAccess:Date
/// The last time the path was modified
varlastModified:Date
/// The last time the path had a status change
varlastAttributeChange:Date
/// The time when the path was created (macOS only)
varcreation:Date

Opening Paths

FilePath:

letfile=FilePath("/tmp/test")letopenFile:OpenFile=try file.open(mode:"r+")
// Open files can be written to or read from (depending on the permissions used above)
letcontent:String=try openFile.read()try openFile.write(content)

DirectoryPath:

letdir=DirectoryPath("/tmp")letopenDir:OpenDirectory=try dir.open()
// Open directories can be traversed
letchildren= openDir.children()
// Recursively traversing directories requires opening sub-directories and may throw errors
letrecursiveChildren=try openDir.recursiveChildren()

With Closure:

Paths may also be opened for the duration of a provided closure:

letdir=DirectoryPath("/tmp")try dir.open(){ openDirectory inletchildren= openDirectory.children()print(children)}

Creating Paths

Any Path conforming to Openable:

varfile=FilePath("/tmp/test")
// Creates a file with the write permissions and returns the opened file
letopenFile:OpenFile=try file.create(mode:FileMode(owner:.readWriteExecute, group:.readWrite, other:.none))

Creating Intermediate Directories:

In the event you need to create the intermediate paths as well:

varfile=FilePath("/tmp/testdir/test")letopenFile:OpenFile=try file.create(options:.createIntermediates)

With Contents:

Paths whose Open<...> variation conforms to Writable can be created with predetermined contents:

varfile=FilePath("/tmp/test")try file.create(contents:"Hello World")print(try file.read()) // "Hello World"

With Closure:

Paths may also be opened for the duration of a provided closure:

varfile=FilePath("/tmp/test")try file.create(){ openFile intry openFile.write("Hello world")letcontents:String=try openFile.read(from:.beginning)print(contents) // Hello World
}

Deleting Paths

The current path only:

This is the same for all paths

varfile=FilePath("/tmp/test")try file.delete()

Recursively delete directories:

vardir=DirectoryPath("/tmp/test")try dir.recursiveDelete()

NOTE: Be VERY cautious with this as it cannot be undone (just like rm -rf).

Reading Files

letfile=FilePath("/tmp/test")
// All of the following operations are available on both a FilePath and an OpenFile
// Read the whole file
letcontents:String=try file.read()
// Read up to 1024 bytes
letcontents:String=try file.read(bytes:1024)
// Read content as ascii characters instead of utf8
letcontents:String=try file.read(encoding:.ascii)
// Read to the end, but starting at 1024 bytes from the beginning of the file
letcontents:String=try file.read(from:Offset(from:.beginning, bytes:1024))
// Read the last 1024 bytes from of the file using the ascii encoding
letcontents: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.

Writing Files

letfile=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.

Buffered File Writing

// 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

Getting Directory Contents:

Immediate children:

letdir=DirectoryPath("/tmp")letchildren=try dir.children()
// This same operation is safe, assuming you've already opened the directory
letopenDir=try dir.open()letchildren= openDir.children()print(children.files)print(children.directories)print(children.other)

Recursive children:

letdir=DirectoryPath("/tmp")letchildren=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)
letopenDir=try dir.open()letchildren=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
letchildren=try dir.recursiveChildren(depth:5)

Hidden Files:

// Both .children() and .recursiveChildren() support getting hidden files/directories (files that begin with a '.')
letchildren=try dir.children(options:.includeHidden)letrecursiveChildren=try dir.recursiveChildren(depth:5, options:.includeHidden)

Changing Path Metadata:

Ownership:

varpath=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:
vardir=DirectoryPath(path)try dir.recursiveChange(owner:"ponyboy47")

Permissions:

varpath=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:
vardir=DirectoryPath(path)try dir.recursiveChange(owner:.readWriteExecute, group:.readWrite, others:.read)

Moving Paths:

varpath=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")

Globbing:

letglobData=tryglob(pattern:"/tmp/*")
// Just like getting a directories children:
print(globData.files)print(globData.directories)print(globData.other)
// You can also glob from a DirectoryPath
lethome=DirectoryPath.home
letglobData=try home.glob("*.swift")print(globData.files)print(globData.directories)print(globData.other)

Temporary Paths:

Creating Temporary Paths:

lettmpFile=tryFilePath.temporary()
// /tmp/vDjKM1C
lettmpDir=tryDirectoryPath.temporary()
// /tmp/rYcznHQ
// You can optionally specify a prefix for the path name
lettmpFile=tryFilePath.temporary(prefix:"com.pathman.")
// /tmp/com.pathman.gHyiZq
// You can optionally specify a base directory where the temporary path will be stored
lettmpDirectory=tryDirectoryPath.temporary(base:DirectoryPath("/path/to/my/tmp")!, prefix:"com.pathman.")
// /path/to/my/tmp/com.pathman.2eH4iB

With Closure:

// When creating a temporary path with a closure, the path of the temporary
// file is returned instead of an Opened path
lettmpFile:FilePath=tryFilePath.temporary(){ openFile intry 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
tryFilePath.temporary(options:.deleteOnCompletion){ openFile intry openFile.write("Hello World")}

Links:

Target to Destination:

// You can link to an existing path
letdir=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
letlink=try dir.link(at:"~/tmpDir.link")letlink=try dir.link(at:"~/tmpDir.symbolic", type:.symbolic)letlink=try dir.link(at:"~/tmpDir.soft", type:.soft)
// Creates a hard link to dir at the specified path
letlink=try dir.link(at:"~/tmpDir.hard", type:.hard)

Destination from Target:

letlinkedFile=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
letlink=try linkedFile.link(from:"/path/to/link/target")letlink=try linkedFile.link(from:"/path/to/link/target", type:.symbolic)letlink=try linkedFile.link(from:"/path/to/link/target", type:.soft)
// Creates a hard link to dir at the specified path
letlink=try linkedFile.link(from:"/path/to/link/target", type:.hard)

Changing the Default Link Type:

Pathman uses .symbolic/.soft links as the default, but this may be changed.

Pathman.defaultLinkType =.hard

Copy Paths:

FilePath:

letfile=FilePath("/path/to/file")letcopyPath=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")

DirectoryPath:

letdir=DirectoryPath("/path/to/directory")letcopyPath=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])

About

Swift type-safe path, file, and directory library using POSIX C APIs

Topics

Resources

Stars

14 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Pathman

Build StatuscodecovMaintainabilityCurrent VersionSupported PlatformsLanguageLanguage VersionLicense
A type-safe path library for Apple's Swift language.

Motivation

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).

Goals

  • 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 NSErrors when FileManager throws
  • 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, and URL types
  • Ease of Use
    • No clunky interface just to get attributes of a path
      • Was anyone ever a fan of FileAttributeKeys?
  • Expose low-level control with high-level safety built-in

Installation

Compatibility:

  • Swift 5.0
  • Ubuntu
  • macOS

Swift Package Manager:

Add this to your Package.swift dependencies:

.package(url:"https://github.com/Ponyboy47/Pathman.git", from:"0.20.1")

Usage

Paths

There are 3 different Path types right now: GenericPath, FilePath, and DirectoryPath

// Paths can be initialized from Strings, Arrays, or Slices
letgenericString=GenericPath("/tmp")letgenericArray=GenericPath(["/","tmp"])letgenericSlice=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:
letfile=FilePath("/tmp/")letdirectory=DirectoryPath("/tmp/")

Path Information

// 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
varid:DeviceID
/// The inode of the path
varinode:Inode
/// The type of the path, if it exists
vartype:PathType
/// Whether the path exists
varexists:Bool
/// Whether the path exists and is a file
varisFile:Bool
/// Whether the path exists and is a directory
varisDirectory:Bool
/// Whether the path exists and is a link
varisLink:Bool
/// The URL representation of the path
varurl:URL
/// The permissions of the path
varpermissions:FileMode
/// The user id of the user that owns the path
varowner:UID
// The name of the user that owns the path
varownerName:String?
/// The group id of the user that owns the path
vargroup:GID
/// The name of the group that owns the path
vargroupName:String?
/// The device id (if special file)
vardevice:DeviceID
/// The total size, in bytes
varsize:OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The blocksize for filesystem I/O
varblockSize:BlockSize
/// The number of 512B block allocated
varblocks:OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The parent directory of the path
varparent:DirectoryPath
/// The pieces that make up the path
varcomponents:[String]
/// The final piece of the path (filename or directory name)
varlastComponent:String?
/// The final piece of the path with the extension stripped off
varlastComponentWithoutExtension:String?
/// The extension of the path
varextension:String?
/// The last time the path was accessed
varlastAccess:Date
/// The last time the path was modified
varlastModified:Date
/// The last time the path had a status change
varlastAttributeChange:Date
/// The time when the path was created (macOS only)
varcreation:Date

Opening Paths

FilePath:

letfile=FilePath("/tmp/test")letopenFile:OpenFile=try file.open(mode:"r+")
// Open files can be written to or read from (depending on the permissions used above)
letcontent:String=try openFile.read()try openFile.write(content)

DirectoryPath:

letdir=DirectoryPath("/tmp")letopenDir:OpenDirectory=try dir.open()
// Open directories can be traversed
letchildren= openDir.children()
// Recursively traversing directories requires opening sub-directories and may throw errors
letrecursiveChildren=try openDir.recursiveChildren()

With Closure:

Paths may also be opened for the duration of a provided closure:

letdir=DirectoryPath("/tmp")try dir.open(){ openDirectory inletchildren= openDirectory.children()print(children)}

Creating Paths

Any Path conforming to Openable:

varfile=FilePath("/tmp/test")
// Creates a file with the write permissions and returns the opened file
letopenFile:OpenFile=try file.create(mode:FileMode(owner:.readWriteExecute, group:.readWrite, other:.none))

Creating Intermediate Directories:

In the event you need to create the intermediate paths as well:

varfile=FilePath("/tmp/testdir/test")letopenFile:OpenFile=try file.create(options:.createIntermediates)

With Contents:

Paths whose Open<...> variation conforms to Writable can be created with predetermined contents:

varfile=FilePath("/tmp/test")try file.create(contents:"Hello World")print(try file.read()) // "Hello World"

With Closure:

Paths may also be opened for the duration of a provided closure:

varfile=FilePath("/tmp/test")try file.create(){ openFile intry openFile.write("Hello world")letcontents:String=try openFile.read(from:.beginning)print(contents) // Hello World
}

Deleting Paths

The current path only:

This is the same for all paths

varfile=FilePath("/tmp/test")try file.delete()

Recursively delete directories:

vardir=DirectoryPath("/tmp/test")try dir.recursiveDelete()

NOTE: Be VERY cautious with this as it cannot be undone (just like rm -rf).

Reading Files

letfile=FilePath("/tmp/test")
// All of the following operations are available on both a FilePath and an OpenFile
// Read the whole file
letcontents:String=try file.read()
// Read up to 1024 bytes
letcontents:String=try file.read(bytes:1024)
// Read content as ascii characters instead of utf8
letcontents:String=try file.read(encoding:.ascii)
// Read to the end, but starting at 1024 bytes from the beginning of the file
letcontents:String=try file.read(from:Offset(from:.beginning, bytes:1024))
// Read the last 1024 bytes from of the file using the ascii encoding
letcontents: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.

Writing Files

letfile=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.

Buffered File Writing

// 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

Getting Directory Contents:

Immediate children:

letdir=DirectoryPath("/tmp")letchildren=try dir.children()
// This same operation is safe, assuming you've already opened the directory
letopenDir=try dir.open()letchildren= openDir.children()print(children.files)print(children.directories)print(children.other)

Recursive children:

letdir=DirectoryPath("/tmp")letchildren=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)
letopenDir=try dir.open()letchildren=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
letchildren=try dir.recursiveChildren(depth:5)

Hidden Files:

// Both .children() and .recursiveChildren() support getting hidden files/directories (files that begin with a '.')
letchildren=try dir.children(options:.includeHidden)letrecursiveChildren=try dir.recursiveChildren(depth:5, options:.includeHidden)

Changing Path Metadata:

Ownership:

varpath=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:
vardir=DirectoryPath(path)try dir.recursiveChange(owner:"ponyboy47")

Permissions:

varpath=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:
vardir=DirectoryPath(path)try dir.recursiveChange(owner:.readWriteExecute, group:.readWrite, others:.read)

Moving Paths:

varpath=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")

Globbing:

letglobData=tryglob(pattern:"/tmp/*")
// Just like getting a directories children:
print(globData.files)print(globData.directories)print(globData.other)
// You can also glob from a DirectoryPath
lethome=DirectoryPath.home
letglobData=try home.glob("*.swift")print(globData.files)print(globData.directories)print(globData.other)

Temporary Paths:

Creating Temporary Paths:

lettmpFile=tryFilePath.temporary()
// /tmp/vDjKM1C
lettmpDir=tryDirectoryPath.temporary()
// /tmp/rYcznHQ
// You can optionally specify a prefix for the path name
lettmpFile=tryFilePath.temporary(prefix:"com.pathman.")
// /tmp/com.pathman.gHyiZq
// You can optionally specify a base directory where the temporary path will be stored
lettmpDirectory=tryDirectoryPath.temporary(base:DirectoryPath("/path/to/my/tmp")!, prefix:"com.pathman.")
// /path/to/my/tmp/com.pathman.2eH4iB

With Closure:

// When creating a temporary path with a closure, the path of the temporary
// file is returned instead of an Opened path
lettmpFile:FilePath=tryFilePath.temporary(){ openFile intry 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
tryFilePath.temporary(options:.deleteOnCompletion){ openFile intry openFile.write("Hello World")}

Links:

Target to Destination:

// You can link to an existing path
letdir=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
letlink=try dir.link(at:"~/tmpDir.link")letlink=try dir.link(at:"~/tmpDir.symbolic", type:.symbolic)letlink=try dir.link(at:"~/tmpDir.soft", type:.soft)
// Creates a hard link to dir at the specified path
letlink=try dir.link(at:"~/tmpDir.hard", type:.hard)

Destination from Target:

letlinkedFile=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
letlink=try linkedFile.link(from:"/path/to/link/target")letlink=try linkedFile.link(from:"/path/to/link/target", type:.symbolic)letlink=try linkedFile.link(from:"/path/to/link/target", type:.soft)
// Creates a hard link to dir at the specified path
letlink=try linkedFile.link(from:"/path/to/link/target", type:.hard)

Changing the Default Link Type:

Pathman uses .symbolic/.soft links as the default, but this may be changed.

Pathman.defaultLinkType =.hard

Copy Paths:

FilePath:

letfile=FilePath("/path/to/file")letcopyPath=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")

DirectoryPath:

letdir=DirectoryPath("/path/to/directory")letcopyPath=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])

About

Swift type-safe path, file, and directory library using POSIX C APIs

Topics

Resources

Stars

14 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Pathman

Build StatuscodecovMaintainabilityCurrent VersionSupported PlatformsLanguageLanguage VersionLicense
A type-safe path library for Apple's Swift language.

Motivation

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).

Goals

  • 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 NSErrors when FileManager throws
  • 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, and URL types
  • Ease of Use
    • No clunky interface just to get attributes of a path
      • Was anyone ever a fan of FileAttributeKeys?
  • Expose low-level control with high-level safety built-in

Installation

Compatibility:

  • Swift 5.0
  • Ubuntu
  • macOS

Swift Package Manager:

Add this to your Package.swift dependencies:

.package(url:"https://github.com/Ponyboy47/Pathman.git", from:"0.20.1")

Usage

Paths

There are 3 different Path types right now: GenericPath, FilePath, and DirectoryPath

// Paths can be initialized from Strings, Arrays, or Slices
letgenericString=GenericPath("/tmp")letgenericArray=GenericPath(["/","tmp"])letgenericSlice=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:
letfile=FilePath("/tmp/")letdirectory=DirectoryPath("/tmp/")

Path Information

// 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
varid:DeviceID
/// The inode of the path
varinode:Inode
/// The type of the path, if it exists
vartype:PathType
/// Whether the path exists
varexists:Bool
/// Whether the path exists and is a file
varisFile:Bool
/// Whether the path exists and is a directory
varisDirectory:Bool
/// Whether the path exists and is a link
varisLink:Bool
/// The URL representation of the path
varurl:URL
/// The permissions of the path
varpermissions:FileMode
/// The user id of the user that owns the path
varowner:UID
// The name of the user that owns the path
varownerName:String?
/// The group id of the user that owns the path
vargroup:GID
/// The name of the group that owns the path
vargroupName:String?
/// The device id (if special file)
vardevice:DeviceID
/// The total size, in bytes
varsize:OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The blocksize for filesystem I/O
varblockSize:BlockSize
/// The number of 512B block allocated
varblocks:OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The parent directory of the path
varparent:DirectoryPath
/// The pieces that make up the path
varcomponents:[String]
/// The final piece of the path (filename or directory name)
varlastComponent:String?
/// The final piece of the path with the extension stripped off
varlastComponentWithoutExtension:String?
/// The extension of the path
varextension:String?
/// The last time the path was accessed
varlastAccess:Date
/// The last time the path was modified
varlastModified:Date
/// The last time the path had a status change
varlastAttributeChange:Date
/// The time when the path was created (macOS only)
varcreation:Date

Opening Paths

FilePath:

letfile=FilePath("/tmp/test")letopenFile:OpenFile=try file.open(mode:"r+")
// Open files can be written to or read from (depending on the permissions used above)
letcontent:String=try openFile.read()try openFile.write(content)

DirectoryPath:

letdir=DirectoryPath("/tmp")letopenDir:OpenDirectory=try dir.open()
// Open directories can be traversed
letchildren= openDir.children()
// Recursively traversing directories requires opening sub-directories and may throw errors
letrecursiveChildren=try openDir.recursiveChildren()

With Closure:

Paths may also be opened for the duration of a provided closure:

letdir=DirectoryPath("/tmp")try dir.open(){ openDirectory inletchildren= openDirectory.children()print(children)}

Creating Paths

Any Path conforming to Openable:

varfile=FilePath("/tmp/test")
// Creates a file with the write permissions and returns the opened file
letopenFile:OpenFile=try file.create(mode:FileMode(owner:.readWriteExecute, group:.readWrite, other:.none))

Creating Intermediate Directories:

In the event you need to create the intermediate paths as well:

varfile=FilePath("/tmp/testdir/test")letopenFile:OpenFile=try file.create(options:.createIntermediates)

With Contents:

Paths whose Open<...> variation conforms to Writable can be created with predetermined contents:

varfile=FilePath("/tmp/test")try file.create(contents:"Hello World")print(try file.read()) // "Hello World"

With Closure:

Paths may also be opened for the duration of a provided closure:

varfile=FilePath("/tmp/test")try file.create(){ openFile intry openFile.write("Hello world")letcontents:String=try openFile.read(from:.beginning)print(contents) // Hello World
}

Deleting Paths

The current path only:

This is the same for all paths

varfile=FilePath("/tmp/test")try file.delete()

Recursively delete directories:

vardir=DirectoryPath("/tmp/test")try dir.recursiveDelete()

NOTE: Be VERY cautious with this as it cannot be undone (just like rm -rf).

Reading Files

letfile=FilePath("/tmp/test")
// All of the following operations are available on both a FilePath and an OpenFile
// Read the whole file
letcontents:String=try file.read()
// Read up to 1024 bytes
letcontents:String=try file.read(bytes:1024)
// Read content as ascii characters instead of utf8
letcontents:String=try file.read(encoding:.ascii)
// Read to the end, but starting at 1024 bytes from the beginning of the file
letcontents:String=try file.read(from:Offset(from:.beginning, bytes:1024))
// Read the last 1024 bytes from of the file using the ascii encoding
letcontents: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.

Writing Files

letfile=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.

Buffered File Writing

// 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

Getting Directory Contents:

Immediate children:

letdir=DirectoryPath("/tmp")letchildren=try dir.children()
// This same operation is safe, assuming you've already opened the directory
letopenDir=try dir.open()letchildren= openDir.children()print(children.files)print(children.directories)print(children.other)

Recursive children:

letdir=DirectoryPath("/tmp")letchildren=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)
letopenDir=try dir.open()letchildren=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
letchildren=try dir.recursiveChildren(depth:5)

Hidden Files:

// Both .children() and .recursiveChildren() support getting hidden files/directories (files that begin with a '.')
letchildren=try dir.children(options:.includeHidden)letrecursiveChildren=try dir.recursiveChildren(depth:5, options:.includeHidden)

Changing Path Metadata:

Ownership:

varpath=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:
vardir=DirectoryPath(path)try dir.recursiveChange(owner:"ponyboy47")

Permissions:

varpath=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:
vardir=DirectoryPath(path)try dir.recursiveChange(owner:.readWriteExecute, group:.readWrite, others:.read)

Moving Paths:

varpath=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")

Globbing:

letglobData=tryglob(pattern:"/tmp/*")
// Just like getting a directories children:
print(globData.files)print(globData.directories)print(globData.other)
// You can also glob from a DirectoryPath
lethome=DirectoryPath.home
letglobData=try home.glob("*.swift")print(globData.files)print(globData.directories)print(globData.other)

Temporary Paths:

Creating Temporary Paths:

lettmpFile=tryFilePath.temporary()
// /tmp/vDjKM1C
lettmpDir=tryDirectoryPath.temporary()
// /tmp/rYcznHQ
// You can optionally specify a prefix for the path name
lettmpFile=tryFilePath.temporary(prefix:"com.pathman.")
// /tmp/com.pathman.gHyiZq
// You can optionally specify a base directory where the temporary path will be stored
lettmpDirectory=tryDirectoryPath.temporary(base:DirectoryPath("/path/to/my/tmp")!, prefix:"com.pathman.")
// /path/to/my/tmp/com.pathman.2eH4iB

With Closure:

// When creating a temporary path with a closure, the path of the temporary
// file is returned instead of an Opened path
lettmpFile:FilePath=tryFilePath.temporary(){ openFile intry 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
tryFilePath.temporary(options:.deleteOnCompletion){ openFile intry openFile.write("Hello World")}

Links:

Target to Destination:

// You can link to an existing path
letdir=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
letlink=try dir.link(at:"~/tmpDir.link")letlink=try dir.link(at:"~/tmpDir.symbolic", type:.symbolic)letlink=try dir.link(at:"~/tmpDir.soft", type:.soft)
// Creates a hard link to dir at the specified path
letlink=try dir.link(at:"~/tmpDir.hard", type:.hard)

Destination from Target:

letlinkedFile=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
letlink=try linkedFile.link(from:"/path/to/link/target")letlink=try linkedFile.link(from:"/path/to/link/target", type:.symbolic)letlink=try linkedFile.link(from:"/path/to/link/target", type:.soft)
// Creates a hard link to dir at the specified path
letlink=try linkedFile.link(from:"/path/to/link/target", type:.hard)

Changing the Default Link Type:

Pathman uses .symbolic/.soft links as the default, but this may be changed.

Pathman.defaultLinkType =.hard

Copy Paths:

FilePath:

letfile=FilePath("/path/to/file")letcopyPath=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")

DirectoryPath:

letdir=DirectoryPath("/path/to/directory")letcopyPath=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])

About

Swift type-safe path, file, and directory library using POSIX C APIs

Topics

Resources

Stars

14 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Pathman

Build StatuscodecovMaintainabilityCurrent VersionSupported PlatformsLanguageLanguage VersionLicense
A type-safe path library for Apple's Swift language.

Motivation

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).

Goals

  • 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 NSErrors when FileManager throws
  • 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, and URL types
  • Ease of Use
    • No clunky interface just to get attributes of a path
      • Was anyone ever a fan of FileAttributeKeys?
  • Expose low-level control with high-level safety built-in

Installation

Compatibility:

  • Swift 5.0
  • Ubuntu
  • macOS

Swift Package Manager:

Add this to your Package.swift dependencies:

.package(url:"https://github.com/Ponyboy47/Pathman.git", from:"0.20.1")

Usage

Paths

There are 3 different Path types right now: GenericPath, FilePath, and DirectoryPath

// Paths can be initialized from Strings, Arrays, or Slices
letgenericString=GenericPath("/tmp")letgenericArray=GenericPath(["/","tmp"])letgenericSlice=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:
letfile=FilePath("/tmp/")letdirectory=DirectoryPath("/tmp/")

Path Information

// 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
varid:DeviceID
/// The inode of the path
varinode:Inode
/// The type of the path, if it exists
vartype:PathType
/// Whether the path exists
varexists:Bool
/// Whether the path exists and is a file
varisFile:Bool
/// Whether the path exists and is a directory
varisDirectory:Bool
/// Whether the path exists and is a link
varisLink:Bool
/// The URL representation of the path
varurl:URL
/// The permissions of the path
varpermissions:FileMode
/// The user id of the user that owns the path
varowner:UID
// The name of the user that owns the path
varownerName:String?
/// The group id of the user that owns the path
vargroup:GID
/// The name of the group that owns the path
vargroupName:String?
/// The device id (if special file)
vardevice:DeviceID
/// The total size, in bytes
varsize:OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The blocksize for filesystem I/O
varblockSize:BlockSize
/// The number of 512B block allocated
varblocks:OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The parent directory of the path
varparent:DirectoryPath
/// The pieces that make up the path
varcomponents:[String]
/// The final piece of the path (filename or directory name)
varlastComponent:String?
/// The final piece of the path with the extension stripped off
varlastComponentWithoutExtension:String?
/// The extension of the path
varextension:String?
/// The last time the path was accessed
varlastAccess:Date
/// The last time the path was modified
varlastModified:Date
/// The last time the path had a status change
varlastAttributeChange:Date
/// The time when the path was created (macOS only)
varcreation:Date

Opening Paths

FilePath:

letfile=FilePath("/tmp/test")letopenFile:OpenFile=try file.open(mode:"r+")
// Open files can be written to or read from (depending on the permissions used above)
letcontent:String=try openFile.read()try openFile.write(content)

DirectoryPath:

letdir=DirectoryPath("/tmp")letopenDir:OpenDirectory=try dir.open()
// Open directories can be traversed
letchildren= openDir.children()
// Recursively traversing directories requires opening sub-directories and may throw errors
letrecursiveChildren=try openDir.recursiveChildren()

With Closure:

Paths may also be opened for the duration of a provided closure:

letdir=DirectoryPath("/tmp")try dir.open(){ openDirectory inletchildren= openDirectory.children()print(children)}

Creating Paths

Any Path conforming to Openable:

varfile=FilePath("/tmp/test")
// Creates a file with the write permissions and returns the opened file
letopenFile:OpenFile=try file.create(mode:FileMode(owner:.readWriteExecute, group:.readWrite, other:.none))

Creating Intermediate Directories:

In the event you need to create the intermediate paths as well:

varfile=FilePath("/tmp/testdir/test")letopenFile:OpenFile=try file.create(options:.createIntermediates)

With Contents:

Paths whose Open<...> variation conforms to Writable can be created with predetermined contents:

varfile=FilePath("/tmp/test")try file.create(contents:"Hello World")print(try file.read()) // "Hello World"

With Closure:

Paths may also be opened for the duration of a provided closure:

varfile=FilePath("/tmp/test")try file.create(){ openFile intry openFile.write("Hello world")letcontents:String=try openFile.read(from:.beginning)print(contents) // Hello World
}

Deleting Paths

The current path only:

This is the same for all paths

varfile=FilePath("/tmp/test")try file.delete()

Recursively delete directories:

vardir=DirectoryPath("/tmp/test")try dir.recursiveDelete()

NOTE: Be VERY cautious with this as it cannot be undone (just like rm -rf).

Reading Files

letfile=FilePath("/tmp/test")
// All of the following operations are available on both a FilePath and an OpenFile
// Read the whole file
letcontents:String=try file.read()
// Read up to 1024 bytes
letcontents:String=try file.read(bytes:1024)
// Read content as ascii characters instead of utf8
letcontents:String=try file.read(encoding:.ascii)
// Read to the end, but starting at 1024 bytes from the beginning of the file
letcontents:String=try file.read(from:Offset(from:.beginning, bytes:1024))
// Read the last 1024 bytes from of the file using the ascii encoding
letcontents: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.

Writing Files

letfile=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.

Buffered File Writing

// 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

Getting Directory Contents:

Immediate children:

letdir=DirectoryPath("/tmp")letchildren=try dir.children()
// This same operation is safe, assuming you've already opened the directory
letopenDir=try dir.open()letchildren= openDir.children()print(children.files)print(children.directories)print(children.other)

Recursive children:

letdir=DirectoryPath("/tmp")letchildren=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)
letopenDir=try dir.open()letchildren=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
letchildren=try dir.recursiveChildren(depth:5)

Hidden Files:

// Both .children() and .recursiveChildren() support getting hidden files/directories (files that begin with a '.')
letchildren=try dir.children(options:.includeHidden)letrecursiveChildren=try dir.recursiveChildren(depth:5, options:.includeHidden)

Changing Path Metadata:

Ownership:

varpath=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:
vardir=DirectoryPath(path)try dir.recursiveChange(owner:"ponyboy47")

Permissions:

varpath=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:
vardir=DirectoryPath(path)try dir.recursiveChange(owner:.readWriteExecute, group:.readWrite, others:.read)

Moving Paths:

varpath=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")

Globbing:

letglobData=tryglob(pattern:"/tmp/*")
// Just like getting a directories children:
print(globData.files)print(globData.directories)print(globData.other)
// You can also glob from a DirectoryPath
lethome=DirectoryPath.home
letglobData=try home.glob("*.swift")print(globData.files)print(globData.directories)print(globData.other)

Temporary Paths:

Creating Temporary Paths:

lettmpFile=tryFilePath.temporary()
// /tmp/vDjKM1C
lettmpDir=tryDirectoryPath.temporary()
// /tmp/rYcznHQ
// You can optionally specify a prefix for the path name
lettmpFile=tryFilePath.temporary(prefix:"com.pathman.")
// /tmp/com.pathman.gHyiZq
// You can optionally specify a base directory where the temporary path will be stored
lettmpDirectory=tryDirectoryPath.temporary(base:DirectoryPath("/path/to/my/tmp")!, prefix:"com.pathman.")
// /path/to/my/tmp/com.pathman.2eH4iB

With Closure:

// When creating a temporary path with a closure, the path of the temporary
// file is returned instead of an Opened path
lettmpFile:FilePath=tryFilePath.temporary(){ openFile intry 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
tryFilePath.temporary(options:.deleteOnCompletion){ openFile intry openFile.write("Hello World")}

Links:

Target to Destination:

// You can link to an existing path
letdir=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
letlink=try dir.link(at:"~/tmpDir.link")letlink=try dir.link(at:"~/tmpDir.symbolic", type:.symbolic)letlink=try dir.link(at:"~/tmpDir.soft", type:.soft)
// Creates a hard link to dir at the specified path
letlink=try dir.link(at:"~/tmpDir.hard", type:.hard)

Destination from Target:

letlinkedFile=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
letlink=try linkedFile.link(from:"/path/to/link/target")letlink=try linkedFile.link(from:"/path/to/link/target", type:.symbolic)letlink=try linkedFile.link(from:"/path/to/link/target", type:.soft)
// Creates a hard link to dir at the specified path
letlink=try linkedFile.link(from:"/path/to/link/target", type:.hard)

Changing the Default Link Type:

Pathman uses .symbolic/.soft links as the default, but this may be changed.

Pathman.defaultLinkType =.hard

Copy Paths:

FilePath:

letfile=FilePath("/path/to/file")letcopyPath=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")

DirectoryPath:

letdir=DirectoryPath("/path/to/directory")letcopyPath=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])

About

Swift type-safe path, file, and directory library using POSIX C APIs

Topics

Resources

Stars

14 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Pathman

Build StatuscodecovMaintainabilityCurrent VersionSupported PlatformsLanguageLanguage VersionLicense
A type-safe path library for Apple's Swift language.

Motivation

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).

Goals

  • 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 NSErrors when FileManager throws
  • 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, and URL types
  • Ease of Use
    • No clunky interface just to get attributes of a path
      • Was anyone ever a fan of FileAttributeKeys?
  • Expose low-level control with high-level safety built-in

Installation

Compatibility:

  • Swift 5.0
  • Ubuntu
  • macOS

Swift Package Manager:

Add this to your Package.swift dependencies:

.package(url:"https://github.com/Ponyboy47/Pathman.git", from:"0.20.1")

Usage

Paths

There are 3 different Path types right now: GenericPath, FilePath, and DirectoryPath

// Paths can be initialized from Strings, Arrays, or Slices
letgenericString=GenericPath("/tmp")letgenericArray=GenericPath(["/","tmp"])letgenericSlice=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:
letfile=FilePath("/tmp/")letdirectory=DirectoryPath("/tmp/")

Path Information

// 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
varid:DeviceID
/// The inode of the path
varinode:Inode
/// The type of the path, if it exists
vartype:PathType
/// Whether the path exists
varexists:Bool
/// Whether the path exists and is a file
varisFile:Bool
/// Whether the path exists and is a directory
varisDirectory:Bool
/// Whether the path exists and is a link
varisLink:Bool
/// The URL representation of the path
varurl:URL
/// The permissions of the path
varpermissions:FileMode
/// The user id of the user that owns the path
varowner:UID
// The name of the user that owns the path
varownerName:String?
/// The group id of the user that owns the path
vargroup:GID
/// The name of the group that owns the path
vargroupName:String?
/// The device id (if special file)
vardevice:DeviceID
/// The total size, in bytes
varsize:OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The blocksize for filesystem I/O
varblockSize:BlockSize
/// The number of 512B block allocated
varblocks:OSOffsetInt
// macOS -> Int64
// Linux -> Int
/// The parent directory of the path
varparent:DirectoryPath
/// The pieces that make up the path
varcomponents:[String]
/// The final piece of the path (filename or directory name)
varlastComponent:String?
/// The final piece of the path with the extension stripped off
varlastComponentWithoutExtension:String?
/// The extension of the path
varextension:String?
/// The last time the path was accessed
varlastAccess:Date
/// The last time the path was modified
varlastModified:Date
/// The last time the path had a status change
varlastAttributeChange:Date
/// The time when the path was created (macOS only)
varcreation:Date

Opening Paths

FilePath:

letfile=FilePath("/tmp/test")letopenFile:OpenFile=try file.open(mode:"r+")
// Open files can be written to or read from (depending on the permissions used above)
letcontent:String=try openFile.read()try openFile.write(content)

DirectoryPath:

letdir=DirectoryPath("/tmp")letopenDir:OpenDirectory=try dir.open()
// Open directories can be traversed
letchildren= openDir.children()
// Recursively traversing directories requires opening sub-directories and may throw errors
letrecursiveChildren=try openDir.recursiveChildren()

With Closure:

Paths may also be opened for the duration of a provided closure:

letdir=DirectoryPath("/tmp")try dir.open(){ openDirectory inletchildren= openDirectory.children()print(children)}

Creating Paths

Any Path conforming to Openable:

varfile=FilePath("/tmp/test")
// Creates a file with the write permissions and returns the opened file
letopenFile:OpenFile=try file.create(mode:FileMode(owner:.readWriteExecute, group:.readWrite, other:.none))

Creating Intermediate Directories:

In the event you need to create the intermediate paths as well:

varfile=FilePath("/tmp/testdir/test")letopenFile:OpenFile=try file.create(options:.createIntermediates)

With Contents:

Paths whose Open<...> variation conforms to Writable can be created with predetermined contents:

varfile=FilePath("/tmp/test")try file.create(contents:"Hello World")print(try file.read()) // "Hello World"

With Closure:

Paths may also be opened for the duration of a provided closure:

varfile=FilePath("/tmp/test")try file.create(){ openFile intry openFile.write("Hello world")letcontents:String=try openFile.read(from:.beginning)print(contents) // Hello World
}

Deleting Paths

The current path only:

This is the same for all paths

varfile=FilePath("/tmp/test")try file.delete()

Recursively delete directories:

vardir=DirectoryPath("/tmp/test")try dir.recursiveDelete()

NOTE: Be VERY cautious with this as it cannot be undone (just like rm -rf).

Reading Files

letfile=FilePath("/tmp/test")
// All of the following operations are available on both a FilePath and an OpenFile
// Read the whole file
letcontents:String=try file.read()
// Read up to 1024 bytes
letcontents:String=try file.read(bytes:1024)
// Read content as ascii characters instead of utf8
letcontents:String=try file.read(encoding:.ascii)
// Read to the end, but starting at 1024 bytes from the beginning of the file
letcontents:String=try file.read(from:Offset(from:.beginning, bytes:1024))
// Read the last 1024 bytes from of the file using the ascii encoding
letcontents: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.

Writing Files

letfile=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.

Buffered File Writing

// 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

Getting Directory Contents:

Immediate children:

letdir=DirectoryPath("/tmp")letchildren=try dir.children()
// This same operation is safe, assuming you've already opened the directory
letopenDir=try dir.open()letchildren= openDir.children()print(children.files)print(children.directories)print(children.other)

Recursive children:

letdir=DirectoryPath("/tmp")letchildren=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)
letopenDir=try dir.open()letchildren=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
letchildren=try dir.recursiveChildren(depth:5)

Hidden Files:

// Both .children() and .recursiveChildren() support getting hidden files/directories (files that begin with a '.')
letchildren=try dir.children(options:.includeHidden)letrecursiveChildren=try dir.recursiveChildren(depth:5, options:.includeHidden)

Changing Path Metadata:

Ownership:

varpath=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:
vardir=DirectoryPath(path)try dir.recursiveChange(owner:"ponyboy47")

Permissions:

varpath=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:
vardir=DirectoryPath(path)try dir.recursiveChange(owner:.readWriteExecute, group:.readWrite, others:.read)

Moving Paths:

varpath=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")

Globbing:

letglobData=tryglob(pattern:"/tmp/*")
// Just like getting a directories children:
print(globData.files)print(globData.directories)print(globData.other)
// You can also glob from a DirectoryPath
lethome=DirectoryPath.home
letglobData=try home.glob("*.swift")print(globData.files)print(globData.directories)print(globData.other)

Temporary Paths:

Creating Temporary Paths:

lettmpFile=tryFilePath.temporary()
// /tmp/vDjKM1C
lettmpDir=tryDirectoryPath.temporary()
// /tmp/rYcznHQ
// You can optionally specify a prefix for the path name
lettmpFile=tryFilePath.temporary(prefix:"com.pathman.")
// /tmp/com.pathman.gHyiZq
// You can optionally specify a base directory where the temporary path will be stored
lettmpDirectory=tryDirectoryPath.temporary(base:DirectoryPath("/path/to/my/tmp")!, prefix:"com.pathman.")
// /path/to/my/tmp/com.pathman.2eH4iB

With Closure:

// When creating a temporary path with a closure, the path of the temporary
// file is returned instead of an Opened path
lettmpFile:FilePath=tryFilePath.temporary(){ openFile intry 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
tryFilePath.temporary(options:.deleteOnCompletion){ openFile intry openFile.write("Hello World")}

Links:

Target to Destination:

// You can link to an existing path
letdir=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
letlink=try dir.link(at:"~/tmpDir.link")letlink=try dir.link(at:"~/tmpDir.symbolic", type:.symbolic)letlink=try dir.link(at:"~/tmpDir.soft", type:.soft)
// Creates a hard link to dir at the specified path
letlink=try dir.link(at:"~/tmpDir.hard", type:.hard)

Destination from Target:

letlinkedFile=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
letlink=try linkedFile.link(from:"/path/to/link/target")letlink=try linkedFile.link(from:"/path/to/link/target", type:.symbolic)letlink=try linkedFile.link(from:"/path/to/link/target", type:.soft)
// Creates a hard link to dir at the specified path
letlink=try linkedFile.link(from:"/path/to/link/target", type:.hard)

Changing the Default Link Type:

Pathman uses .symbolic/.soft links as the default, but this may be changed.

Pathman.defaultLinkType =.hard

Copy Paths:

FilePath:

letfile=FilePath("/path/to/file")letcopyPath=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")

DirectoryPath:

letdir=DirectoryPath("/path/to/directory")letcopyPath=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])

About

Swift type-safe path, file, and directory library using POSIX C APIs

Topics

Resources

Stars

14 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages