Building a NodeJS REPL CLI system

PeterZ
2 min readFeb 4, 2024

REPL stands for Read-Eval-Print Loop and is often referred to as a simple interactive computer programming environment that takes single user inputs, executes them, and returns the result to the user [1]. For intance, NodeJS provides such an evironment after you execute nodein a terminal. The REPL allows you to type JS code and execute it in NodeJS right away.

Having understood that, you can think of a REPL CLI system as a REPL environment that only executes some predefined commands. Such a system would be useful especially when you want to create or udpate some state information which is used by other commands as inputs.

A simple example

By using the repl module shipped with NodeJS and Commander.js, we can build such a system with relative ease. Here, we demonstrate it by creating a simple system that allows users to use command name to save their names and command hello to print a message to say hello to that name.

Commands:
name <name> Save name
hello Say hello to saved name

Check index.ts for the full code.

Setting up CLI

We first load Commander.js

import { Command } from 'commander';

and generate an instance of class Command

const program = new Command();
program
.version('0.0.1')
.description('My custom REPL');

We then creates two separate sub-commands

let _name: string = '';
const nameCmd = program.command('name');
nameCmd
.description('Save name')
.arguments('<name>')
.action((name: string) => { // define the callback as async if
_name = name; // the function is asynchronous
console.log(`Name saved: ${name}`);
});

const helloCmd = program.command('hello');
helloCmd
.description('Say hello to saved name')
.action(() => {
if(_name.length > 0) {
console.log(`Hello, ${_name}`);
} else {
console.log('No name saved');
}
});

Overriding exit behaviours

By default, commander calls process.exit when it detects errors, or after displaying the help or version. We don't want such a behaviour since it would termiate the program. Intead, we only want the error message shown in the terminal. Luckly, commander allows us to do that by exitOverride:

program.exitOverride();
nameCmd.exitOverride();
helloCmd.exitOverride();

Note that we have to call exitOverride not only for program, but for nameCmd and nameCmd. It is important to call exitOverride for all instances of class Command during the creation of the CLI system.

Setting up REPL

The last thing to do is to call function start from module repl. We need to first import start at the beginning:

import { start } from 'repl';

then add

start({
prompt: 'my-repl> ',
ignoreUndefined: true,
eval: (cmd, context, filename, callback) => {
const args = cmd.trim().split(' ');
try {
program.parseAsync(args, { from: 'user' });
callback(null, undefined);
} catch (err) {
callback(null, undefined);
}
}
});

To run the code, install typescript and ts-node and execute

npx ts-node ./index.ts

References

[1] https://en.wikipedia.org/wiki/Read–eval–print_loop

--

--