58

I am wondering if there is a simple way to get "synchronous" readline or at least get the appearance of synchronous I/O in node.js

I use something like this but it is quite awkward

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

var i = 0;
var s1 = '';
var s2 = '';

rl.on('line', function(line){
    if(i==0) { s1 = line; }
    else if(i==1) { s2 = line; }
    i++;
})

rl.on('close', function() {
    //do something with lines
})'

Instead of this I would prefer if it were as easy as something like

var s1 = getline(); // or "await getline()?"
var s2 = getline(); // or "await getline()?"

Helpful conditions:

(a) Prefer not using external modules or /dev/stdio filehandle, I am submitting code to a code submission website and these do not work there

(b) Can use async/await or generators

(c) Should be line based

(d) Should not require reading entire stdin into memory before processing

Colin D
  • 2,822
  • 1
  • 31
  • 38
  • 1
    Possible duplicate of [node.js: readSync from stdin?](http://stackoverflow.com/questions/3430939/node-js-readsync-from-stdin) – supersam654 Apr 26 '17 at 16:32
  • @supersam654 I added some extra conditions to de-duplicate. My condition is to allow reading (single) lines using the getline type function using the readline interface in a clean "synchronous" way or to simulate this using "async/await" functions. – Colin D Apr 26 '17 at 17:28

12 Answers12

93

Just in case someone stumbles upon here in future

Node 11.7 added support for this using async await

const readline = require('readline');
//const fileStream = fs.createReadStream('input.txt');

const rl = readline.createInterface({
  input: process.stdin, //or fileStream 
  output: process.stdout
});

for await (const line of rl) {
  console.log(line)
}

Remember to wrap it in async function(){} otherwise you will get a reserved_keyword_error.

const start = async () =>{
    for await (const line of rl) {
        console.log(line)
    }
}
start()

To read an individual line, you can use the async iterator manually

const it = rl[Symbol.asyncIterator]();
const line1 = await it.next();
Michael Mior
  • 28,107
  • 9
  • 89
  • 113
Aishwat Singh
  • 4,331
  • 2
  • 26
  • 48
  • 4
    Can I call get a single line using this method? For example, i was thinking before the loop I want to read one or two lines, so I wanted to say like const header = await rl.next() but this did not work even in node 11.7 – Colin D Jan 19 '19 at 22:13
  • https://stackoverflow.com/questions/45556535/nodejs-readline-only-read-the-first-2-lines @ColinD – Aishwat Singh Jan 21 '19 at 05:34
  • 2
    I find the syntactic complexity of those answers to be unnecessarily complex. I know it is perfectly doable but I'm looking for something simple like `var h1 = await rl.next(); var h2 = await rl.next(); for await (const line of rl) { /* process rest of file */ }` but as I mentioned above the rl iterator does not provide for that it seems – Colin D Jan 21 '19 at 18:25
  • oh! i get it, vl check the implementation of readline and `for await ` – Aishwat Singh Jan 22 '19 at 05:00
  • 1
    @AishwatSingh When I use your code, I get a syntax error on the line with the await. Unexpected token, expected ( Can you verify that it is working still? – Nakul Tiruviluamala Jan 26 '20 at 07:06
  • @NakulTiruviluamala I just tried it out, it works. Can you add a snippet of breaking line? – Aishwat Singh Jan 27 '20 at 05:53
  • @AishwatSingh ` const line1 = await rl.next(); ` produces error `TypeError: rl.next is not a function` node 14.4 – Colin D Jun 27 '20 at 23:02
  • @ColinD just tried it out in `v14.4.0` still works, did you wrap it in async? – Aishwat Singh Jun 30 '20 at 02:48
  • ``` const readline = require("readline"); //const fileStream = fs.createReadStream('input.txt'); const rl = readline.createInterface({ input: process.stdin, //or fileStream output: process.stdout, }); (async () => { const line1 = await rl.next(); const line2 = await rl.next(); console.log("hi", line1, line2); })(); ``` produces (node:1798580) UnhandledPromiseRejectionWarning: TypeError: rl.next is not a function – Colin D Jun 30 '20 at 13:22
  • @ColinD think you need this ``` const readline = require("readline"); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const start = async () => { const it = rl[Symbol.asyncIterator]() const line1 = await it.next() const line2 = await it.next() console.log("hi", line1, line2); }; start() ``` Also this may help https://nodejs.org/api/readline.html#readline_event_line – Aishwat Singh Jun 30 '20 at 14:55
  • Thanks that is great. Would you be interested in adding this as an addendum to your answer? I could accept your answer then – Colin D Jun 30 '20 at 16:14
43

You can just wrap it in a promise -

const answer = await new Promise(resolve => {
  rl.question("What is your name? ", resolve)
})
console.log(answer)
Epic Speedy
  • 636
  • 1
  • 11
  • 25
Brian Burns
  • 20,575
  • 8
  • 83
  • 77
30

Like readline module, there is another module called readline-sync, which takes synchronous input.

Example:

const reader = require("readline-sync"); //npm install readline-sync
let username = reader.question("Username: ");
const password = reader.question("Password: ",{ hideEchoBack: true });
if (username == "admin" && password == "foobar") {
    console.log("Welcome!")
}
Community
  • 1
  • 1
Jaidee
  • 929
  • 12
  • 5
  • 1
    This should be the right answer for this question (it's the best practice) – johannchopin Nov 17 '19 at 23:25
  • @johannchopin no this is not the approach I want. I am using pipes via stdin or large datasets, not question and answer which is what that module is what readline-sync is aparently designed to do – Colin D Nov 18 '19 at 02:20
  • There is another one called `readline-async` in case anybody needs promises. – Danilo Gómez Apr 18 '20 at 14:04
  • 5
    @johannchopin it can't be the best practice. readline-sync is not standard. It's a dependency.. – fredericrous Jul 12 '21 at 09:51
  • 1
    Also, `readline-sync` can be expensive since it spawns a child process internally. There is a very simple way to achieve the same effect without child process, which I outlined in my answer. – martian17 Apr 03 '23 at 03:04
17

I think this is what you want :

const readline = require('readline');

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

const getLine = (function () {
    const getLineGen = (async function* () {
        for await (const line of rl) {
            yield line;
        }
    })();
    return async () => ((await getLineGen.next()).value);
})();

const main = async () => {
    let a = Number(await getLine());
    let b = Number(await getLine());
    console.log(a+b);
    process.exit(0);
};

main();

Note: this answer use experimental features and need Node v11.7

hamid k
  • 481
  • 4
  • 11
  • This looks good, how would you setup a loop to read until the end of the input after adding those two numbers for example? – Colin D Mar 14 '19 at 16:23
  • getLine will return undefined if it reaches EOF, so while (1){ let x = await getLine(); if (x === undefined) break; /* then work with x */ } should work. – hamid k Mar 14 '19 at 18:06
14

Try this. It's still not a perfect replication of a synchronous line reading function -- e.g. async functions still happen later, so some of your calling code may execute out of order, and you can't call it from inside a normal for loop -- but it's a lot easier to read than the typical .on or .question code.

// standard 'readline' boilerplate
const readline = require('readline');
const readlineInterface = readline.createInterface({
        input: process.stdin,
        output: process.stdout
});

// new function that promises to ask a question and 
// resolve to its answer
function ask(questionText) {
  return new Promise((resolve, reject) => {
    readlineInterface.question(questionText, (input) => resolve(input) );
  });
}

// launch your program since `await` only works inside `async` functions
start()

// use promise-based `ask` function to ask several questions
// in a row and assign each answer to a variable
async function start() {
  console.log()
  let name = await ask("what is your name? ")
  let quest = await ask("what is your quest? ")
  let color = await ask("what is your favorite color? ")
  console.log("Hello " + name + "! " + 
    "Good luck with " + quest + 
    "and here is a " + color + " flower for you.");
  process.exit() 
}

UPDATE: https://www.npmjs.com/package/readline-promise implements it (source code here: https://github.com/bhoriuchi/readline-promise/blob/master/src/index.js#L192 ). It implements several other features as well, but they seem useful too, and not too overengineered, unlike some other NPM packages that purport to do the same thing. Unfortunately, I can't get it to work due to https://github.com/bhoriuchi/readline-promise/issues/5 but I like its implementation of the central function:

function ask(questionText) {
  return new Promise((resolve, reject) => {
    readlineInterface.question(questionText, resolve);
  });
}
AlexChaffee
  • 8,092
  • 2
  • 49
  • 55
  • I like this solution. Using promises leverage the power of async/await context without the need for another library. I would rather pass the readline interface as a parameter to the ask function so that you can use different interfaces. – Amin NAIRI Apr 04 '20 at 19:13
5

Using generators your example would look like this:

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

var i = 0;
var s1 = '';
var s2 = '';

var iter=(function* () {
    s1 = yield;
    i++;
    s2 = yield;
    i++;
    while (true) {
        yield;
        i++;
    }
})(); iter.next();
rl.on('line', line=>iter.next(line))

rl.on('close', function() {
    //do something with lines
})

So yield here acts as if it were a blocking getline() and you can handle lines in the usual sequential fashion.


UPD:
And an async/await version might look like the following:

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

var i = 0;
var s1 = '';
var s2 = '';

var continuation;
var getline = (() => {
    var thenable = {
        then: resolve => {
            continuation = resolve;
        }
    };
    return ()=>thenable;
})();
(async function() {
    s1 = await getline();
    i++;
    s2 = await getline();
    i++;
    while (true) {
        await getline();
        i++;
    }
})();
rl.on('line', line=>continuation(line))

rl.on('close', function() {
    //do something with lines
})

In both of these "synchronous" versions, i is not used for distinguishing lines and only useful for counting the total number of them.

Des Nerger
  • 167
  • 2
  • 5
2

Here's an example but it requires reading entire stdin before giving results however which is not ideal

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


function lineiterator() {
    var currLine = 0;
    var lines = [];
    return new Promise(function(resolve, reject) {

        rl.on('line', function (line){
            lines.push(line)
        })
        rl.on('close', function () {
            resolve({
                next: function() {
                    return currLine < lines.length ? lines[currLine++]: null;
                }
            });
        })
    })
}

Example

lineiterator().then(function(x) {
    console.log(x.next())
    console.log(x.next())
})

$ echo test$\ntest | node test.js
test
test
Colin D
  • 2,822
  • 1
  • 31
  • 38
2

The simplest (and preferred) option is available in the docs. https://nodejs.org/api/readline.html#rlquestionquery-options-callback

const util = require('util');
const question = util.promisify(rl.question).bind(rl);

async function questionExample() {
  try {
    const answer = await question('What is you favorite food? ');
    console.log(`Oh, so your favorite food is ${answer}`);
  } catch (err) {
    console.error('Question rejected', err);
  }
}
questionExample();
Bogdans
  • 145
  • 7
1

Node v17.0.0 added a Promises API for readline, readline/promises

const readlinePromises = require('node:readline/promises');

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

const answer = await rl.question('What is your favorite food? ');
console.log(`Oh, so your favorite food is ${answer}`);
Dylan I
  • 138
  • 1
  • 8
0

Since I don't know how many strings you need I put them all in an Array

Don't hesitate to comment if you need a more detailed answer or if my answer is not exact :

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

var i = 0;
var strings = [];

rl.on('line', function(line) {
    // 2 lines below are in case you want to stop the interface after 10 lines
    // if (i == 9)
    //  rl.close()
    strings[i] = line
    i++
}).on('close', function() {
    console.log(strings)
})
// this is in case you want to stop the program when you type ctrl + C
process.on('SIGINT', function() {
    rl.close()
})
Amine
  • 349
  • 2
  • 16
  • This requires reading entire stdin into memory, I would prefer this is not the case because I read large files via stdin and needs streaming – Colin D Apr 26 '17 at 17:26
  • it is a stream, so the only reason it goes into memory is because I put it in the variable strings, but if you do something else with it it won't go into memory – Amine Apr 27 '17 at 07:45
  • I understand. But I want something like in perl `my $line = <>`. Or getline in c++. Is that so hard? I feel like my request is not so outlandish and I visualize it being completed with generators or async/await – Colin D Apr 27 '17 at 12:56
  • The real question is what do you want to do with all the lines? – Amine Apr 27 '17 at 13:03
  • Currently the lines of data are piped into the program via stdin, and the data generally has some header lines and then a lot of data. There are no signals for the header, you just infer it as needed by the problem, and the data and header contain a mix of strings and numbers. Therefore it is much easier to just have control over a getline function than to tweak code inside the on('line') callback. – Colin D Apr 27 '17 at 13:37
  • I understand what type of data will be streamed to the interface now. However, I don't see how it would me much easier than the getline function, the `on('line', callback)` is actually not a "tweak" code since this is nodejs native. Can you try to explain what is the behaviour that you want, or if you can't, can you explain what do you mean by "control" over a getline function? – Amine Apr 27 '17 at 14:40
  • When I use this callback technique, I have to add unnecessary "if(line==1) ... if(line==2) ...." statements inside on('line', function() { }) to selectively parse header lines, that seems very awkward. I would rather have "control" over when I grab a line from stdin using a simple "var line = getline()" or "var line = lines.next()" (generator style) or "var line = await getline()" (async/await style) – Colin D Apr 27 '17 at 15:47
0

We can use promise and process.stdin events together to simulate a synchronous input system

const { EOL } = require("os");
const getLine = async () =>
    (
        await new Promise((resolve) => {
            process.stdin.on("data", (line) => {
                resolve("" + line);
            });
        })
    ).split(EOL)[0];

const line = await getLine();
console.log(line);
Noman
  • 124
  • 2
  • 9
0

Turns out there is a way to achieve this synchronously without wrapping everything around async or spawning child processes like how it's done inside readline-sync npm module which is expensive.
The key is to utilize the synchronous file system API, which stdin is compatible with.

const fs = require("fs");

const getchar = function(){
    // Allocate a temp buffer. This can be declared outside this function to be reused.
    const buffer = Buffer.alloc(1);
    // Read a single character from stdin to the temp buffer
    fs.readSync(0/*File descriptor for stdin*/, buffer, 0, 1);
    return buffer[0];
};

const readline = function(){
    let c;
    const buff = [];
    // Read characters one by one until encountering newline
    while((c = getchar()) !== '\n'.charCodeAt(0)){
        buff.push(c);
    }
    // Convert the buffer to string
    return Buffer.from(buff).toString("utf8");
};

const prompt = function(pmt){
    process.stdout.write(pmt);
    return readline();
};

const age = prompt("your age: ");
console.log(`You are ${age} years old`);
const name = prompt("your name: ");
console.log(`Your name is ${name}`);
martian17
  • 418
  • 3
  • 14