7

I'm not quite grasping how exactly node works regarding async and loops. What I want to achieve here is have the console print out "Command: " and await for the user's input. But while it's waiting I want it to run "someRandomFunction()" endlessly until the user inputs "exit" onto the terminal.

Would appreciate all the help - and possibly an explanation so I can understand!

Thank you! :)

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

rl.question("Command: ", function(answer) {
    if (answer == "exit"){
        rl.close();
    } else {
        // If not "exit", How do I recall the function again?
    }
});

someRandomFunction();
Bryan
  • 1,438
  • 3
  • 15
  • 24
  • Node.js does not execute scripts in parallel. If you really have endless-loop it will block your callback that is waiting for user input. So what do you want to do in your `someRandomFunction` ? – t.niese Jun 12 '14 at 10:32
  • Why not use `setInterval()` with `someRandomFunction()` ? The key is to make `someRandomFunction()` not take very long and accumulate its step to a closure or global. When it runs, nothing else does. – Paul Jun 12 '14 at 10:37
  • @t.niese I ran into this problem and I just figured it out this morning (before reading your post) about why it wouldn't let me input into the terminal. Thanks for the note! – Bryan Jun 12 '14 at 16:58
  • @Paul I thought about setInterval but had a different use for running the someRandomFunction - ended up using cron because it pertained to my need. Thanks for the insight! – Bryan Jun 12 '14 at 17:02

3 Answers3

11

I would suggest making the function repeatable like so.

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

var waitForUserInput = function() {
  rl.question("Command: ", function(answer) {
    if (answer == "exit"){
        rl.close();
    } else {
        waitForUserInput();
    }
  });
}

Then call

waitForUserInput();
someRandomFunction();

I'm unsure if the syntax you are using for .question is correct though, does that part of the code work?

You may also write this in the following manner.

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

function waitForUserInput() {
  rl.question("Command: ", function(answer) {
    if (answer == "exit"){
        rl.close();
    } else {
        waitForUserInput();
    }
  });
}

The important lesson here is that to re-use a function it has to be named and be available in scope. If you have any more questions about this please ask.

Pablo Jomer
  • 9,870
  • 11
  • 54
  • 102
  • 1
    The whole thing works now! Thank you! .question works, I got its function from [readline](http://nodejs.org/api/readline.html#readline_rl_question_query_callback) – Bryan Jun 12 '14 at 16:55
  • Cool I guess its some library unknown to me. :D Hope you can continue your project now! – Pablo Jomer Jun 13 '14 at 06:03
  • 1
    I'm one short from unlocking upvotes, will do when I unlock it! – Bryan Jun 13 '14 at 17:28
  • 3
    Pardon the noob question, but doesn't this keep adding to the stack by continually calling itself? – joelc May 28 '17 at 21:31
  • Could you test it? – Pablo Jomer May 29 '17 at 08:52
  • 1
    You can test it with the simple html and js file run it and inspect using chrome source > callstack, refer this post to test it out: https://stackoverflow.com/questions/10761894/does-chrome-have-a-built-in-call-stack#:~:text=3359.139%20%5DGo%20to%20Developer%20Tools,To%20print%20the%20call%20stack. It'll add new entry to the call stack every time we make call back to same function. – Akshay Jan 28 '21 at 18:15
8

The other answer is fine but uses recursion unnecessarily.

The key is to solving this is to separate, in your mind, the simple loop-based approach that is used in other languages from the asynchronous approach of Node.

In other languages, you might use a loop like this:

while not finished:
  line = readline.read() 
  if line == 'woof':
    print('BARK')
  elif line == 'exit':
    finished = True
  ... # etc

Node, at least with Readline, doesn't work this way.

In Node, you fire up Readline, give it event handlers, then return, and handle the completion of the readline loop later.

Consider this code, which you can copy-paste-run:

const readline = require('readline');

function replDemo() {
  return new Promise(function(resolve, reject) {
    let rl = readline.createInterface(process.stdin, process.stdout)
    rl.setPrompt('ready> ')
    rl.prompt();
    rl.on('line', function(line) {
      if (line === "exit" || line === "quit" || line == 'q') {
        rl.close()
        return // bail here, so rl.prompt() isn't called again
      }

      if (line === "help" || line === '?') {
        console.log(`commands:\n  woof\n  exit|quit\n`)
      } else if (line === "woof") {
        console.log('BARK!')
      } else if (line === "hello") {
        console.log('Hi there')
      } else {
        console.log(`unknown command: "${line}"`)
      }
      rl.prompt()

    }).on('close',function(){
      console.log('bye')
      resolve(42) // this is the final result of the function
    });
  })
}

async function run() {
  try {
    let replResult = await replDemo()
    console.log('repl result:', replResult)

  } catch(e) {
    console.log('failed:', e)
  }
}

run()

Run this and you'll get output like this:

$ node src/repl-demo.js
ready> hello
Hi there
ready> boo
unknown command: "boo"
ready> woof
BARK!
ready> exit
bye
repl result: 42

Note that the run function calls replDemo and "awaits" the result of the promise.

If you're unfamiliar with async/await, here's the same logic written it the "traditional" Promise style:

function run2() {
  replDemo().then(result => {
    console.log('repl result:', result)
  }).catch(e => {
    console.log('failed:', e)
  })
  console.log('replDemo has been called')
}

Note that I added output "replDemo has been called" for a reason - Running the above shows output like this:

$ node src/repl-demo.js
ready> replDemo has been called
woof
BARK!
ready> hello
Hi there
ready> bye
repl result: 42

Note how "replDemo has been called" appears immediately after the first "ready>" prompt. That's because the replDemo() function returns immediately, then run2() exits immediately, and the main is all done - yet the readline is still executing!

That's hard to grasp if you come from an imperative programming background like me. The async event-driven loop at the core of nodejs keeps running until all the work is done, and that occurs when the last promise is resolved, which happens when the readline instance is "closed", which happens when "exit" is entered by the user (or EOF is received, which is CTRL+D on most systems, and CTRL+Z on Windows).

Andrew E
  • 7,697
  • 3
  • 42
  • 38
0

I don't see an ES module version in the answers so far. Doing it this way is "cleaner" in 2 ways:

  1. Use of async (well, just a top-level await) helps avoid unnecessary "clutter" of callbacks or Promises/then(). Yes, Node specializes in async execution, but this isn't a multi-user web server... it's synchronous cmd-line end-user input use-case.
  2. More importantly, it allows for an infinite loop with a do-while (as it should be) instead of (yuck) recursion. The loop exits if "quit" is input by the end-user.

This snippet seems more reasonable than other solutions:

import * as readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';

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

do {
    var answer = await rl.question('Enter something: ');
    if (answer === 'quit') break;
    console.log(`You entered: ${answer}`);
} while(true);

rl.close();

Sample output:

$ node inploop.mjs
Enter something: ok
You entered: ok
Enter something: here
You entered: here
Enter something: quit
$
wescpy
  • 10,689
  • 3
  • 54
  • 53