├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Package.resolved ├── Package.swift ├── README.md ├── StreamReader.swift └── tea.yaml /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | paths: 4 | - '*.swift' 5 | - .github/workflows/ci.yml 6 | schedule: 7 | - cron: '3 4 * * 6' # 3:04 AM, every Saturday 8 | concurrency: 9 | group: ${{ github.head_ref || 'cron' }} 10 | cancel-in-progress: true 11 | jobs: 12 | apple: 13 | runs-on: ${{ matrix.os || 'macos-latest' }} 14 | strategy: 15 | matrix: 16 | xcode: 17 | - 10 18 | - 11 19 | - 12 20 | os: 21 | - macos-10.15 22 | include: 23 | - xcode: 13 24 | os: macos-11 25 | - xcode: 12 26 | os: macos-11 27 | - xcode: ~ 28 | platform: watchOS 29 | os: ~ 30 | - xcode: ~ 31 | platform: iOS 32 | os: ~ 33 | - xcode: ~ 34 | platform: tvOS 35 | os: ~ 36 | - xcode: ~ 37 | platform: macOS 38 | os: ~ 39 | steps: 40 | - uses: actions/checkout@v2 41 | - uses: mxcl/xcodebuild@v1 42 | with: 43 | xcode: ${{ matrix.xcode }} 44 | platform: ${{ matrix.platform }} 45 | action: build 46 | warnings-as-errors: true 47 | linux: 48 | runs-on: ubuntu-latest 49 | strategy: 50 | matrix: 51 | image: 52 | - swift:4.2 53 | - swift:5.4 54 | - swiftlang/swift:nightly-5.5 55 | container: 56 | image: ${{ matrix.image }} 57 | steps: 58 | - uses: actions/checkout@v2 59 | - run: swift build 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | /.swiftpm 6 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Path.swift", 6 | "repositoryURL": "https://github.com/mxcl/Path.swift", 7 | "state": { 8 | "branch": null, 9 | "revision": "39f81ae258dcbb6a3d3995ac9547dd6b073179a4", 10 | "version": "1.2.1" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "StreamReader", 6 | products: [ 7 | .library(name: "StreamReader", targets: ["StreamReader"]), 8 | ], 9 | dependencies: [ 10 | .package(url: "https://github.com/mxcl/Path.swift", from: "1.0.0-alpha.1"), 11 | ], 12 | targets: [ 13 | .target(name: "StreamReader", dependencies: ["Path"], path: ".", sources: ["StreamReader.swift"]), 14 | ] 15 | ) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StreamReader 2 | 3 | Efficiently reads a file delimited by a character (by default `\n`). The delimiter is omitted from the returned `String`. Can be iterated. 4 | 5 | ```swift 6 | import StreamReader 7 | 8 | for line in StreamReader(path: Path.cwd/"foo") { 9 | print(line) 10 | } 11 | 12 | StreamReader(path: Path.home/"bar").compactMap(Int.init) 13 | ``` 14 | -------------------------------------------------------------------------------- /StreamReader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Path 3 | 4 | // I confess I copy and pasted this from StackOverflow.com 5 | // But then like I modified it heavily, so it’s more mine now. 6 | 7 | public class StreamReader { 8 | let encoding = String.Encoding.utf8 9 | let chunkSize = 4096 10 | var fileHandle: FileHandle! 11 | let delimiter = Data(repeating: 10, count: 1) 12 | var buffer: Data 13 | var atEof = false 14 | 15 | public struct OpenError: LocalizedError { 16 | public let path: Path 17 | public var errorDescription: String? { 18 | return "could not open: \(path)" 19 | } 20 | } 21 | 22 | public convenience init(path: Path) throws { 23 | guard let fileHandle = FileHandle(forReadingAtPath: path.string) else { 24 | throw OpenError(path: path) 25 | } 26 | self.init(fileHandle: fileHandle) 27 | } 28 | 29 | public init(fileHandle: FileHandle) { 30 | self.fileHandle = fileHandle 31 | self.buffer = Data(capacity: chunkSize) 32 | } 33 | 34 | deinit { 35 | close() 36 | } 37 | 38 | /// Return next line, or nil on EOF. 39 | public func pop() -> String? { 40 | precondition(fileHandle != nil, "Attempt to read from closed file") 41 | 42 | // Read data chunks from file until a line delimiter is found: 43 | while !atEof { 44 | if let range = buffer.range(of: delimiter) { 45 | // Convert complete line (excluding the delimiter) to a string: 46 | let line = String(data: buffer.subdata(in: 0.. 0 { 53 | buffer.append(tmpData) 54 | } else { 55 | // EOF or read error. 56 | atEof = true 57 | if buffer.count > 0 { 58 | // Buffer contains last line in file (not terminated by delimiter). 59 | let line = String(data: buffer as Data, encoding: encoding) 60 | buffer.count = 0 61 | return line 62 | } 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | /// Start reading from the beginning of file. 69 | func rewind() -> Void { 70 | fileHandle.seek(toFileOffset: 0) 71 | buffer.count = 0 72 | atEof = false 73 | } 74 | 75 | /// Close the underlying file. No reading must be done after calling this method. 76 | func close() -> Void { 77 | fileHandle?.closeFile() 78 | fileHandle = nil 79 | } 80 | } 81 | 82 | extension StreamReader: Sequence { 83 | public func makeIterator() -> AnyIterator { 84 | return AnyIterator { 85 | return self.pop() 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | # created with https://mash.pkgx.sh/mxcl/tea-register 3 | --- 4 | version: 1.0.0 5 | codeOwners: 6 | - '0x5E2DE4A68df811AAAD32d71fb065e6946fA5C8d9' # mxcl 7 | quorum: 1 8 | --------------------------------------------------------------------------------