Skip to content

Instantly share code, notes, and snippets.

@kristopherjohnson
Last active May 23, 2022 00:32
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kristopherjohnson/56c2f8606a3a6b264576c305e9f92e86 to your computer and use it in GitHub Desktop.
Save kristopherjohnson/56c2f8606a3a6b264576c305e9f92e86 to your computer and use it in GitHub Desktop.
Iterators for reading bytes or lines from FileHandle objects as sequences
import Foundation
extension FileHandle {
/// Return an iterator over the bytes in the file.
///
/// - returns: An iterator for UInt8 elements.
public func bytes() -> FileHandleByteIterator {
return FileHandleByteIterator(fileHandle: self)
}
/// Return an iterator over the lines in the file.
///
/// A "line" is a String ending in "\n" or the specified delimiter,
/// unless at the end of file.
///
/// - parameter encoding: Encoding of the text. Defaults to UTF8.
/// - parameter delimiter: Byte value that marks end of line. Defaults to LF.
///
/// - returns: An iterator for Strings.
public func lines(encoding: String.Encoding = .utf8, delimiter: UInt8 = 10) -> FileHandleLineIterator {
return FileHandleLineIterator(fileHandle: self, encoding: encoding, delimiter: delimiter)
}
/// Return an iterator over fixed-length blocks read from the file.
///
/// - parameter bytesPerBlock: Length of each block.
///
/// - returns: An iterator for Data elements.
public func blocks(ofLength bytesPerBlock: Int) -> FileHandleBlockIterator {
return FileHandleBlockIterator(fileHandle: self, bytesPerBlock: bytesPerBlock)
}
}
/// An iterator over the bytes read from a FileHandle.
public struct FileHandleByteIterator: IteratorProtocol, Sequence {
let fileHandle: FileHandle
public init(fileHandle: FileHandle) {
self.fileHandle = fileHandle
}
/// Read next byte.
///
/// - returns: Byte, or nil if at end of file.
public func next() -> UInt8? {
return fileHandle.readData(ofLength: 1).first
}
}
/// An iterator over the lines read from a FileHandle.
///
/// A "line" is a String ending with a LF byte or specified byte value.
/// Each returned line includes the delimiter, unless end-of-file was reached.
public struct FileHandleLineIterator: IteratorProtocol, Sequence {
let fileHandle: FileHandle
let encoding: String.Encoding
let delimiter: UInt8
/// Constructor
///
/// - parameter fileHandle: Open file handle.
/// - parameter encoding: Encoding to be used to convert byte sequences to Strings.
/// - parameter delimiter: Byte value that marks end of line.
public init(fileHandle: FileHandle, encoding: String.Encoding = .utf8, delimiter: UInt8 = 10) {
self.fileHandle = fileHandle
self.encoding = encoding
self.delimiter = delimiter
}
/// Read next line from file.
///
/// - returns: Next line, or nil if at end of file.
public func next() -> String? {
var readBuffer = readOneByte()
if readBuffer.count == 0 {
return nil
}
var lineData = readBuffer
var byte = readBuffer.first!
while byte != delimiter {
readBuffer = readOneByte()
if readBuffer.count == 0 {
break
}
lineData.append(readBuffer)
byte = readBuffer.first!
}
return String(data: lineData, encoding: encoding)
}
/// Read next byte.
///
/// - returns: Data containing one byte, or empty Data if at end of file.
///
/// - TODO: This implementation uses `FileHandle.readData(ofLength: 1)` to read a byte at a time. There may be more performant ways to read the data.
private func readOneByte() -> Data {
return fileHandle.readData(ofLength: 1)
}
}
/// An iterator that reads fixed-length blocks of data from a FileHandle.
public struct FileHandleBlockIterator: IteratorProtocol, Sequence {
let fileHandle: FileHandle
let bytesPerBlock: Int
/// Constructor.
///
/// - parameter fileHandle: Open file handle.
/// - parameter bytesPerBlock: Number of bytes in each fixed-size block.
public init(fileHandle: FileHandle, bytesPerBlock: Int) {
assert(bytesPerBlock > 0)
self.fileHandle = fileHandle
self.bytesPerBlock = bytesPerBlock
}
/// Read next block.
///
/// - returns: Data, or nil if at end of file.
public func next() -> Data? {
var block = fileHandle.readData(ofLength: bytesPerBlock)
if block.count == bytesPerBlock {
return block
}
else if block.count == 0 {
return nil
}
// Keep reading until we fill the block or hit end of file.
var nextData = fileHandle.readData(ofLength: bytesPerBlock - block.count)
while nextData.count > 0 {
block.append(nextData)
if block.count == bytesPerBlock {
return block
}
nextData = fileHandle.readData(ofLength: bytesPerBlock - block.count)
}
return block
}
}
@kristopherjohnson
Copy link
Author

kristopherjohnson commented Feb 11, 2017

Example usage:

// Example of using FileHandle.lines().
func printNumberedLines(fileHandle: FileHandle) {
    var lineNumber = 1
    for line in fileHandle.lines() {
        print("\(lineNumber): \(line)", terminator: "")
        lineNumber += 1
    }
    print()
}

printNumberedLines(fileHandle: FileHandle.standardInput)
// Example of using FileHandle.bytes().
func hexDump(fileHandle: FileHandle) {
    var offset = 0
    for byte in fileHandle.bytes() {
        if offset % 16 == 0 {
            print(String(format: "\n%08x ", offset), terminator: "")
        }
        print(String(format: " %02x", Int(byte)), terminator: "")
        offset += 1
    }
    print()
}

hexDump(fileHandle: FileHandle.standardInput)
// Example of using FileHandle.blocks(ofLength:).
func hexDumpWithAscii(fileHandle: FileHandle, bytesPerLine: Int = 16) {

    func write(_ text: String) {
        print(text, terminator: "")
    }

    func hexRepresentation(byte: UInt8) -> String {
        return String(format: " %02x", UInt(byte))
    }

    func asciiRepresentation(byte: UInt8) -> String {
        switch byte {
        case 0x20...0x7e:
            var buffer = byte
            return String(bytesNoCopy: &buffer, length: 1, encoding: .ascii, freeWhenDone: false) ?? "."
        default:
            return "."
        }
    }

    var offset = 0
    fileHandle.blocks(ofLength: bytesPerLine).lazy.forEach { bytes in
        write(String(format: "%08x ", offset))

        bytes.lazy.map(hexRepresentation).forEach(write)

        // If last line has fewer than bytesPerLine bytes, then
        // fill space so ASCII column will line up.
        for _ in 0..<(bytesPerLine - bytes.count) {
            write("   ")
        }

        write("  |")
        bytes.lazy.map(asciiRepresentation).forEach(write)
        print("|")

        offset += bytes.count
    }
}

hexDumpWithAscii(fileHandle: FileHandle.standardInput, bytesPerLine: 32)

@ShikiSuen
Copy link

Excuse me. Could you please tell me which license are you going to use for this Swift snippet?

@kristopherjohnson
Copy link
Author

kristopherjohnson commented May 22, 2022

I consider it to be public domain.

If "public domain" isn't meaningful for a particular jurisdiction or use, then the MIT license terms are fine. I'm not going to enforce any sort of copyright terms for this.

@ShikiSuen
Copy link

@kristopherjohnson Copy that. Thanks for your explanation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment