281

I'm looking to process a text file with node using a command line call like:

node app.js < input.txt

Each line of the file needs to be processed individually, but once processed the input line can be forgotten.

Using the on-data listener of the stdin, I get the input steam chunked by a byte size so I set this up.

process.stdin.resume();
process.stdin.setEncoding('utf8');

var lingeringLine = "";

process.stdin.on('data', function(chunk) {
    lines = chunk.split("\n");

    lines[0] = lingeringLine + lines[0];
    lingeringLine = lines.pop();

    lines.forEach(processLine);
});

process.stdin.on('end', function() {
    processLine(lingeringLine);
});

But this seems so sloppy. Having to massage around the first and last items of the lines array. Is there not a more elegant way to do this?

13 Answers 13

321

You can use the readline module to read from stdin line by line:

const readline = require('readline');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  terminal: false
});

rl.on('line', (line) => {
    console.log(line);
});

rl.once('close', () => {
     // end of input
 });
Sign up to request clarification or add additional context in comments.

10 Comments

That seems to work well for entering input by hand in the console, however, when I pass a file into the command the file is sent to stdout. A bug? readline is considered unstable at this point.
I think you can just change process.stdout to a different writable stream — it could be as simple as output: new require('stream').Writable()
Unfortunately, I need the stdout. I left it out of my question, but I'm trying to get the app to be usable as node app.js < input.txt > output.txt.
Apparently this is 'by design' github.com/joyent/node/issues/4243#issuecomment-10133900. So I ended up doing as you said and provided the output option a dummy writable stream, then wrote directly to the stdout stream. I don't like it, but it works.
Looks like if you pass the argument terminal: false to createInterface, it fixes this problem.
|
147
// Work on POSIX and Windows
var fs = require("fs");
var stdinBuffer = fs.readFileSync(0); // STDIN_FILENO = 0
console.log(stdinBuffer.toString());

15 Comments

Could you include some details? There is already a highly rated accepted answer
This doesn't work for me (node v9.2.0, Windows). Error: EISDIR: illegal operation on a directory, fstat at tryStatSync (fs.js:534:13)`
@AlexChaffee: There appears to be a bug on Windows (still present as of v9.10.1) if there's no stdin input or if stdin is closed - see this GitHub issue. Apart from this, however, the solution does work on Windows.
works very well and is the shortest by far, could make it shorter by doing fs.readFileSync(0).toString()
Note that the "magic number" 0 can be replaced with the clearer process.stdin.fd (which is just hard-coded to 0 but makes it more obvious what you're doing)
|
70

readline is specifically designed to work with terminal (that is process.stdin.isTTY === true). There are a lot of modules which provide split functionality for generic streams, like split. It makes things super-easy:

process.stdin.pipe(require('split')()).on('data', processLine)

function processLine (line) {
  console.log(line + '!')
}

2 Comments

no it's not. If you don't want to read line-by-line you don't need it at all
Tip: if you want to run some code after processing all the lines, add .on('end', doMoreStuff) after the first .on(). Remember that if you just write the code normally after the statement with .on(), that code will run before any input is read, because JavaScript isn’t synchronous.
24
#!/usr/bin/env node

const EventEmitter = require('events');

function stdinLineByLine() {
  const stdin = new EventEmitter();
  let buff = '';

  process.stdin
    .on('data', data => {
      buff += data;
      lines = buff.split(/\r\n|\n/);
      buff = lines.pop();
      lines.forEach(line => stdin.emit('line', line));
    })
    .on('end', () => {
      if (buff.length > 0) stdin.emit('line', buff);
    });

  return stdin;
}

const stdin = stdinLineByLine();
stdin.on('line', console.log);

Comments

21

Node.js has changed a lot since the accepted answer was posted, so here is a modern example using readline to split the stream into lines, for await to read from the stream, and ES modules:

import { createInterface } from "node:readline"

for await (const line of createInterface({ input: process.stdin })) {
  // Do something with `line` here.
  console.log(line)
}

2 Comments

(1/2) Beware of surprising behavior: if there's any delay between creation of the interface and for await () loop, any lines that were read during the delay won't be iterated
(2/2) Here's a program to demonstrate. Pass small file to stdin (so it can be read entirely in 10s) and see that it won't be printed: ``` import { createInterface } from 'readline/promises' void main() async function main() { const rl = createInterface(process.stdin) await delay(10_000) for await (const line of rl) { console.log(line) } } function delay(ms) { return new Promise((resolve) => { setTimeout(resolve, ms) }) } ```
5

Just translating what https://stackoverflow.com/a/76743097/895245 said to plain JavaScript, require and adding a async function callback to allow us to test it:

readlines.js

const readline = require('readline');

(async function () {
for await (const line of readline.createInterface({ input: process.stdin })) {
  console.log(line)
}
})()

We can then test this with:

(echo asdf; sleep 1; echo qwer; sleep 1; echo zxcv) | node  readlines.js

and it outputs:

asdf
qwer
zxcv

where each line is printed immediately after it is read from stdin, spaced 1 second apart. This confirms to us that lines are being read one by one and immediately after they are made available.

Tested on Node.js v16.14.2, Ubuntu 23.04.

Comments

3

New answer to old question.

Since Node 10 (April 2018) ReadableStreams such as process.stdin support for-await-of loops thanks to the addition of a Symbol.asyncIterator method (ReadableStream documentation, Symbol.asyncIterator documentation).

Using this we can create an adaptor that goes from iterating through chunks of data, to iterating through lines. The logic for doing this was adapted from this answer.

function streamByLines(stream) {
  stream.setEncoding('utf8');
  return {
    async *[Symbol.asyncIterator]() {
      let buffer = '';

      for await (const chunk of stream) {
        buffer += chunk;
        const lines = buffer.split(/\r?\n/);
        buffer = lines.pop();
        for (const line of lines) {
          yield line;
        }
      }
      if (buffer.length > 0) yield buffer;
    },
  };
}

You can use it like this (in a context where await is allowed)

for await (const line of streamByLines(process.stdin)) {
  console.log('Current line:', line)
}

Comments

0

This is a slightly more concise version:

const partialLine = '';
const lines = [];
const emitter = new EventEmitter()

process.stdin.on('data', (data) => {
  const buffer = this._partialLine + data
  const newLines = buffer.split('\n')
  partialLine = newLines.pop() || ''
  lines.push(...newLines)
  if (lines.length) this._emitter.emit('data')
})

Comments

-1

In my case the program (elinks) returned lines that looked empty, but in fact had special terminal characters, color control codes and backspace, so grep options presented in other answers did not work for me. So I wrote this small script in Node.js. I called the file tight, but that's just a random name.

#!/usr/bin/env node

function visible(a) {
    var R  =  ''
    for (var i = 0; i < a.length; i++) {
        if (a[i] == '\b') {  R -= 1; continue; }  
        if (a[i] == '\u001b') {
            while (a[i] != 'm' && i < a.length) i++
            if (a[i] == undefined) break
        }
        else R += a[i]
    }
    return  R
}

function empty(a) {
    a = visible(a)
    for (var i = 0; i < a.length; i++) {
        if (a[i] != ' ') return false
    }
    return  true
}

var readline = require('readline')
var rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false })

rl.on('line', function(line) {
    if (!empty(line)) console.log(line) 
})

Comments

-1

I have revisited the code years later, here is the updated code

It should be good for large files piped into stdin, Supporting utf8 and pause && resume It can read a stream line by line and process the input without choking.

StreamLinesReader Class Description:

The StreamLinesReader class is designed to process streams of text data, specifically handling each line of input efficiently and correctly. Key features of this class include:

  • The class sets the stream's encoding to UTF-8. To ensure that multibyte utf-8 characters are not split in the middle.

  • Stream uses Pause-Resume Mechanism: The class pauses the input stream upon receiving data. This mechanism is essential for controlling the flow of data, especially in scenarios where the line processing function is slower than the rate at which data is received. By pausing the stream, it prevents buffer overflow and ensures that each line is processed sequentially without losing any data.

  • Accumulation of Incomplete Lines: In cases where the data chunks received do not always end with a newline character, the class accumulates these partial lines. It holds them in a buffer until a complete line (ending with a newline) is received. This approach ensures that lines are processed only when they are complete, preserving the integrity of the data.

  • Handling of Split Lines: When a chunk of data is received, the class splits it into lines and processes each line individually. If the last part of the chunk does not end with a newline, this part is buffered and not returned right away. After processing all complete lines, the class pushes the last (potentially incomplete) line into the buffer. After all lines were processed the stream is then resumed to receive more data, allowing the buffered line to be completed in the next chunk.

  • Stream End Processing: When the end of the stream is reached, the class checks if there is any remaining data in the buffer (an incomplete line) and then it processes it. Additionally, the class provides a mechanism to notify when the stream processing is complete, through the wait function which returns a promise that resolves upon the completion of the stream processing.

class code:

class StreamLinesReader {
    constructor(stream, onLineFunction, onEnd = undefined) {
        stream.pause();

        this.stream = stream;
        this.onLine = onLineFunction;
        this.onEnd = onEnd;
        this.buffer = [];
        this.line = 0;

        this.stream.setEncoding('utf8');

        this.stream.on('data', (chunk) => {
            stream.pause();
            this.processChunk(chunk).then(() => this.stream.resume());
        });

        this.stream.on('end', async () => {
            if (this.buffer.length) {
                const str = this.buffer.join('');
                await this.onLine(str, this.line++);
            }
            if (this.onEnd) await this.onEnd();
            if (this.resolveWait) this.resolveWait();
        });

        this.stream.resume();
    }

    async processChunk(chunk) {
        const newlines = /\r\n|\n/;
        const lines = chunk.split(newlines);

        if (lines.length === 1) {
            this.buffer.push(lines[0]);
            return;
        }

        // Join buffer and first line
        this.buffer.push(lines[0]);
        const str = this.buffer.join('');
        this.buffer.length = 0;
        await this.onLine(str, this.line++);

        // Process lines in the chunk
        for (let i = 1; i < lines.length - 1; i++) {
            await this.onLine(lines[i], this.line++);
        }

        // Buffer the last line (might be the beginning of the next line)
        this.buffer.push(lines[lines.length - 1]);
    }

    // optional:
    waitEnd() {
        // Return a new promise and save the resolve function
        return new Promise((resolve) => {
            this.resolveWait = resolve;
        });
    }
}

example usage:

session.on('pty', (accept, reject, info) => {
    accept();
    session.on('shell', (accept, reject) => {
        const stream = accept();

        const onLineFunction = async (line, lineNumber) => {
            console.log(lineNumber, "line ", line);
            if (line === 'exit') {
                stream.end();
                // Assuming conn is a connection variable defined elsewhere
                conn.end();
            }
        };

        const onEndFunction = async () => {
            console.log("Stream has ended");
        };

        new StreamLinesReader(stream, onLineFunction, onEndFunction);
        
        const OUTPUT = 'shell output!\n';
        stream.write(OUTPUT);
    });
});

my old synchronous code was:

read stream line by line,should be good for large files piped into stdin, my version:

var n=0;
function on_line(line,cb)
{
    ////one each line
    console.log(n++,"line ",line);
    return cb();
    ////end of one each line
}

var fs = require('fs');
var readStream = fs.createReadStream('all_titles.txt');
//var readStream = process.stdin;
readStream.pause();
readStream.setEncoding('utf8');

var buffer=[];
readStream.on('data', (chunk) => {
    const newlines=/[\r\n]+/;
    var lines=chunk.split(newlines)
    if(lines.length==1)
    {
        buffer.push(lines[0]);
        return;
    }   
    
    buffer.push(lines[0]);
    var str=buffer.join('');
    buffer.length=0;
    readStream.pause();

    on_line(str,()=>{
        var i=1,l=lines.length-1;
        i--;
        function while_next()
        {
            i++;
            if(i<l)
            {
                return on_line(lines[i],while_next);
            }
            else
            {
                buffer.push(lines.pop());
                lines.length=0;
                return readStream.resume();
            }
        }
        while_next();
    });
  }).on('end', ()=>{
      if(buffer.length)
          var str=buffer.join('');
          buffer.length=0;
        on_line(str,()=>{
            ////after end
            console.error('done')
            ////end after end
        });
  });
readStream.resume();

2 Comments

what is happening in this answer?
@activedecay Added an explanation and an es6 code
-1

If you wish to await the input entry you can do:

function getUserInputOnEnter() {
    return new Promise(resolve => {
        rl.on('line', line => resolve(line))
    });
}

and then use it as :

let cnt = await getUserInputOnEnter();

1 Comment

This will only resolve the first line.
-2

if you want to ask the user number of lines first:

    //array to save line by line 
    let xInputs = [];

    const getInput = async (resolve)=>{
            const readline = require('readline').createInterface({
                input: process.stdin,
                output: process.stdout,
            });
            readline.on('line',(line)=>{
            readline.close();
            xInputs.push(line);
            resolve(line);
            })
    }

    const getMultiInput = (numberOfInputLines,callback)=>{
        let i = 0;
        let p = Promise.resolve(); 
        for (; i < numberOfInputLines; i++) {
            p = p.then(_ => new Promise(resolve => getInput(resolve)));
        }
        p.then(()=>{
            callback();
        });
    }

    //get number of lines 
    const readline = require('readline').createInterface({
        input: process.stdin,
        output: process.stdout,
        terminal: false
    });
    readline.on('line',(line)=>{
        getMultiInput(line,()=>{
           //get here the inputs from xinputs array 
        });
        readline.close();
    })

Comments

-9
process.stdin.pipe(process.stdout);

2 Comments

Please add some explanation too
Hi Ayush. Thanks for your answer. Usually answers with an explanation are more welcomed there. Would you like to add an explanation to your answer? You may improve formatting of your answer as well.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.