Interacting with the file system
Standard in (stdin
) refers to an input stream that a program can use to read input from a command shell or Terminal. Similarly, standard out (stdout
) refers to the stream that is used to write the output. Standard error (stderr
) is a separate stream to stdout
that is typically reserved for outputting errors and diagnostic data.
In this recipe, we’re going to learn how to handle input with stdin
, write output to stdout
, and log errors to stderr
.
Getting ready
For this recipe, let’s first create a single file named greeting.js
. The program will ask for user input via stdin
, return a greeting via stdout
, and log an error to stderr
when invalid input is provided. Let’s create a directory to work in, too:
$ mkdir interfacing-with-io $ cd interfacing-with-io $ touch greeting.js
Now that we’ve set up our directory and file, we’re ready to move on to the recipe steps.
How to do it…
In this recipe, we’re going to create a program that can read from stdin
and write to stdout
and stderr
:
- First, we need to tell the program to listen for user input. This can be done by adding the following lines to
greeting.js
:console.log('What is your name?'); process.stdin.on('data', (data) => { // processing on each data event });
- We can run the file using the following command. Observe that the application does not exit because it is continuing to listen for
process.stdin
data events:$ node greeting.js
- Exit the program using Ctrl + C.
- We can now tell the program what it should do each time it detects a data event. Add the following lines below the
// processing on each data
event
comment:const name = data.toString().trim().toUpperCase(); process.stdout.write(`Hello ${name}!`);
- You can now type input to your program. When you press Enter, it will return a greeting and your name in uppercase:
$ node greeting.js What is your name? Beth Hello BETH!
- We can now add a check for whether the input string is empty and log to
stderr
if it is. Change your file to the following:console.log('What is your name?'); process.stdin.on('data', (data) => { // processing on each data event const name = data.toString().trim().toUpperCase(); if (name !== '') { process.stdout.write(`Hello ${name}!`); } else { process.stderr.write('Input was empty.\n'); } });
- Run the program again and hit Enter with no input:
$ node greeting.js What is your name? Input was empty.
We’ve now created a program that can read from stdin
and write to stdout
and stderr
.
How it works…
The process.stdin
, process.stdout
, and process.stderr
properties are all properties on the process object. A global process object provides information and control of the Node.js process. For each of the I/O channels (standard in, standard out, standard error), they emit data events for every chunk of data received. In this recipe, we were running the program in interactive mode where each data chunk was determined by the newline character when you hit Enter in your shell.
The process.stdin.on('data', (data) => {...});
instance is what listens for these data events. Each data event returns a Buffer
object. The Buffer
object (typically named data
) returns a binary representation of the input.
The const name = data.toString()
instance is what turns the Buffer
object into a string. The trim()
function removes all whitespace characters – including spaces, tabs, and newline characters – from the beginning and end of a string. The whitespace characters include spaces, tabs, and newline characters.
We write to stdout
and stderr
using the respective properties on the process object (process.stdout.write
, process.stderr.write
).
During the recipe, we also used Ctrl + C to exit the program in the shell. Ctrl + C sends SIGINT
, or signal interrupt, to the Node.js process. For more information about signal events, refer to the Node.js Process API documentation: https://nodejs.org/api/process.html#process_signal_events.
Important note
Console APIs: Under the hood, console.log
and console.err
are using process.stdout
and process.stderr
. Console methods are higher-level APIs and include automatic formatting. It’s typical to use console methods for convenience and lower-level process methods when you require more control over the stream.
There’s more…
As of Node.js 17.0.0, Node.js provides an Experimental Readline Promises API, which is used for reading a file line by line. The Promises API variant of this allows you to use async
/await
instead of callbacks, providing a more modern and cleaner approach to handling asynchronous operations.
Here is an example of how the Promises API variant can be used to create a similar program to the greeting.js
file created in the main recipe:
const readline = require('node:readline/promises'); async function greet () { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const name = await rl.question('What is your name?\n'); console.log(`Hello ${name}!`); rl.close(); } greet();
This Node.js script utilizes the node:readline/promises
module, which provides the Promise
variant of the Readline API. It defines an asynchronous function, greet()
, which prompts the user for their name in the console and then greets them with a personalized message – similar to the main recipe program. Using the Readline Promises API allows us to use the async
/await
syntax for cleaner asynchronous code flow. We’ll cover more about the async
/await
syntax in later recipes and chapters.
See also
- The Decoupling I/O recipe in Chapter 3