Node.js version: 22.1.0.
Using Node.js to create your own scripts works well. And if you're used to using Javascript or Typescript, writing your scripts will be very quick. But while I was using k9s (a CLI manager for kubernetes), I wondered if it was possible to make such a tool with Node.js. Without going that far, I embarked on a small project to analyze the Readline module in Node.js. This is to be used both in scripts and in more complete CLI applications.
I’m going to detail the difficulties I encountered in using Node.js Readline and creating a basic CLI application. (And not the module itself, read the doc!) I've put together all the solutions to the problems I encountered right here, which should give you a comprehensive guide to understanding and avoiding the same complications.
This blog is longer than initially expected and will be divided into three parts:
- Part one - the basics - is below.
- Part two - going further - is here: (coming start of March 2025)
- Part three - advanced notes - is here: (coming end of March 2025)
About my test project.
To learn new terms in another language, I've often used paper cards. On one side there's a word in one language, on the other side the word in the other language. The exercise is perfect for this project. The application has to display one side of a “card”, the user responds, validates the answer, and moves on to the next card. In the course of the project, I added “game” features such as clues, lives, time, etc. to the application.
I've used as few external libraries as possible in order to fully understand readline's capabilities and limitations.
You can access the project in question here: https://github.com/ger-benjamin/cli-learning-cards/tree/1.1.0
The basics
Readline “question”
To question the user, my first approach was to use Readline's question
method.
import { createInterface } from 'node:readline';
import { stdin, stdout } from 'node:process';
const rl = createInterface({
input: stdin,
output: stdout,
});
rl.question("What's your name?\n", (answer) => {
console.log(`Hello ${answer}`);
rl.close();
});
The result is as follows:
> node test.mjs
What's your name?
Zorro
Hello Zorro
And it works well... but it's not very flexible when it comes to the UI, as we'll come back to later.
Note that I used createInterface
from node:readline
. The equivalent is possible with node:readlinePromise
, but instead of a callback, you'll use an await
to get your response. The advantage of callback here is that I don't have to wait for the response to execute other things.
I start by creating a readline interface. I bind to it the standard input (stdin
, the keyboard), and the standard output (stdout
, the terminal) of the process. This would also make it possible to link input and output to a file, for example.
As the interface is an instance, it is possible to have several instances. This may be practical (we will see that later), but not necessarily with rl.question
.
Readline stream
Note that createInterface
creates a stream (duplex). And since in my example it's linked to stdin and stdout, there's no reason for the stream to stop on its own. This allows me to keep the process active.
After printing my response via a console.log, I close the stream to exit the process. In this case, rl.pause()
would amount to the same thing, since the stream here is keeping the process alive.
Since I'm using readline as a stream (and not as a promise), I have access to certain events. So I can perform the same script with rl.(“on”, (line) => callback);
import { createInterface } from 'node:readline';
import { stdin, stdout } from 'node:process';
const rl = createInterface({
input: stdin,
output: stdout,
});
rl.on("line", (line) => {
console.log(`Hello ${line}`);
rl.close();
});
console.log("What's your name?");
A subtle difference here is that I write my question with console.log
, and console.log
automatically adds a line break. This was not the case with rl.question
, which in my case is based on stdout.write. This kind of difference is listed here for example: https://www.geeksforgeeks.org/difference-between-process-stdout-write-and-console-log-in-node-js/
This way of writing the code is a little more flexible, since it's not necessary to call rl.question
after each entry. But this is not exclusive, and you can mix the two:
import { createInterface } from 'node:readline';
import { stdin, stdout } from 'node:process';
const rl = createInterface({
input: stdin,
output: stdout,
});
const printQuestion = () => console.log("What's you name?");
rl.on("line", (line) => {
console.log(`Hello ${line}`);
rl.close();
}).on("SIGINT", () => {
rl.question("Are you sure you want to exit?\n", (answer) => {
if (answer.match(/^y(es)?$/i)) {
rl.close();
} else {
printQuestion();
}
});
});
printQuestion();
Here, a confirmation with question
is requested before closing the process.
Key concepts
A few key points on the basic principles:
-
Readline.createInterface
allows you to createstream
orpromise
instances ofReadline.Interface
(Be careful not to accidentally mix the two). - An open stream keeps the process alive.
- Stdin and stdout are linked to one process.
- Stdin and stdout are unique to a process.
-
rl.question()
orrl.on(‘line’, cb)
allows you to read the input. -
Console.log
, orstdout.write
, orrl.write
, allows you to write to the output.
Part two - going further - is here: (coming start of March 2025)
Part three - advanced notes - is here: (coming end of March 2025)
Top comments (0)