Parse User Input in Node.js with readline

Share this video with your friends

Social Share Links

Send Tweet
Published 2 years ago
Updated 2 years ago

Reading user input is a big aspect of this project because we will be asking for a question or an answer for the CLI to work with.

Node has some internal options for parsing user input. We will check out the readline module and how it parses input. We will also see the promise driven version of the module and use a top level await to ask questions in the CLI.

This package has some limitations in how you present questions to your user though so if presentation is important to you, you will want to consider using a 3rd party library.

Kevin Cunningham: [0:00] For this project, the ability to be able to ask a user a question and respond to that question is going to be super important. We can do that with using Node internals or by leaning on a third-party library. Let's look at Node internals first. [0:14] Node internals has a library called readline. I can import it like that, import readline from 'readline', or I can prefix it with node: . This is true for any of the Node internal libraries, a mixture that you're always going to get, the Node library and not another package that was name-squatting. Let's look at two ways to be able to deal with this asking a question.

[0:40] For both of these approaches, we'll create an interface. An interface is an example of a duplex stream, one that has both an input and an output. We want to parse in a few options. We want to tell our interface that we are using a terminal, so we'll set that to true. We want to tell our interface that the input we're expecting is going to be from stdin.

[1:00] We want to tell our interface that the output is in stdout. Then, we'll ask our question. We set a variable to capture the input. This interface has an EventListener for the input. I'm going to listen to the input for every key pressed. That gives me an event.

[1:14] It also gives me some more data, a readline data. Let's console.log into those and see what we get. What is your name? It's waiting. If I type in K, I get K because of the stdout. I get K because that's the event. I get this object because that's rl. That's what I'm getting from the readline interface.

[1:33] I'm getting the sequence, the name of the character, whether or not Ctrl was held on, whether or not the method key was held on, whether or not the Shift was held on. I hold on Shift, then K, Shift is true, if I don't, then Shift is false. If I hold on Ctrl and then K, the sequence I get is \x0b, which is what Ctrl-K looks like, true and K.

[1:57] We don't want to just console.log these. We want to check whether or not the return key has been pressed. I pressed the return. We can see it's got a name of return. I want to check if rl.name === "return". Then, I want to console.log("Your name is"), and then that input that we've been collecting. Then, maybe we'll have the next question. "Where do you live?"

[2:20] If it's not returned, all I want to do is to append to that input whatever the event was. Let's try it now. What's your name? Kevin. Your name is Kevin. Where do you live? We can add custom logic to ask the next question, clear out the input collected, and to keep going.

[2:38] If, like me, you're using Node 18, which is the current LTS -- actually, if you're using Node 17, this is true for you as well -- you can use the promises version of readline. To do that, all I need to do is to rename this import from readline to readline/promises. We create the interface in the same way but now we can await the only answer.

[2:59] Instead of having to do with any of this, and instead of logging the question, we can instead get the answer and with readline, a question, "What is your name?" Because we're in module mode, in the SM mode, that we've got top-level await, we do need to wrap that the function.

[3:17] We can now console.log and say, "Your name is." Then we're finished, we can call close(), so we're finished for interface. Let's try that. What's your name? Kevin. Your name is Kevin. We don't call close. We're still waiting for some input. We could have a second question here.

[3:39] We could have, answer2 = await rl.question("Where do you live?"). Then console.log( "You live in ${answer2}." ). Try that. What's your name? Kevin. Where do you live? Northern Ireland. I live in Northern Ireland. Then I need to call rl.close, when I'm finished asking and answering this question. What's your name? Kevin. Where do you live? Northern Ireland.

[4:01] We get quite far here just by using readline. Lots of functionality that we could see we could build here, but there are things like using color on the command line, having multiple choice questions. Lots of different ways we could imagine user input being passed in on the command line.

[4:19] This method, while functional, isn't this beautiful for our user. This might be the point where you think, is there a third-party package that can do this work for me? I want to focus on the core functionality of my tool and dealing with multiple terminals and operating system, field beyond the scope of what I'm doing.

Andrew
Andrew
~ 2 years ago

don't understand how u used rl.input

I can't find .input even in documentation

Andrew
Andrew
~ 2 years ago

oh, I think I have to make an extended interface, listener must be put on stdin. Sorry for misunderstanding. Typescript sometimes is tricky ))

Xavier Glab
Xavier Glab
~ 2 years ago

I am using node v18.13.0 and copied your code example with promisified readline from the github and got an error. Started googing around and found example on this page https://www.typeerror.org/docs/node/readline which changes your import:

import readline from "node:readline/promises";

to

import * as readline from "node:readline/promises";

and it worked.

Just a note for future self and others.

Xavier Glab
Xavier Glab
~ 2 years ago

Ok - the import was not a problem, in your code example you have

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

So both input and output maps to process.stdin, the output should be mapped to process.stdout.

~ 2 years ago

I can't get this example to work, and show keypress events. The only way I got it to show keypress events was:

import readline from "node:readline"; import { stdin as input, stdout as output } from "node:process";

const rl = readline.createInterface({ terminal: true, input, output }); readline.emitKeypressEvents(input, rl); process.stdin.on("keypress", (key, seq) => console.log(key, seq));

Kevin Cunningham
Kevin Cunninghaminstructor
~ 2 years ago

I have rebuilt this using v18.13.0. And you're right Xavier, although in the video I correctly had output mapped to stdout, in the repo it's mapped to stdin - I've fixed that now!

Here's the version using event listeners and not promises that is in the middle of the video. Hopefully that helps the last commenter.

import readline from "node:readline";

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

let input = "";

rl.input.on("keypress", (event, rl) => {
  if (rl.name === "return") {
    console.log(`Your name is ${input}.`);
  } else {
    input += event;
  }
});

and here is the promises version (which is also available in the repo but the output is fixed now)

import readline from "node:readline/promises";

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

const answer = await rl.question("What is your name?");

console.log(`Your name is ${answer}.`);

const answer2 = await rl.question("Where do you live? ");

console.log(`You live in ${answer2}.`);
rl.close();

Hope that helps and reach out again if I can help more!

Kevin.

Markdown supported.
Become a member to join the discussionEnroll Today