78

I understand the rationale in Node.js for asynchronous events and I am learning how to write code that way. However, I am stuck with the following situation:

I want to write code that occasionally pauses for user input.

The program is not intended as a server (though currently it's intended for the command line). I realize this is an atypical use of Node. My goal is to eventually migrate the program back to a client-side Javascript application, but I find working in Node.js to be both fascinating and very useful for debugging. This brings me back to my example that illustrates the problem:

It reads in a text file and outputs each line unless the line ends with a "?". In that case, it should pause for user to clarify what was meant by that line. Currently my program outputs all lines first and waits for the clarifications at the end.

Is there any way to force node.js to pause for command-line input precisely for the cases where the conditional fires (i.e., where the line ends with a "?")?

var fs = require("fs");
var filename = "";
var i = 0;
var lines = [];

// modeled on http://st-on-it.blogspot.com/2011/05/how-to-read-user-input-with-nodejs.html
var query = function(text, callback) {
    process.stdin.resume();
    process.stdout.write("Please clarify what was meant by: " + text);
    process.stdin.once("data", function(data) {
        callback(data.toString().trim());
    });
};

if (process.argv.length > 2) {
    filename = process.argv[2];
    fs.readFile(filename, "ascii", function(err, data) {
        if (err) {
            console.error("" + err);
            process.exit(1);
        }
        lines = data.split("\n");
        for (i = 0; i < lines.length; i++) {
            if (/\?$/.test(lines[i])) { // ask user for clarification
                query(lines[i], function(response) {
                    console.log(response);
                    process.stdin.pause();
                });
            }
            else {
                console.log(lines[i]);
            }
        }
    });
}
else {
    console.error("File name must be supplied on command line.");
    process.exit(1);
}  
Meredith
  • 3,928
  • 4
  • 33
  • 58
blandish
  • 1,245
  • 1
  • 9
  • 11

5 Answers5

157

Here's another way that has no dependencies (readline is built-in)

const readline = require('readline');

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

    return new Promise(resolve => rl.question(query, ans => {
        rl.close();
        resolve(ans);
    }))
}


const ans = await askQuestion("Are you sure you want to deploy to PRODUCTION? ");
mpen
  • 272,448
  • 266
  • 850
  • 1,236
  • 4
    This is so perfect and concise and lovely! Thanks! – Henry Blyth Jan 02 '20 at 17:18
  • I am having `ReferenceError: require is not defined in ES module scope, you can use import instead` error – alper Jun 30 '21 at 14:10
  • 2
    @alper You use mjs or babel or something? `require` should work in plain old Node (not browser!). But anyway, just change it to `import * as readline from 'readline'` – mpen Jun 30 '21 at 18:36
  • I get an error: 'TypeError: input.on is not a function'. Don't know why – Tom Smykowski May 12 '22 at 13:38
  • 1
    @TomSmykowski No, I don't know why. `input.on` does not appear in this script, but try popping up a Node repl and type `process.stdin.on`. It should be a function. – mpen May 13 '22 at 01:46
13

The trick is to not do it itteratively, but do the for loop recursively. So that the next line is printOut in a callback, that is called either A: after the line gets printed, or B: after the console input has been processed.

var fs = require("fs");

// modeled on http://st-on-it.blogspot.com/2011/05/how-to-read-user-input-with-nodejs.html
function query(text, callback) {
    'use strict';
    process.stdin.resume();
    process.stdout.write("Please clarify what was meant by: " + text);
    process.stdin.once("data", function (data) {
        callback(data.toString().trim());
    });
}

function printLinesWaitForQuestions(lines, someCallbackFunction) {
    'use strict';

    function continueProcessing() {
        if (lines.length) {
            printNextLine(lines.pop());
        } else {
            someCallbackFunction();
        }
    }

    function printNextLine(line) {

        if (/\?$/.test(line)) { // ask user for clarification
            query(line, function (response) {
                console.log(response);
                process.stdin.pause();
                continueProcessing();
            });
        } else {
            console.log(line);
            continueProcessing();
        }
    }

    continueProcessing();
}

if (process.argv.length > 2) {
    var filename = process.argv[2];
    fs.readFile(filename, "ascii", function (err, data) {
        'use strict';

        if (err) {
            console.error("" + err);
            process.exit(1);
        }

        var lines = data.split("\n");
        printLinesWaitForQuestions(lines, function () {
            console.log('Were done now');
        });
    });
} else {
    console.error("File name must be supplied on command line.");
    process.exit(1);
}

This is a good solution for two reasons:

  1. It's relatively clean and the entire process can be contained within its own function closure, potentially leading to modularization.
  2. It doesn't break other asynchronous things you may want to do. There is no iterative waiting for loop and only one async task being launched per array of lines that you have. What if, in your version, you had millions of lines? You would have spun up millions of async outputs instantaneously... BAD! The recursive method not only allows better concurrency of other async work you want to do, but you don't clog the event loop with mini async tasks from one function call. This can cause memory issues, performance degradation, and other issues worthy of avoiding, particularly on large inputs.
Meredith
  • 3,928
  • 4
  • 33
  • 58
MobA11y
  • 18,425
  • 3
  • 49
  • 76
  • 1
    Wow, that helps a lot! Not only does it solve that problem, I can see how it generalizes. Thank you so much. – blandish Aug 13 '13 at 15:44
13

I found a module that does this really easily for yes or no:

https://www.npmjs.com/package/cli-interact

Install: npm install cli-interact --save-dev

How to use is taken straight from the npm site:

var query = require('cli-interact').getYesNo;
var answer = query('Is it true');
console.log('you answered:', answer);
user3413723
  • 11,147
  • 6
  • 55
  • 64
  • 5
    I appreciate the suggestion and it looks promising; but crashes when I run it. However, it refers to [link]https://github.com/anseki/readline-sync which also looks promising. – blandish Jun 30 '15 at 17:20
4

Here's the same answer as mpen, but without the confusing / unnecessary promise wrapper:

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

rl.question('Press [Y] to continue: ', ans => {
    if (ans == 'y') console.log('i will continue')
    else console.log('i will not continue');
    rl.close();
});
stackers
  • 2,701
  • 4
  • 34
  • 66
  • 1
    How do you make it wait for the input? I tried and code proceeds ahead without waiting and taking cli input. – Majoris Jul 07 '22 at 20:21
0

In the documentation I found this, without using a promise wrapper.

const readline = require('node:readline');
const { stdin: input, stdout: output } = require('node:process');

const rl = readline.createInterface({ input, output });

rl.question('What do you think of Node.js? ', (answer) => {
  // TODO: Log the answer in a database
  console.log(`Thank you for your valuable feedback: ${answer}`);

  rl.close();
});