Node.js: readline inside replace() - node.js

I have a string with the alphabet and certain letters are in parenthesis. It means these letters should be uppercase. But the user has to be asked first if he wants it.
The problem is, I have to call readline inside a loop and this doesn't work.
const readline = require('readline');
const text = 'a(b)cdefg(h)ijklmnopqrst(u)vwxyz';
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
text.replace(/\((\w)\)/g, function (match, p1) {
let answer;
rl.question(`Do you want to change ${p1}? [yes/no] `, input => {
answer = input;
});
return answer === "yes" ? p1.toUpperCase() : p1;
});
console.log("Result:", text);
It just prints out Do you want to change b? [yes/no] Result: a(b)cdefg(h)ijklmnopqrst(u)vwxyz.
How can this be solved?

As Nick has explained in the comments the main issue of your code it that it does not handle the async readline operation correctly - here's how I would do this:
use a promisifed version of readline to prevent nested callbacks
use a while loop to step through all regex matches and await the user input in order to convert the chars to upper case or not
Something like (still needs error handling, but should give you an idea):
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
function question(query) {
return new Promise(resolve => {
readline.question(query, resolve);
})
}
const specialCharsRegex = /\((\w)\)/g;
const text = 'a(b)cdefg(h)ijklmnopqrst(u)vwxyz';
(async () => {
let result = text;
let currMatch;
while ((currMatch = specialCharsRegex.exec(text)) !== null) {
const convertInput = await question(`Do you want to change ${currMatch[1]}? [yes/no] `);
result = result.replace(currMatch[0], v => {
return convertInput === "yes" ? currMatch[1].toUpperCase() : currMatch[1];
});
}
console.log("Result: ", result);
readline.close();
})();

The main issue with your code is that you're trying to do something asynchronous in a callback that is expected to be synchronous - that is, you're trying to read user input in the .replace() callback, but reading user input doesn't happen immediately, so the callback to .question() occurs sometime after your question has been asked and your replace callback has terminated/finished. Making rl.question() synchronous would fix this (Node.js synchronous prompt) and would allow you to prompt against the exact words you're replacing.
If you want to keep things asynchronous, you could first match the letters you want to replace using .matchAll(), that way you'll have the text/groups, and the indexes of the characters you want to change. Then, you can use a loop to iterate the matches, and prompt the user whether they want to change the group. For each group, you can update the text accordingly by updating the text at the specified index. You could use replace, but if your text has duplicates eg: "a(b)c(b)e", and you said "no" for the first (b) but yes for the second (b), then you'll either need to replace both occurrences, or the first match. By using the index, you can specify exactly which group to change:
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin, //or fileStream
output: process.stdout
});
const question = q => new Promise(res => rl.question(q, res));
let text = 'a(b)cdefg(h)ijklmnopqrst(u)vwxyz';
(async () => {
const matches = text.matchAll(/\((\w)\)/g);
for(const {0: match, 1: p1, index} of matches) {
const ans = await question(`Do you want to change ${p1}? [yes/no] `);
if(ans === "yes")
text = text.slice(0, index) + match.toUpperCase() + text.slice(index + match.length);
}
console.log(text);
})();

Related

Getting values from stdin with Node.js & TypeScript

I have some tasks, basically a CLI script that takes x lines, and based on said lines makes some calculations.
Eg.:
Input:
3 // number of iterations to do something
ATCCGCTTAGAGGGATT // first string
GTCCGTTTAGAAGGTTT // second string
// 2nd iteration starts
abcdefghijklmnopqrstuvwxyz
bcdefghijklmnopqrstuvwxyza
// 3rd iteration starts
abcdefghijklmnopqrstuvwxyz0123456789
abcdefghijklmnopqrstuvwxyz0123456789
Output:
ATCCGCTTAGAGGGATT // first string
GTCCGTTTAGAAGGTTT // second string
*....*.....*..*.. // visual marker of differences between the two strings(*)
abcdefghijklmnopqrstuvwxyz
bcdefghijklmnopqrstuvwxyza
**************************
abcdefghijklmnopqrstuvwxyz0123456789
abcdefghijklmnopqrstuvwxyz0123456789
....................................
What i've tried:
const rl = require('readline').createInterface({
input: process.stdin,
output: process.stdout,
prompt: ''
})
let numberOfTests = 0;
let firstText = '';
let secondText = '';
let differences = '';
rl.prompt();
rl.on('line', (line: any) => {
let loop = 0;
numberOfTests = line;
firstText = line;
secondText = line;
calculateDifferences(firstText, secondText); // basic function that loops through the arrays and makes the visual marker.
loop ++;
if (loop === numberOfTests) {
rl.close();
}
}).on('close', () => {
console.log('Have a great day!');
process.exit(0);
});
The main issue I am facing is that i cannot separate the input stream from one another.
By the time we are at the secondText every variable will be equal to wheter I input on the 3rd time.
How can you separate the inputs from each other?
Node documentation is not so detailed about this.Or I cannot see it through correctly.
Managed to find a solution to differenciate the rl instances.
Following this comment is 100% working in my use-case

Asynchronous file read reading different number of lines each time, not halting

I built a simple asynchronous implementation of the readlines module built into nodejs, which is simply a wrapper around the event-based module itself. The code is below;
const readline = require('readline');
module.exports = {
createInterface: args => {
let self = {
interface: readline.createInterface(args),
readLine: () => new Promise((succ, fail) => {
if (self.interface === null) {
succ(null);
} else {
self.interface.once('line', succ);
}
}),
hasLine: () => self.interface !== null
};
self.interface.on('close', () => {
self.interface = null;
});
return self;
}
}
Ideally, I would use it like so, in code like this;
const readline = require("./async-readline");
let filename = "bar.txt";
let linereader = readline.createInterface({
input: fs.createReadStream(filename)
});
let lines = 0;
while (linereader.hasLine()) {
let line = await linereader.readLine();
lines++;
console.log(lines);
}
console.log("Finished");
However, i've observed some erratic and unexpected behavior with this async wrapper. For one, it fails to recognize when the file ends, and simply hangs once it reaches the last line, never printing "Finished". And on top of that, when the input file is large, say a couple thousand lines, it's always off by a few lines and doesn't successfully read the full file before halting. in a 2000+ line file it could be off by as many as 20-40 lines. If I throw a print statement into the .on('close' listener, I see that it does trigger; however, the program still doesn't recognize that it should no longer have lines to read.
It seems that in nodejs v11.7, the readline interface was given async iterator functionality and can simply be looped through with a for await ... of loop;
const rl = readline.createInterface({
input: fs.createReadStream(filename);
});
for await (const line of rl) {
console.log(line)
}
How to get synchronous readline, or "simulate" it using async, in nodejs?

Nodejs read file line by line and accumulate results in global object

Classic embarrassing newbie question. Why doesn't my store variable contain any results? I know it is accumulating results along the way. I also know enough about nodejs to know it has to do with promises, single-threadedness, etc.
var readline = require('readline');
var fs = require("fs");
var pathToFile = '/scratch/myData.csv';
var rd = readline.createInterface({
input: fs.createReadStream(pathToFile),
// output: process.stdout,
console: false
});
var store = {};
rd.on('line', function(line) {
store[line] = 1;
// console.log (`store is now: ${JSON.stringify (store)}`);
});
console.log (`store is now: ${JSON.stringify (store)}`);
This has nothing to do with Promises, (Although, you can promisify it, if you like).
As you said, it is accumulating the results line by line, but this is hapening inside the scope of the callback function.
And if you want to make use of the data, you will have to call another function inside the callback function when the last line is called, (or maybe listen to a different event).
Something like the following:
var store = {};
rd.on('line', function(line) {
store[line] = 1;
// console.log (`store is now: ${JSON.stringify (store)}`);
})
.on('close', function() {
myFunc(store);
});
function myFunc(store){
console.log (`store is now: ${JSON.stringify (store)}`);
}

Is there a way to get synchronous terminal input in Node.js

I have looked at similar questions on SO and I have an ongoing question about if it is possible to get synchronous input from the command line.
I know about readline and process.stdin.on('readable', ...) but both of those seem to be asynchronous.
I am looking to for a way to prompt the user for input where code later on in my script does not run before there is user input.
May be you can try this in case if you know fix number of inputs you want in sync manner.
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
let i = 0;
rl.question('Number of inputs : ', (answer1) => {
rl.on('line', (answer2) => {
console.log(`input: ${answer2}`);
i++;
if (i >= answer1) {
rl.close();
}
});
});

Node.js synchronous prompt

I'm using the prompt library for Node.js and I have this code:
var fs = require('fs'),
prompt = require('prompt'),
toCreate = toCreate.toLowerCase(),
stats = fs.lstatSync('./' + toCreate);
if(stats.isDirectory()){
prompt.start();
var property = {
name: 'yesno',
message: 'Directory esistente vuoi continuare lo stesso? (y/n)',
validator: /y[es]*|n[o]?/,
warning: 'Must respond yes or no',
default: 'no'
};
prompt.get(property, function(err, result) {
if(result === 'no'){
console.log('Annullato!');
process.exit(0);
}
});
}
console.log("creating ", toCreate);
console.log('\nAll done, exiting'.green.inverse);
If the prompt is show it seems that it doesn't block code execution but the execution continues and the last two messages by the console are shown while I still have to answer the question.
Is there a way to make it blocking?
With flatiron's prompt library, unfortunately, there is no way to have the code blocking. However, I might suggest my own sync-prompt library. Like the name implies, it allows you to synchronously prompt users for input.
With it, you'd simply issue a function call, and get back the user's command line input:
var prompt = require('sync-prompt').prompt;
var name = prompt('What is your name? ');
// User enters "Mike".
console.log('Hello, ' + name + '!');
// -> Hello, Mike!
var hidden = true;
var password = prompt('Password: ', hidden);
// User enters a password, but nothing will be written to the screen.
So give it a try, if you'd like.
Bear in mind: DO NOT use this on web applications. It should only be used on command line applications.
Update: DO NOT USE THIS LIBRARY AT ALL. IT IS A TOTAL JOKE, TO BE PERFECTLY FRANK.
Since Node.js 8, you can do the following using async/await:
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function readLineAsync(message) {
return new Promise((resolve, reject) => {
rl.question(message, (answer) => {
resolve(answer);
});
});
}
// Leverages Node.js' awesome async/await functionality
async function demoSynchronousPrompt() {
var promptInput = await readLineAsync("Give me some input >");
console.log("Won't be executed until promptInput is received", promptInput);
rl.close();
}
Since IO in Node doesn't block, you're not going to find an easy way to make something like this synchronous. Instead, you should move the code into the callback:
...
prompt.get(property, function (err, result) {
if(result === 'no'){
console.log('Annullato!');
process.exit(0);
}
console.log("creating ", toCreate);
console.log('\nAll done, exiting'.green.inverse);
});
or else extract it and call the extracted function:
...
prompt.get(property, function (err, result) {
if(result === 'no'){
console.log('Annullato!');
process.exit(0);
} else {
doCreate();
}
});
...
function doCreate() {
console.log("creating ", toCreate);
console.log('\nAll done, exiting'.green.inverse);
}
Old question, I know, but I just found the perfect tool for this. readline-sync gives you a synchronous way to collect user input in a node script.
It's dead simple to use and it doesn't require any dependencies (I couldn't use sync-prompt because of gyp issues).
From the github readme:
var readlineSync = require('readline-sync');
// Wait for user's response.
var userName = readlineSync.question('May I have your name? ');
console.log('Hi ' + userName + '!');
I'm not affiliated with the project in any way, but it just made my day, so I had to share.
I've come across this thread and all the solutions either:
Don't actually provide a syncronous prompt solution
Are outdated and don't work with new versions of node.
And for that reason I have created syncprompt
. Install it with npm i --save syncprompt and then just add:
var prompt = require('syncprompt');
For example, this will allow you to do:
var name = prompt("Please enter your name? ");
It also supports prompting for passwords:
var topSecretPassword = prompt("Please enter password: ", true);
Vorpal.js is a library I made that has just recently been released. It provides synchronous command execution with an interactive prompt, like you are asking. The below code will do what you are asking:
var vorpal = require('vorpal')();
vorpal.command('do sync')
.action(function (args) {
return 'i have done sync';
});
With the above, the prompt will come back after a second is up (only after callback is called).
This is dependency free, synchronous and works on Windows, Linux and OSX:
// Synchronously prompt for input
function prompt(message)
{
// Write message
process.stdout.write(message);
// Work out shell command to prompt for a string and echo it to stdout
let cmd;
let args;
if (os.platform() == "win32")
{
cmd = 'cmd';
args = [ '/V:ON', '/C', 'set /p response= && echo !response!' ];
}
else
{
cmd = 'bash';
args = [ '-c', 'read response; echo "$response"' ];
}
// Pipe stdout back to self so we can read the echoed value
let opts = {
stdio: [ 'inherit', 'pipe', 'inherit' ],
shell: false,
};
// Run it
return child_process.spawnSync(cmd, args, opts).stdout.toString().trim();
}
const buffer = Buffer.alloc(1024);
require("fs").readSync(process.stdin.fd, buffer);
console.log(buffer.toString());
You can use prompt-sync
const prompt = require('prompt-sync')()
const ans = prompt('How many more times? ') // get input from the user.
P.S. prompt-sync acts weird, if prompt message contains new line character, so if you need multiline prompt just use console.log():
const prompt = require('prompt-sync')()
console.log('How many more times?\n')
const ans = prompt('') // get input from the user.

Resources