Ever wondered howcreate-react-app .
orgit init
or simplynode -v
works? These are CLIs or Command Line Interfaces. I'm pretty sure most of us have used CLIs at some point of time in our lives. Even commands that we use everyday likels
orcd
are also CLIs.
Today, I'm going to show you how to set up a very simple CLI with some of the common features we find in CLIs. Nothing too complex... Let's dive right into CLIs
What are CLIs
CLIs are commands that we run in our terminal to do something. If you want a wikipedia definition 🙄 -
A command-line interface processes commands to a computer program in the form of lines of text. The program which handles the interface is called a command-line interpreter or command-line processor. Operating systems implement a command-line interface in a shell for interactive access to operating system functions or services.
Why are CLIs necessary?
In the modern world of GUIs(Graphical User Interfaces), you might ask that why should we know about CLIs? Weren't they used in the 80s? I agree with you a 💯 percent. They are outdated but a lot of old applications still use CLIs. The terminal/command prompt generally has more permissions and access compared to GUI applications by default(It's bad user experience to allow 100 permissions to run an app).
Why build CLIs with Node
The main advantage is the ecosystem of over 1 million packages we get with Node. Using these we can avoid boilerplate code and implement functionality easily.
Getting Started with CLIs
Before doing anything else, we need to create apackage.json
.
- Create an empty folder
- Run
npm init
and quickly fill in the options.
Let's create our javascript file, I'm naming itcli.js
. This is a common convention which is followed as the file immediately tells us about its function. I'm going to add aconsole.log('CLI')
to our file so that we can know that everything is working.
We need to update thepackage.json
to run our CLI. Add thebin
property into the file.
"bin": { "hello-world": "cli.js"}
Thehello-world
property is the command name we want to run and thecli.js
is the filename. If you want to use another name or your file is stored in a different path, update it accordingly.
Let's install our NPM package. In your terminal/command prompt run the following -
npm i -g .
We all have come acrossnpm install
in the past. We add the-g
flag so that NPM installs the package globally. The.
tells NPM to install the package. NPM installs the file that is there in themain
property of thepackage.json
. If you have changed
Now we can run the command name we set earlier(hello-world
) and our CLI should boot up.
hello
However, we get the following error -
The error seems to tell us that compiler is not able to understand that we have javascript code. So, to tell the compiler that it should usenode
to run our file. For this, we use ashebang. Add the following code at the top of your file.
#!/usr/bin/env node
This is the path to node. We tell *nix systems that the interpreter of our file should be at the specified path. IN windows, this line will be be ignored as it is specified as a comment but NPM will pick it up when the package is being installed.
There we go, now it should work.
We have not actually done anything in our CLI except logging some data. Let's begin by implementing common features in CLIs.
Prompts
Prompts are questions that are asked to the user. You might have come across it in Javascript by calling theprompt
function. I'm going to be using theprompts package. This package simplifies allows us to prompt the user and get the response with just a few lines of code.
Install it by running -
npm i prompts
Next, add the following code into your file and we will walkthrough it together.
const prompts = require('prompts');// IIFE for using async functions(async () => { const response = await prompts({ type: 'number', name: 'value', message: 'Enter your name', }); console.log(response.value); // the name the user entered.})();
Note: IIFE's are function expressions that are immediately called. It shortens saving the function expression to a variable and the calling it.
Our basic CLI should look like this -
Awesome right? I think the terminal looks a bit dull, let's colour it up!
Colours with Chalk
Do you know how colours are added into terminals? ANSI escape codes are used for this(sounds like a code for a prison). If you've ever tried to read binary, reading ANSI is quite similar.
Thankfully, there are packages that help us with this. I'm going to usechalk for the purpose of this tutorial.
Install it by running -
npm i chalk
Let's get started by replacing the originalconsole.log()
with the following.
console.log(chalk.bgCyan.white(`Hello ${response.value}`)); console.log(chalk.bgYellowBright.black(`Welcome to "Hello World with CLIs"`));
As you can see, the firstlog
chains thebgCyan
and thewhite
methods. Check theofficial documentation to know more.
After this, our code looks like this -
const response = await prompts({ type: 'text', name: 'value', message: 'What is your name?',});console.log(chalk.bgCyan.white(`Hello ${response.value}`)); console.log(chalk.bgYellowBright.black(`Welcome to "Hello World with CLIs"`));
That's a good start, now let's bump up the font size. I can barely see anything.
Playing with Fonts
Changing fonts are quite difficult through javascript and I spent a lot of time searching for a package that does just that. Thankfully, I got it. We'll be using thecfonts package. Go ahead and install it by running the following -
npm i cfonts
Begin by replacing our previousconsole.log
with the following -
CFonts.say(`Hello ${response.value}`, { font: 'tiny', colors: ['cyanBright'],});CFonts.say(`Welcome to Hello World with CLIs`, { font: 'simple', colors: ['cyanBright'],});
Our Hello World CLI looks like this -
Let's add some basic arguments like-n
or--name
and-v
or--version
. We'll be using theyargs package to simplify the process. If you don't want to use it, you can useprocess.argv
to access the arguments.
Begin by installing theyargs
package.
npm i yargs
Let's import it into our code -
const yargs = require('yargs/yargs');const { hideBin } = require('yargs/helpers');const argv = yargs(hideBin(process.argv)).argv;
Theargv
stands for all the arguments specified by the user. We use thehideBin()
method to remove the--
in front of arguments. Example -
hideBin('--example'); // output -> example
Now we can add a series ofIF and ELSEIF
checks for the arguments provided. You can also use aSwitch
statement.
const packageJson = require('./package.json');if (argv.version || argv.v) { console.log(packageJson.version);} else if (argv.name || argv.n) { console.log(packageJson.name);} else if (argv.init) { // Put our functionality of printing hello world, asking name etc.} else { console.log('Please specify an argument/command');}
Since package.json is essentially a javascript object, we can pull off the properties from it.yargs
helpfully provides an object of the specified arguments. We can check if the required properties. There's also anelse
check to see if the user has not given any arguments. Now let me show you some other common features in CLIs.
Loaders
Asynchronous and time consuming operations are very common in CLIs. We don't want to leave the user thinking his computer has hung up. We'll use theora package for loaders. Begin by installingora
-
npm i ora
Let's require the package and setup a simple loader -
const ora = require('ora');const spinner = ora('Loading...').start();
This loader will run forever because we haven't stopped it. We don't have any code that takes a substantial amount of time(API requests, data processing etc), We'll usesetTimeout()
to simulate the end of loading.
const spinner = ora('Loading...').start();setTimeout(() => { spinner.stop(); (async () => { const response = await prompts({ type: 'text', name: 'value', message: 'What is your name?', }); CFonts.say(`Hello ${response.value}`, { font: 'tiny', colors: ['cyanBright'], }); CFonts.say(`Welcome to Hello World with CLIs`, { font: 'simple', colors: ['cyanBright'], }); })();}, 1000);
I've put our IIFE function inside oursetTimeout()
so that it gets executed only after the loader finishes. Our loader runs for2000
milliseconds or2
seconds.
Let's do a quick refactor. We have a callback in oursetTimeout()
which can be madeasync
. We can now remove theIIFE
function.
setTimeout(async () => { spinner.stop(); // Body of IIFE function goes here.}, 2000);
There's a lot lot more to do withora, check out theofficial docs to know more.
Lists
Remember theIF ELSEIF
we had setup for arguments? Let's add a--help
argument that lists out all the commands. Lists are a very important part of CLIs. I'm using a helpful packagelistr to handle these lists. Let's add the following code into our file in the place where we have ourIF
checks.
else if (argv.help) { const tasks = new Listr([ { title: '--init: Start the CLI', }, { title: '--name: Gives the name of the package', }, { title: '--version: Gives the version of the package', }, { title: '--help: Lists all available commands', }, ]); tasks .run() .then(() => { console.log('Done'); }) .catch((error) => { console.log(error); });}
We have created a new if check for--help
. Inside that, we passed an array of tasks into theListr
class. THis will basically list the following. This can be done byconsole.log()
but the reason I have usedlistr
is because in each object in the tasks array, we can also specify a task property with an arrow function. Check out thedocumentation to know more.
Executing Commands from Code
A lot of CLIs will want to access other CLIs and run commands such asgit init
ornpm install
etc. I'm using the packageexeca for the purpose of this tutorial. Begin by installing the module.
npm i execa
Then, require the module at the top of the file.
const execa = require('execa');
In theELSE
block for our arguments condition, add the following code. If you remember, we already had aconsole.log()
asking us to specify a command. Let's run our previous--help
command and list out the available commands.
else { console.log('Please specify a command'); const { stdout } = await execa('hello-world', ['--help']); console.log(stdout);}
We call theexeca()
function and pass in the name of the command. Then, in an array we pass in the arguments to be provided. We willawait
this and then destructure the output. Then we will log the output to simulate running the command.
Our CLI should finally look like this -
That's it guys, thank you for reading this post. I hope you guys liked it. If you found the post useful or liked it, please follow me to get notified about new posts. If you have questions, ask them in the comments and I'll try my best to answer them. As a bonus, I have made a list of some packages that can be used while developing CLIs.
Helpful Packages
These are a list of packages that might help you while developing CLIs. Keep in mind that this is by no means an exhaustive list. There are over 1 million packages on NPM and it is impossible to cover them all.
Inputs
Coloured Responses/Output
Loaders
Boxes Around Output
Top comments(4)

- Email
- LocationIndia
- Joined
Glad to know you liked it.

- LocationIsrael
- EducationSelf-taught
- WorkSenior front end engineer - LinearB
- Joined
That's a great introduction to CLIs. If anyone is looking for a more complete solution I strongly recommend checking oclif:
oclif.io/

- Email
- LocationIndia
- Joined
Thank you for sharing the resource!
For further actions, you may consider blocking this person and/orreporting abuse