├── test ├── data │ ├── empty_file.txt │ ├── one_line_file.txt │ ├── multibyte_file.txt │ ├── separator_file.txt │ ├── multi_separator_file.txt │ ├── windows_buffer_overlap_file.txt │ ├── one_line_file_no_endline.txt │ ├── three_line_file.txt │ ├── mac_os_9_file.txt │ ├── normal_file.txt │ ├── unix_file.txt │ └── windows_file.txt └── line_reader.js ├── .gitignore ├── package.json ├── LICENSE ├── README.md └── lib └── line_reader.js /test/data/empty_file.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/data/one_line_file.txt: -------------------------------------------------------------------------------- 1 | one line file 2 | -------------------------------------------------------------------------------- /test/data/multibyte_file.txt: -------------------------------------------------------------------------------- 1 | ふうりうの初やおくの田植うた 2 | 3 | -------------------------------------------------------------------------------- /test/data/separator_file.txt: -------------------------------------------------------------------------------- 1 | foo;bar 2 | ;baz 3 | -------------------------------------------------------------------------------- /test/data/multi_separator_file.txt: -------------------------------------------------------------------------------- 1 | foo||bar 2 | ||baz 3 | -------------------------------------------------------------------------------- /test/data/windows_buffer_overlap_file.txt: -------------------------------------------------------------------------------- 1 | test 2 | file -------------------------------------------------------------------------------- /test/data/one_line_file_no_endline.txt: -------------------------------------------------------------------------------- 1 | one line file no endline -------------------------------------------------------------------------------- /test/data/three_line_file.txt: -------------------------------------------------------------------------------- 1 | This is line one. 2 | This is line two. 3 | This is line three. 4 | -------------------------------------------------------------------------------- /test/data/mac_os_9_file.txt: -------------------------------------------------------------------------------- 1 | Jabberwocky ’Twas brillig, and the slithy toves Did gyre and gimble in the wabe; -------------------------------------------------------------------------------- /test/data/normal_file.txt: -------------------------------------------------------------------------------- 1 | Jabberwocky 2 | 3 | ’Twas brillig, and the slithy toves 4 | Did gyre and gimble in the wabe; 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/data/unix_file.txt: -------------------------------------------------------------------------------- 1 | Jabberwocky 2 | 3 | ’Twas brillig, and the slithy toves 4 | Did gyre and gimble in the wabe; 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/data/windows_file.txt: -------------------------------------------------------------------------------- 1 | Jabberwocky 2 | 3 | ’Twas brillig, and the slithy toves 4 | Did gyre and gimble in the wabe; 5 | 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directories 28 | node_modules 29 | jspm_packages 30 | 31 | # Optional npm cache directory 32 | .npm 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "line-reader", 3 | "version": "0.4.0", 4 | "description": "Asynchronous, buffered, line-by-line file/stream reader", 5 | "url": "https://github.com/nickewing/line-reader", 6 | "keywords": [ 7 | "file", 8 | "line", 9 | "reader", 10 | "scanner" 11 | ], 12 | "author": "Nick Ewing ", 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/nickewing/line-reader/issues" 16 | }, 17 | "directories": { 18 | "lib": "./lib" 19 | }, 20 | "engines": { 21 | "node": ">= 0.10.0" 22 | }, 23 | "main": "./lib/line_reader", 24 | "scripts": { 25 | "test": "node_modules/.bin/mocha test/line_reader.js" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://nickewing@github.com/nickewing/line-reader.git" 30 | }, 31 | "devDependencies": { 32 | "mocha": "^2.4.5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 by Nick Ewing 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Line Reader 2 | =========== 3 | [![npm version](https://img.shields.io/npm/v/line-reader.svg)](https://www.npmjs.com/package/line-reader) 4 | [![Downloads/month](https://img.shields.io/npm/dm/line-reader.svg)](https://www.npmjs.com/package/line-reader) 5 | 6 | Asynchronous, buffered, line-by-line file/stream reader with support for 7 | user-defined line separators. 8 | 9 | Install 10 | ------- 11 | 12 | `npm install line-reader` 13 | 14 | Usage 15 | ----- 16 | 17 | The `eachLine` function reads each line of the given file. Upon each new line, 18 | the given callback function is called with two parameters: the line read and a 19 | boolean value specifying whether the line read was the last line of the file. 20 | If the callback returns `false`, reading will stop and the file will be closed. 21 | 22 | ```javascript 23 | var lineReader = require('line-reader'); 24 | 25 | lineReader.eachLine('file.txt', function(line, last) { 26 | console.log(line); 27 | 28 | if (/* done */) { 29 | return false; // stop reading 30 | } 31 | }); 32 | ``` 33 | 34 | `eachLine` can also be used in an asynchronous manner by providing a third 35 | callback parameter like so: 36 | 37 | ```javascript 38 | var lineReader = require('line-reader'); 39 | 40 | lineReader.eachLine('file.txt', function(line, last, cb) { 41 | console.log(line); 42 | 43 | if (/* done */) { 44 | cb(false); // stop reading 45 | } else { 46 | cb(); 47 | } 48 | }); 49 | ``` 50 | 51 | You can provide an optional second node-style callback that will be called with 52 | `(err)` on failure or `()` when finished (even if you manually terminate iteration 53 | by returning `false` from the iteratee): 54 | 55 | ```javascript 56 | var lineReader = require('line-reader'); 57 | 58 | // read all lines: 59 | lineReader.eachLine('file.txt', function(line) { 60 | console.log(line); 61 | }, function (err) { 62 | if (err) throw err; 63 | console.log("I'm done!!"); 64 | }); 65 | ``` 66 | 67 | For more granular control, `open`, `hasNextLine`, and `nextLine` maybe be used 68 | to iterate a file (but you must `close` it yourself): 69 | 70 | ```javascript 71 | // or read line by line: 72 | lineReader.open('file.txt', function(err, reader) { 73 | if (err) throw err; 74 | if (reader.hasNextLine()) { 75 | reader.nextLine(function(err, line) { 76 | try { 77 | if (err) throw err; 78 | console.log(line); 79 | } finally { 80 | reader.close(function(err) { 81 | if (err) throw err; 82 | }); 83 | } 84 | }); 85 | } 86 | else { 87 | reader.close(function(err) { 88 | if (err) throw err; 89 | }); 90 | } 91 | }); 92 | ``` 93 | 94 | You may provide additional options in a hash before the callbacks to `eachLine` or `open`: 95 | * `separator` - a `string` or `RegExp` separator (defaults to `/\r\n?|\n/`) 96 | * `encoding` - file encoding (defaults to `'utf8'`) 97 | * `bufferSize` - amount of bytes to buffer (defaults to 1024) 98 | 99 | For example: 100 | 101 | ```javascript 102 | lineReader.eachLine('file.txt', {separator: ';', encoding: 'utf8'}, function(line, last, cb) { 103 | console.log(line); 104 | }); 105 | lineReader.open('file.txt', {bufferSize: 1024}, function(err, reader) { 106 | ... 107 | }); 108 | ``` 109 | 110 | Streams 111 | ------- 112 | 113 | Both `eachLine` and `open` support passing either a file name or a read stream: 114 | 115 | ```javascript 116 | // reading from stdin 117 | lineReader.eachLine(process.stdin, function(line) {}); 118 | 119 | // reading with file position boundaries 120 | var readStream = fs.createReadStream('test.log', { start: 0, end: 10000 }); 121 | lineReader.eachLine(readStream, function(line) {}); 122 | ``` 123 | 124 | Note however that if you're reading user input from stdin then the 125 | [readline module](https://nodejs.org/api/readline.html) is probably a better choice. 126 | 127 | Promises 128 | -------- 129 | 130 | `eachLine` and `open` are compatible with `promisify` from [bluebird](https://github.com/petkaantonov/bluebird/blob/master/API.md#promisepromisifyfunction-nodefunction--dynamic-receiver---function): 131 | 132 | ```javascript 133 | var lineReader = require('line-reader'), 134 | Promise = require('bluebird'); 135 | 136 | var eachLine = Promise.promisify(lineReader.eachLine); 137 | eachLine('file.txt', function(line) { 138 | console.log(line); 139 | }).then(function() { 140 | console.log('done'); 141 | }).catch(function(err) { 142 | console.error(err); 143 | }); 144 | ``` 145 | 146 | If you're using a promise library that doesn't have a promisify function, here's how you can do it: 147 | 148 | ```javascript 149 | var lineReader = require('line-reader'), 150 | Promise = require(...); 151 | 152 | var eachLine = function(filename, options, iteratee) { 153 | return new Promise(function(resolve, reject) { 154 | lineReader.eachLine(filename, options, iteratee, function(err) { 155 | if (err) { 156 | reject(err); 157 | } else { 158 | resolve(); 159 | } 160 | }); 161 | }); 162 | } 163 | eachLine('file.txt', function(line) { 164 | console.log(line); 165 | }).then(function() { 166 | console.log('done'); 167 | }).catch(function(err) { 168 | console.error(err); 169 | }); 170 | ``` 171 | 172 | Contributors 173 | ------------ 174 | 175 | * Nick Ewing 176 | * Andy Edwards (jedwards1211) 177 | * Jameson Little (beatgammit) 178 | * Masum (masumsoft) 179 | * Matthew Caruana Galizia (mattcg) 180 | * Ricardo Bin (ricardohbin) 181 | 182 | Paul Em has also written a reverse-version of this gem to read files from bottom to top: [reverse-line-reader](https://github.com/paul-em/reverse-line-reader). 183 | 184 | Copyright 2011 Nick Ewing. 185 | -------------------------------------------------------------------------------- /lib/line_reader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'), 4 | StringDecoder = require('string_decoder').StringDecoder; 5 | 6 | function createLineReader(readStream, options, creationCb) { 7 | if (options instanceof Function) { 8 | creationCb = options; 9 | options = undefined; 10 | } 11 | if (!options) options = {}; 12 | 13 | var encoding = options.encoding || 'utf8', 14 | separator = options.separator || /\r\n?|\n/, 15 | bufferSize = options.bufferSize || 1024, 16 | bufferStr = '', 17 | decoder = new StringDecoder(encoding), 18 | closed = false, 19 | eof = false, 20 | separatorIndex = -1, 21 | separatorLen, 22 | readDefer, 23 | moreToRead = false, 24 | findSeparator; 25 | 26 | if (separator instanceof RegExp) { 27 | findSeparator = function() { 28 | var result = separator.exec(bufferStr); 29 | if (result && (result.index + result[0].length < bufferStr.length || eof)) { 30 | separatorIndex = result.index; 31 | separatorLen = result[0].length; 32 | } else { 33 | separatorIndex = -1; 34 | separatorLen = 0; 35 | } 36 | }; 37 | } else { 38 | separatorLen = separator.length; 39 | findSeparator = function() { 40 | separatorIndex = bufferStr.indexOf(separator); 41 | }; 42 | } 43 | 44 | function getReadStream() { 45 | return readStream; 46 | } 47 | 48 | function close(cb) { 49 | if (!closed) { 50 | closed = true; 51 | if (typeof readStream.close == 'function') { 52 | readStream.close(); 53 | } 54 | setImmediate(cb); 55 | } 56 | } 57 | 58 | function onFailure(err) { 59 | close(function(err2) { 60 | return creationCb(err || err2); 61 | }); 62 | } 63 | 64 | function isOpen() { 65 | return !closed; 66 | } 67 | 68 | function isClosed() { 69 | return closed; 70 | } 71 | 72 | function waitForMoreToRead(cb) { 73 | if (moreToRead) { 74 | cb(); 75 | } else { 76 | readDefer = cb; 77 | } 78 | } 79 | 80 | function resumeDeferredRead() { 81 | if (readDefer) { 82 | readDefer(); 83 | readDefer = null; 84 | } 85 | } 86 | 87 | function read(cb) { 88 | waitForMoreToRead(function() { 89 | var chunk; 90 | 91 | try { 92 | chunk = readStream.read(bufferSize); 93 | } catch (err) { 94 | cb(err); 95 | } 96 | 97 | if (chunk) { 98 | bufferStr += decoder.write(chunk.slice(0, chunk.length)); 99 | } else { 100 | moreToRead = false; 101 | } 102 | 103 | cb(); 104 | }); 105 | } 106 | 107 | function onStreamReadable() { 108 | moreToRead = true; 109 | resumeDeferredRead(); 110 | } 111 | 112 | function onStreamEnd() { 113 | eof = true; 114 | resumeDeferredRead(); 115 | } 116 | 117 | readStream.on('readable', onStreamReadable); 118 | readStream.on('end', onStreamEnd); 119 | readStream.on('error', onFailure); 120 | 121 | function shouldReadMore() { 122 | findSeparator(); 123 | 124 | return separatorIndex < 0 && !eof; 125 | } 126 | 127 | function callWhile(conditionFn, bodyFn, doneCallback) { 128 | if (conditionFn()) { 129 | bodyFn(function (err) { 130 | if (err) { 131 | doneCallback(err); 132 | } else { 133 | setImmediate(callWhile, conditionFn, bodyFn, doneCallback); 134 | } 135 | }); 136 | } else { 137 | doneCallback(); 138 | } 139 | } 140 | 141 | function readToSeparator(cb) { 142 | callWhile(shouldReadMore, read, cb); 143 | } 144 | 145 | function hasNextLine() { 146 | return bufferStr.length > 0 || !eof; 147 | } 148 | 149 | function nextLine(cb) { 150 | if (closed) { 151 | return cb(new Error('LineReader has been closed')); 152 | } 153 | 154 | function getLine(err) { 155 | if (err) { 156 | return cb(err); 157 | } 158 | 159 | if (separatorIndex < 0 && eof) { 160 | separatorIndex = bufferStr.length; 161 | } 162 | var ret = bufferStr.substring(0, separatorIndex); 163 | 164 | bufferStr = bufferStr.substring(separatorIndex + separatorLen); 165 | separatorIndex = -1; 166 | cb(undefined, ret); 167 | } 168 | 169 | findSeparator(); 170 | 171 | if (separatorIndex < 0) { 172 | if (eof) { 173 | if (hasNextLine()) { 174 | separatorIndex = bufferStr.length; 175 | getLine(); 176 | } else { 177 | return cb(new Error('No more lines to read.')); 178 | } 179 | } else { 180 | readToSeparator(getLine); 181 | } 182 | } else { 183 | getLine(); 184 | } 185 | } 186 | 187 | readToSeparator(function(err) { 188 | if (err) { 189 | onFailure(err); 190 | } else { 191 | return creationCb(undefined, { 192 | hasNextLine: hasNextLine, 193 | nextLine: nextLine, 194 | close: close, 195 | isOpen: isOpen, 196 | isClosed: isClosed, 197 | getReadStream: getReadStream 198 | }); 199 | } 200 | }); 201 | } 202 | 203 | function open(filenameOrStream, options, cb) { 204 | if (options instanceof Function) { 205 | cb = options; 206 | options = undefined; 207 | } 208 | 209 | var readStream; 210 | 211 | if (typeof filenameOrStream.read == 'function') { 212 | readStream = filenameOrStream; 213 | } else if (typeof filenameOrStream === 'string' || filenameOrStream instanceof String) { 214 | readStream = fs.createReadStream(filenameOrStream); 215 | } else { 216 | cb(new Error('Invalid file argument for LineReader.open. Must be filename or stream.')); 217 | return; 218 | } 219 | 220 | readStream.pause(); 221 | createLineReader(readStream, options, cb); 222 | } 223 | 224 | function eachLine(filename, options, iteratee, cb) { 225 | if (options instanceof Function) { 226 | cb = iteratee; 227 | iteratee = options; 228 | options = undefined; 229 | } 230 | var asyncIteratee = iteratee.length === 3; 231 | 232 | var theReader; 233 | var getReaderCb; 234 | 235 | open(filename, options, function(err, reader) { 236 | theReader = reader; 237 | if (getReaderCb) { 238 | getReaderCb(reader); 239 | } 240 | 241 | if (err) { 242 | if (cb) cb(err); 243 | return; 244 | } 245 | 246 | function finish(err) { 247 | reader.close(function(err2) { 248 | if (cb) cb(err || err2); 249 | }); 250 | } 251 | 252 | function newRead() { 253 | if (reader.hasNextLine()) { 254 | setImmediate(readNext); 255 | } else { 256 | finish(); 257 | } 258 | } 259 | 260 | function continueCb(continueReading) { 261 | if (continueReading !== false) { 262 | newRead(); 263 | } else { 264 | finish(); 265 | } 266 | } 267 | 268 | function readNext() { 269 | reader.nextLine(function(err, line) { 270 | if (err) { 271 | finish(err); 272 | } 273 | 274 | var last = !reader.hasNextLine(); 275 | 276 | if (asyncIteratee) { 277 | iteratee(line, last, continueCb); 278 | } else { 279 | if (iteratee(line, last) !== false) { 280 | newRead(); 281 | } else { 282 | finish(); 283 | } 284 | } 285 | }); 286 | } 287 | 288 | newRead(); 289 | }); 290 | 291 | // this hook is only for the sake of testing; if you choose to use it, 292 | // please don't file any issues (unless you can also reproduce them without 293 | // using this). 294 | return { 295 | getReader: function(cb) { 296 | if (theReader) { 297 | cb(theReader); 298 | } else { 299 | getReaderCb = cb; 300 | } 301 | } 302 | }; 303 | } 304 | 305 | module.exports.open = open; 306 | module.exports.eachLine = eachLine; 307 | -------------------------------------------------------------------------------- /test/line_reader.js: -------------------------------------------------------------------------------- 1 | var lineReader = require('../lib/line_reader'), 2 | assert = require('assert'), 3 | fs = require('fs'), 4 | testFilePath = __dirname + '/data/normal_file.txt', 5 | windowsFilePath = __dirname + '/data/windows_file.txt', 6 | windowsBufferOverlapFilePath = __dirname + '/data/windows_buffer_overlap_file.txt', 7 | unixFilePath = __dirname + '/data/unix_file.txt', 8 | macOs9FilePath = __dirname + '/data/mac_os_9_file.txt', 9 | separatorFilePath = __dirname + '/data/separator_file.txt', 10 | multiSeparatorFilePath = __dirname + '/data/multi_separator_file.txt', 11 | multibyteFilePath = __dirname + '/data/multibyte_file.txt', 12 | emptyFilePath = __dirname + '/data/empty_file.txt', 13 | oneLineFilePath = __dirname + '/data/one_line_file.txt', 14 | oneLineFileNoEndlinePath = __dirname + '/data/one_line_file_no_endline.txt', 15 | threeLineFilePath = __dirname + '/data/three_line_file.txt', 16 | testSeparatorFile = ['foo', 'bar\n', 'baz\n'], 17 | testFile = [ 18 | 'Jabberwocky', 19 | '', 20 | '’Twas brillig, and the slithy toves', 21 | 'Did gyre and gimble in the wabe;', 22 | '', 23 | '' 24 | ], 25 | testBufferOverlapFile = [ 26 | 'test', 27 | 'file' 28 | ]; 29 | 30 | describe("lineReader", function() { 31 | describe("eachLine", function() { 32 | it("should read lines using the default separator", function(done) { 33 | var i = 0; 34 | 35 | lineReader.eachLine(testFilePath, function(line, last) { 36 | assert.equal(testFile[i], line, 'Each line should be what we expect'); 37 | i += 1; 38 | 39 | if (i === 6) { 40 | assert.ok(last); 41 | } else { 42 | assert.ok(!last); 43 | } 44 | }, function(err) { 45 | assert.ok(!err); 46 | assert.equal(6, i); 47 | done(); 48 | }); 49 | }); 50 | 51 | it("should read windows files by default", function(done) { 52 | var i = 0; 53 | 54 | lineReader.eachLine(windowsFilePath, function(line, last) { 55 | assert.equal(testFile[i], line, 'Each line should be what we expect'); 56 | i += 1; 57 | 58 | if (i === 6) { 59 | assert.ok(last); 60 | } else { 61 | assert.ok(!last); 62 | } 63 | }, function(err) { 64 | assert.ok(!err); 65 | assert.equal(6, i); 66 | done(); 67 | }); 68 | }); 69 | 70 | it("should handle \\r\\n overlapping buffer window correctly", function(done) { 71 | var i = 0; 72 | var bufferSize = 5; 73 | 74 | lineReader.eachLine(windowsBufferOverlapFilePath, {bufferSize: bufferSize}, function(line, last) { 75 | assert.equal(testBufferOverlapFile[i], line, 'Each line should be what we expect'); 76 | i += 1; 77 | 78 | if (i === 2) { 79 | assert.ok(last); 80 | } else { 81 | assert.ok(!last); 82 | } 83 | }, function(err) { 84 | assert.ok(!err); 85 | assert.equal(2, i); 86 | done(); 87 | }); 88 | }); 89 | 90 | it("should read unix files by default", function(done) { 91 | var i = 0; 92 | 93 | lineReader.eachLine(unixFilePath, function(line, last) { 94 | assert.equal(testFile[i], line, 'Each line should be what we expect'); 95 | i += 1; 96 | 97 | if (i === 6) { 98 | assert.ok(last); 99 | } else { 100 | assert.ok(!last); 101 | } 102 | }, function(err) { 103 | assert.ok(!err); 104 | assert.equal(6, i); 105 | done(); 106 | }); 107 | }); 108 | 109 | it("should read mac os 9 files by default", function(done) { 110 | var i = 0; 111 | 112 | lineReader.eachLine(macOs9FilePath, function(line, last) { 113 | assert.equal(testFile[i], line, 'Each line should be what we expect'); 114 | i += 1; 115 | 116 | if (i === 6) { 117 | assert.ok(last); 118 | } else { 119 | assert.ok(!last); 120 | } 121 | }, function(err) { 122 | assert.ok(!err); 123 | assert.equal(6, i); 124 | done(); 125 | }); 126 | }); 127 | 128 | it("should allow continuation of line reading via a callback", function(done) { 129 | var i = 0; 130 | 131 | lineReader.eachLine(testFilePath, function(line, last, cb) { 132 | assert.equal(testFile[i], line, 'Each line should be what we expect'); 133 | i += 1; 134 | 135 | if (i === 6) { 136 | assert.ok(last); 137 | } else { 138 | assert.ok(!last); 139 | } 140 | 141 | process.nextTick(cb); 142 | }, function(err) { 143 | assert.ok(!err); 144 | assert.equal(6, i); 145 | done(); 146 | }); 147 | }); 148 | 149 | it("should separate files using given separator", function(done) { 150 | var i = 0; 151 | lineReader.eachLine(separatorFilePath, {separator: ';'}, function(line, last) { 152 | assert.equal(testSeparatorFile[i], line); 153 | i += 1; 154 | 155 | if (i === 3) { 156 | assert.ok(last); 157 | } else { 158 | assert.ok(!last); 159 | } 160 | }, function(err) { 161 | assert.ok(!err); 162 | assert.equal(3, i); 163 | done(); 164 | }); 165 | }); 166 | 167 | it("should separate files using given separator with more than one character", function(done) { 168 | var i = 0; 169 | lineReader.eachLine(multiSeparatorFilePath, {separator: '||'}, function(line, last) { 170 | assert.equal(testSeparatorFile[i], line); 171 | i += 1; 172 | 173 | if (i === 3) { 174 | assert.ok(last); 175 | } else { 176 | assert.ok(!last); 177 | } 178 | }, function(err) { 179 | assert.ok(!err); 180 | assert.equal(3, i); 181 | done(); 182 | }); 183 | }); 184 | 185 | it("should allow early termination of line reading", function(done) { 186 | var i = 0; 187 | lineReader.eachLine(testFilePath, function(line, last) { 188 | assert.equal(testFile[i], line, 'Each line should be what we expect'); 189 | i += 1; 190 | 191 | if (i === 2) { 192 | return false; 193 | } 194 | }, function(err) { 195 | assert.ok(!err); 196 | assert.equal(2, i); 197 | done(); 198 | }); 199 | }); 200 | 201 | it("should allow early termination of line reading via a callback", function(done) { 202 | var i = 0; 203 | lineReader.eachLine(testFilePath, function(line, last, cb) { 204 | assert.equal(testFile[i], line, 'Each line should be what we expect'); 205 | i += 1; 206 | 207 | if (i === 2) { 208 | cb(false); 209 | } else { 210 | cb(); 211 | } 212 | 213 | }, function(err) { 214 | assert.ok(!err); 215 | assert.equal(2, i); 216 | done(); 217 | }); 218 | }); 219 | 220 | it("should not call callback on empty file", function(done) { 221 | lineReader.eachLine(emptyFilePath, function(line) { 222 | assert.ok(false, "Empty file should not cause any callbacks"); 223 | }, function(err) { 224 | assert.ok(!err); 225 | done() 226 | }); 227 | }); 228 | 229 | it("should error when the user tries calls nextLine on a closed LineReader", function(done) { 230 | lineReader.open(oneLineFilePath, function(err, reader) { 231 | assert.ok(!err); 232 | reader.close(function(err) { 233 | assert.ok(!err); 234 | reader.nextLine(function(err, line) { 235 | assert.ok(err, "nextLine should have errored because the reader is closed"); 236 | done(); 237 | }); 238 | }); 239 | }); 240 | }); 241 | 242 | it("should work with a file containing only one line", function(done) { 243 | lineReader.eachLine(oneLineFilePath, function(line, last) { 244 | return true; 245 | }, function(err) { 246 | assert.ok(!err); 247 | done(); 248 | }); 249 | }); 250 | 251 | it("should work with a file containing only one line and no endline character.", function(done) { 252 | var count = 0; var isDone = false; 253 | lineReader.eachLine(oneLineFileNoEndlinePath, function(line, last) { 254 | assert.equal(last, true, 'last should be true'); 255 | return true; 256 | }, function(err) { 257 | assert.ok(!err); 258 | done(); 259 | }); 260 | }); 261 | 262 | it("should close the file when eachLine finishes", function(done) { 263 | var reader; 264 | lineReader.eachLine(oneLineFilePath, function(line, last) { 265 | return false; 266 | }, function(err) { 267 | assert.ok(!err); 268 | assert.ok(reader.isClosed()); 269 | done(); 270 | }).getReader(function(_reader) { 271 | reader = _reader; 272 | }); 273 | }); 274 | 275 | it("should close the file if there is an error during eachLine", function(done) { 276 | lineReader.eachLine(testFilePath, {bufferSize: 10}, function(line, last) { 277 | }, function(err) { 278 | assert.equal('a test error', err.message); 279 | assert.ok(reader.isClosed()); 280 | done(); 281 | }).getReader(function(_reader) { 282 | reader = _reader; 283 | 284 | reader.getReadStream().read = function() { 285 | throw new Error('a test error'); 286 | }; 287 | }); 288 | }); 289 | }); 290 | 291 | describe("open", function() { 292 | it("should return a reader object and allow calls to nextLine", function(done) { 293 | lineReader.open(testFilePath, function(err, reader) { 294 | assert.ok(!err); 295 | assert.ok(reader.hasNextLine()); 296 | 297 | assert.ok(reader.hasNextLine(), 'Calling hasNextLine multiple times should be ok'); 298 | 299 | reader.nextLine(function(err, line) { 300 | assert.ok(!err); 301 | assert.equal('Jabberwocky', line); 302 | assert.ok(reader.hasNextLine()); 303 | reader.nextLine(function(err, line) { 304 | assert.ok(!err); 305 | assert.equal('', line); 306 | assert.ok(reader.hasNextLine()); 307 | reader.nextLine(function(err, line) { 308 | assert.ok(!err); 309 | assert.equal('’Twas brillig, and the slithy toves', line); 310 | assert.ok(reader.hasNextLine()); 311 | reader.nextLine(function(err, line) { 312 | assert.ok(!err); 313 | assert.equal('Did gyre and gimble in the wabe;', line); 314 | assert.ok(reader.hasNextLine()); 315 | reader.nextLine(function(err, line) { 316 | assert.ok(!err); 317 | assert.equal('', line); 318 | assert.ok(reader.hasNextLine()); 319 | reader.nextLine(function(err, line) { 320 | assert.ok(!err); 321 | assert.equal('', line); 322 | assert.ok(!reader.hasNextLine()); 323 | reader.nextLine(function(err, line) { 324 | assert.ok(err); 325 | done(); 326 | }); 327 | }); 328 | }); 329 | }); 330 | }); 331 | }); 332 | }); 333 | }); 334 | }); 335 | 336 | it("should work with a file containing only one line", function(done) { 337 | lineReader.open(oneLineFilePath, function(err, reader) { 338 | assert.ok(!err); 339 | reader.close(function(err) { 340 | assert.ok(!err); 341 | done(); 342 | }); 343 | }); 344 | }); 345 | 346 | it("should read multibyte characters on the buffer boundary", function(done) { 347 | lineReader.open(multibyteFilePath, {separator: '\n', encoding: 'utf8', bufferSize: 2}, function(err, reader) { 348 | assert.ok(!err); 349 | assert.ok(reader.hasNextLine()); 350 | reader.nextLine(function(err, line) { 351 | assert.ok(!err); 352 | assert.equal('ふうりうの初やおくの田植うた', line, 353 | "Should read multibyte characters on buffer boundary"); 354 | reader.close(function(err) { 355 | assert.ok(!err); 356 | done(); 357 | }); 358 | }); 359 | }); 360 | }); 361 | 362 | it("should support opened streams", function() { 363 | var readStream = fs.createReadStream(testFilePath); 364 | 365 | lineReader.open(readStream, function(err, reader) { 366 | assert.ok(!err); 367 | assert.ok(reader.hasNextLine()); 368 | 369 | assert.ok(reader.hasNextLine(), 'Calling hasNextLine multiple times should be ok'); 370 | 371 | reader.nextLine(function(err, line) { 372 | assert.ok(!err); 373 | assert.equal('Jabberwocky', line); 374 | }); 375 | }); 376 | }); 377 | 378 | it("should handle error while opening read stream", function() { 379 | lineReader.open('a file that does not exist', function(err, reader) { 380 | assert.ok(err); 381 | assert.ok(reader.isClosed()); 382 | }); 383 | }); 384 | 385 | describe("hasNextLine", function() { 386 | it("should return true when buffer is empty but not at EOF", function(done) { 387 | lineReader.open(threeLineFilePath, {separator: '\n', encoding: 'utf8', bufferSize: 36}, function(err, reader) { 388 | assert.ok(!err); 389 | reader.nextLine(function(err, line) { 390 | assert.ok(!err); 391 | assert.equal("This is line one.", line); 392 | assert.ok(reader.hasNextLine()); 393 | reader.nextLine(function(err, line) { 394 | assert.ok(!err); 395 | assert.equal("This is line two.", line); 396 | assert.ok(reader.hasNextLine()); 397 | reader.nextLine(function(err, line) { 398 | assert.ok(!err); 399 | assert.equal("This is line three.", line); 400 | assert.ok(!reader.hasNextLine()); 401 | reader.close(function(err) { 402 | assert.ok(!err); 403 | done(); 404 | }) 405 | }); 406 | }); 407 | }); 408 | }); 409 | }); 410 | }); 411 | }); 412 | }); 413 | --------------------------------------------------------------------------------