Writing CLI Programs in Node.js
Node.js·51 min read·Jan 1, 2025
Node.js is a great versatile tool that packages all the functionalities needed to write a variety of command-line interface programs, such as file management tools, automation scripts, development utilities, API testing tools, and more.
It has access to the underlying APIs of the operation system through the core modules, as well as environment variables, and command-line arguments through the global process object.
Writing an entry point
In software development, an entry point is a point in a program where its execution begins and where it has access to command line arguments.
A common way to implement an entry point in Node.js is to set up a try...catch block in charge of catching and gracefully handling any eventual error that may occur during the script's execution:
// import modules
try {
// execute main logic
} catch(error) {
// handle errors
}
Writing an asynchronous entry point
An elegant way to implement an asynchronous entry point in Node.js is to use an immediately invoked function expression (IIFE), whose anonymous function is declared using the async keyword, which will in turn enable the future use of the await operator within the function's body.
// import modules
(async () => {
try {
// execute main logic
} catch(error) {
// handle errors
}
})();
Setting an exit status
By convention, programs running on Unix-like operating systems will exit with the value 0 if everything went well, and another value between 1 and 255 if something went wrong.
In Node.js, to set a custom exit status and immediately terminate the execution of the program, you can use the global process.exit() method that takes as argument an optional integer representing the exit code:
// import modules
(async () => {
try {
// execute main logic
} catch(error) {
// handle errors
process.exit(1);
}
process.exit(0);
})();
Note: By default, the exit code will be set to
0if it is unspecified.
Accessing command-line arguments
To access the arguments supplied to a Node.js script through the command-line, you can use the global process.argv array:
process.argv[index]
Where:
-
process.argv[0]is the absolute path to the Node.js binary used to execute the script. -
process.argv[1]is the absolute path to the script being executed. -
process.argv[2+n]are the actual command-line arguments supplied to the script.
Note: The elements contained in the
process.argvarray are always strings, even if they represent numbers or other types. Consequently, you will need to explicitly convert them using functions likeparseInt()orJSON.parse().
Example
Let's consider this script, that prints the values of the global process.argv array:
console.log('Node.js path:\n - ' + process.argv[0]);
console.log('Script path:\n - ' + process.argv[1]);
console.log('Command-line arguments:');
for (let i = 2 ; i < process.argv.length ; i++) {
console.log(' - ' + process.argv[i]);
}
Which will produce this output:
$ node index.js Hello World
Node.js path:
- /Users/razvan/.nvm/versions/node/v20.3.0/bin/node
Script path:
- /Users/razvan/scripts/index.js
Command-line arguments:
- Hello
- World
Parsing command-line arguments
Parsing command-line arguments refers to the process of extracting and interpreting the values passed to a program in order to customize its behavior.
In Node.js, to parse command flags and positional arguments, you can use the parseArgs() method of the util core module:
const { parseArgs } = require('node:util');
const { values, positionals } = parseArgs({
options,
allowPositionals?
});
Where:
-
valuesis an object mapping the parsed flags and their values. -
positionalsis an array containing positional arguments. -
optionsis an object used to describe flags. -
allowPositionalsis an optional boolean used to enable positional arguments. Defaults tofalse.
Defining command flags
To define command flags, you can use the following syntax:
{
options: {
flag: {
type,
multiple?,
short?,
default?
},
...
}
}
Where:
-
flagis a string describing the name of the flag in the long format (e.g.,'long'->'--long'). -
typeis a string describing the type of the argument's value. Must be either'string'or'boolean'. -
multipleis a boolean indicating whether this option can be provided multiple times. Iftrue, all values are collected in an array, otherwise, values for the option are last-wins. Defaults tofalse. -
shortis a single character describing the name of the flag in the short format (e.g.,'l'->'-l'). -
defaultis either a string, a boolean, or an array of either type. It must be of the same type as thetypeproperty. When multiple istrue, it must be an array.
Note: It is usually recommended to implement a
--helpor-hflag to display a concise guide on how to use the CLI, including common examples and usage patterns.
Example
Let's consider this script, that re-implements the Unix head utility:
// File: head.js
const { readFileSync } = require('node:fs');
const { parseArgs } = require('node:util');
const { basename } = require('node:path');
try {
// Extract the filename of the script
const scriptName = basename(process.argv[1]);
// Define the `--bytes`, `--lines`, and `--help` flags
let { values: flags, positionals: files } = parseArgs({
options: {
'bytes': {
type: 'string',
short: 'c'
},
'lines': {
type: 'string',
short: 'n'
},
'help': {
type: 'boolean',
short: 'h',
default: false
}
},
allowPositionals: true
});
// Output the script manual if the `--help` flag is set to `true` or no files supplied
if (flags.help || !files.length) {
const scriptManual = `NAME
${scriptName} – display first lines of a file
SYNOPSIS
$ node ${scriptName} [-n count | -c bytes] file ...
OPTIONS
-c bytes, --bytes=bytes
Print bytes of each of the specified files.
-n count, --lines=count
Print count lines of each of the specified files. Defaults to 10.
EXIT STATUS
The ${scriptName} script exits 0 on success, and >0 if an error occurs.`;
console.log(scriptManual);
process.exit(0);
}
// Convert the flags values into integers
flags.bytes = parseInt(flags.bytes);
flags.lines = parseInt(flags.lines);
// Output an error message if both flags are in use
if (!isNaN(flags.bytes) && !isNaN(flags.lines)) {
console.error(`${scriptName}: can\'t combine line and byte counts`);
process.exit(1);
}
// Loop on positional arguments
for (let file of files) {
// Read the content of the current file
const contents = readFileSync(file, { encoding: 'utf8' });
// Output the name of the current file if there are more than one
if (files.length > 1) {
console.log(`==> ${file} <==`);
}
// Output the specified number of bytes
if (flags.bytes) {
process.stdout.write(contents.slice(0, flags.bytes));
}
// Output the specified number of lines or by default 10
else {
process.stdout.write(contents.split('\n').slice(0, flags.lines || 10).join('\n'));
}
}
process.exit(0);
} catch(error) {
console.error(error);
process.exit(1);
}
Which will produce this output:
$ node head.js --help
NAME
head.js – display first lines of a file
SYNOPSIS
$ node head.js [-n count | -c bytes] file ...
OPTIONS
-c bytes, --bytes=bytes
Print bytes of each of the specified files.
-n count, --lines=count
Print count lines of each of the specified files. Defaults to 10.
EXIT STATUS
The head.js script exits 0 on success, and >0 if an error occurs.
$ node head.js head.js
// File: head.js
const { readFileSync } = require('node:fs');
const { parseArgs } = require('node:util');
const { basename } = require('node:path');
try {
let { values: flags, positionals: files } = parseArgs({
options: {
'bytes': {
$ node head.js -c 10 head.js
// File: h
$ node head.js -n 5 head.js
// File: head.js
const { readFileSync } = require('node:fs');
const { parseArgs } = require('node:util');
const { basename } = require('node:path');
$ node head.js -n 5 -c 10 head.js
head.js: can't combine line and byte counts
Accessing environment variables
To access the environment variables available in the shell session the script is launched from, you can use the global process.env object:
process.env.ENVIRONMENT_VARIABLE
Notes:
While this object can be modified by the script, such modifications won't be reflected outside of the Node.js process.
It is usually recommended to provide fallback values in case environment variables are
undefined.
Example
Let's consider this script, that re-implements the Unix printenv utility:
// File: printenv.js
// Remove the first 2 elements of the `argv` array
const args = process.argv.slice(2);
// Check if there are positional arguments
if (args.length > 0) {
// Retrieve the last positional argument
const varName = args[args.length - 1];
// Check if the variable exists in the `env` object
if (varName in process.env) {
// Output its value
console.log(process.env[varName]);
} else {
// Terminate with an error
process.exit(1);
}
} else {
// Loop on each key of the `env` object
for (let key of Object.keys(process.env)) {
// Output the key-value pair
console.log(`${key}=${process.env[key]}`);
}
}
// Terminate without errors
process.exit(0);
Which will produce this output:
$ node printenv.js
SHELL=/bin/bash
USER=razvan
PWD=/Users/razvan/learnbackend/scripts/printenv.js
PS1=\u:\w$
SHLVL=1
HOME=/Users/razvan
$ node printenv.js USERNAME
$ node printenv.js SHELL
/bin/bash
$ node printenv.js SHELL USER
razvan
Making a script executable system-wide
To invoke your Node.js script directly without having to prefix it with the node command, you can add this shebang directive at the very top of your script:
#!/usr/bin/env node
Make you script executable using the chmod command:
$ chmod +x script.js
And copy it into one of the directories specified in your PATH environment variable, such as /usr/local/bin:
$ cp script.js /usr/local/bin/.
🗒️ Summary
Here's a summary of what you've learned in this lesson:
-
The global
process.exit()method is used to set the exit status of the script. -
The global
process.argvarray contains the command-line arguments supplied to the script. -
The
util.parseArgs()static method allows to define and parse command-line flags. -
The global
process.envobject contains the environment variables present in the script's environment.
🤖 Projects
Here's a list of projects to apply what you've just learned:
- lb_bc: Write a command-line script that partially reimplements the
bcutility.
Unlock the Build CLI Apps in JavaScript & Node.js module
Learn how to build, integrate, and safeguard scalable CLI tools, scripts, and apps with JavaScript, Node.js, npm, and Git.
You get immediate access to:
- 45 focused lessons across JavaScript, Node.js, npm, and Git
- 17 real-world projects with commented solutions
- Ongoing updates to this bundle
- Lifetime access to this bundle