Over the last five years, Node.js has helped to bring uniformity to software development. You can do anything in Node.js, whether it be front-end development, server-side scripting, cross-platform desktop applications, cross-platform mobile applications, Internet of Things, you name it. Writing command line tools has also become easier than ever before because of Node.js — not just any command line tools, but tools that are interactive, useful and less time-consuming to develop.
If you are a front-end developer, then you must have heard of or worked on Gulp, Angular CLI, Cordova, Yeoman and others. Have you ever wondered how they work? For example, in the case of Angular CLI, by running a command like ng new <project-name>
, you end up creating an Angular project with basic configuration. Tools such as Yeoman ask for runtime inputs that eventually help you to customize a project’s configuration as well. Some generators in Yeoman help you to deploy a project in your production environment. That is exactly what we are going to learn today.
Further Reading on SmashingMag:
- A Detailed Introduction To Webpack
- An Introduction To Node.js And MongoDB
- Server-Side Rendering With React, Node And Express
- Useful Node.js Tools, Tutorials And Resources
In this tutorial, we will develop a command line application that accepts a CSV file of customer information, and using the SendGrid API, we will send emails to them. Here are the contents of this tutorial:
- “Hello, World”
- Handling command line arguments
- Runtime user inputs
- Asynchronous network communication
- Decorating the CLI output
- Making it a shell command
- Beyond JavaScript
“Hello, World”
This tutorial assumes you have installed Node.js on your system. In case you have not, please install it. Node.js also comes with a package manager named npm. Using npm, you can install many open-source packages. You can get the complete list on npm’s official website. For this project, we will be using many open-source modules (more on that later). Now, let’s create a Node.js project using npm.
$ npm init
name: broadcast
version: 0.0.1
description: CLI utility to broadcast emails
entry point: broadcast.js
I have created a directory named broadcast
, inside of which I have run the npm init
command. As you can see, I have provided basic information about the project, such as name, description, version and entry point. The entry point is the main JavaScript file from where the execution of the script will start. By default, Node.js assigns index.js
as the entry point; however, in this case, we are changing it to broadcast.js
. When you run the npm init
command, you will get a few more options, such as the Git repository, license and author. You can either provide values or leave them blank.
Upon successful execution of the npm init
, you will find that a package.json
file has been created in the same directory. This is our configuration file. At the moment, it holds the information that we provided while creating the project. You can explore more about package.json
in npm’s documentation.
Now that our project is set up, let’s create a “Hello world” program. To start, create a broadcast.js
file in your project, which will be your main file, with the following snippet:
console.log('hello world');
Now, let’s run this code.
$ node broadcast
hello world
As you can see, “hello word” is printed to the console. You can run the script with either node broadcast.js
or node broadcast
; Node.js is smart enough to understand the difference.
According to package.json
’s documentation, there is an option named dependencies
in which you can mention all of the third-party modules that you plan to use in the project, along with their version numbers. As mentioned, we will be using many third-party open-source modules to develop this tool. In our case, package.json
looks like this:
{
"name": "broadcast",
"version": "0.0.1",
"description": "CLI utility to broadcast emails",
"main": "broadcast.js",
"license": "MIT",
"dependencies": {
"async": "^2.1.4",
"chalk": "^1.1.3",
"commander": "^2.9.0",
"csv": "^1.1.0",
"inquirer": "^2.0.0",
"sendgrid": "^4.7.1"
}
}
As you must have noticed, we will be using Async, Chalk, Commander, CSV, Inquirer.js and SendGrid. As we progress ahead with the tutorial, usage of these modules will be explained in detail.
Handling Command Line Arguments
Reading command line arguments is not difficult. You can simply use process.argv
to read them. However, parsing their values and options is a cumbersome task. So, instead of reinventing the wheel, we will use the Commander module. Commander is an open-source Node.js module that helps you write interactive command line tools. It comes with very interesting features for parsing command line options, and it has Git-like subcommands, but the thing I like best about Commander is the automatic generation of help screens. You don’t have to write extra lines of code — just parse the –help
or -h
option. As you start defining various command line options, the –help
screen will get populated automatically. Let’s dive in:
$ npm install commander --save
This will install the Commander module in your Node.js project. Running the npm install with –save
option will automatically include Commander in the project’s dependencies, defined in package.json
. In our case, all of the dependencies have already been mentioned; hence, there is no need to run this command.
var program = require('commander');
program
.version('0.0.1')
.option('-l, --list [list]', 'list of customers in CSV file')
.parse(process.argv)
console.log(program.list);
As you can see, handling command line arguments is straightforward. We have defined a –list
option. Now, whatever values we provide followed by the –list
option will get stored in a variable wrapped in brackets — in this case, list
. You can access it from the program
variable, which is an instance of Commander. At the moment, this program only accepts a file path for the –list
option and prints it in the console.
$ node broadcast --list input/employees.csv
input/employees.csv
You must have noticed also a chained method that we have invoked, named version
. Whenever we run the command providing –version
or -V
as the option, whatever value is passed in this method will get printed.
$ node broadcast --version
0.0.1
Similarly, when you run the command with the –help
option, it will print all of the options and subcommands defined by you. In this case, it will look like this:
$ node broadcast --help
Usage: broadcast [options]
Options:
-h, --help output usage information
-V, --version output the version number
-l, --list <list> list of customers in CSV file
Now that we are accepting file paths from command line arguments, we can start reading the CSV file using the CSV module. The CSV module is an all-in-one-solution for handling CSV files. From creating a CSV file to parsing it, you can achieve anything with this module.
Because we plan to send emails using the SendGrid API, we are using the following document as a sample CSV file. Using the CSV module, we will read the data and display the name and email address provided in the respective rows.
First name | Last name | |
---|---|---|
Dwight | Schrute | dwight.schrute@dundermifflin.com |
Jim | Halpert | jim.halpert@dundermifflin.com |
Pam | Beesly | pam.beesly@dundermifflin.com |
Ryan | Howard | ryan.howard@dundermifflin.com |
Stanley | Hudson | stanley.hudson@dundermifflin.com |
Now, let’s write a program to read this CSV file and print the data to the console.
const program = require('commander');
const csv = require('csv');
const fs = require('fs');
program
.version('0.0.1')
.option('-l, --list [list]', 'List of customers in CSV')
.parse(process.argv)
let parse = csv.parse;
let stream = fs.createReadStream(program.list)
.pipe(parse({ delimiter : ',' }));
stream
.on('data', function (data) {
let firstname = data[0];
let lastname = data[1];
let email = data[2];
console.log(firstname, lastname, email);
});
Using the native File System module, we are reading the file provided via command line arguments. The File System module comes with predefined events, one of which is data
, which is fired when a chunk of data is being read. The parse
method from the CSV module splits the CSV file into individual rows and fires multiple data events. Every data event sends an array of column data. Thus, in this case, it prints the data in the following format:
$ node broadcast --list input/employees.csv
Dwight Schrute dwight.schrute@dundermifflin.com
Jim Halpert jim.halpert@dundermifflin.com
Pam Beesly pam.beesly@dundermifflin.com
Ryan Howard ryan.howard@dundermifflin.com
Stanley Hudson stanley.hudson@dundermifflin.com
Runtime User Inputs
Now we know how to accept command line arguments and how to parse them. But what if we want to accept input during runtime? A module named Inquirer.js enables us to accept various types of input, from plain text to passwords to a multi-selection checklist.
For this demo, we will accept the sender’s email address and name via runtime inputs.
…
let questions = [
{
type : "input",
name : "sender.email",
message : "Sender's email address - "
},
{
type : "input",
name : "sender.name",
message : "Sender's name - "
},
{
type : "input",
name : "subject",
message : "Subject - "
}
];
let contactList = [];
let parse = csv.parse;
let stream = fs.createReadStream(program.list)
.pipe(parse({ delimiter : "," }));
stream
.on("error", function (err) {
return console.error(err.message);
})
.on("data", function (data) {
let name = data[0] + " " + data[1];
let email = data[2];
contactList.push({ name : name, email : email });
})
.on("end", function () {
inquirer.prompt(questions).then(function (answers) {
console.log(answers);
});
});
First, you’ll notice in the example above that we’ve created an array named contactList
, which we’re using to store the data from the CSV file.
Inquirer.js comes with a method named prompt
, which accepts an array of questions that we want to ask during runtime. In this case, we want to know the sender’s name and email address and the subject of their email. We have created an array named questions
in which we are storing all of these questions. This array accepts objects with properties such as type
, which could be anything from an input to a password to a raw list. You can see the list of all available types in the official documentation. Here, name
holds the name of the key against which user input will be stored. The prompt
method returns a promise object that eventually invokes a chain of success and failure callbacks, which are executed when the user has answered all of the questions. The user’s response can be accessed via the answers
variable, which is sent as a parameter to the then
callback. Here is what happens when you execute the code:
$ node broadcast -l input/employees.csv
? Sender's email address - michael.scott@dundermifflin.com
? Sender's name - Micheal Scott
? Subject - Greetings from Dunder Mifflin
{ sender:
{ email: 'michael.scott@dundermifflin.com',
name: 'Michael Scott' },
subject: 'Greetings from Dunder Mifflin' }
Asynchronous Network Communication
Now that we can read the recipient’s data from the CSV file and accept the sender’s details via the command line prompt, it is time to send the emails. We will be using SendGrid’s API to send email.
…
let __sendEmail = function (to, from, subject, callback) {
let template = "Wishing you a Merry Christmas and a " +
"prosperous year ahead. P.S. Toby, I hate you.";
let helper = require('sendgrid').mail;
let fromEmail = new helper.Email(from.email, from.name);
let toEmail = new helper.Email(to.email, to.name);
let body = new helper.Content("text/plain", template);
let mail = new helper.Mail(fromEmail, subject, toEmail, body);
let sg = require('sendgrid')(process.env.SENDGRID_API_KEY);
let request = sg.emptyRequest({
method: 'POST',
path: '/v3/mail/send',
body: mail.toJSON(),
});
sg.API(request, function(error, response) {
if (error) { return callback(error); }
callback();
});
};
stream
.on("error", function (err) {
return console.error(err.response);
})
.on("data", function (data) {
let name = data[0] + " " + data[1];
let email = data[2];
contactList.push({ name : name, email : email });
})
.on("end", function () {
inquirer.prompt(questions).then(function (ans) {
async.each(contactList, function (recipient, fn) {
__sendEmail(recipient, ans.sender, ans.subject, fn);
});
});
});
In order to start using the SendGrid module, we need to get an API key. You can generate this API key from SendGrid’s dashboard (you’ll need to create an account). Once the API key is generated, we will store this key in environment variables against a key named SENDGRID_API_KEY
. You can access environment variables in Node.js using process.env
.
In the code above, we are sending asynchronous email using SendGrid’s API and the Async module. The Async module is one of the most powerful Node.js modules. Handling asynchronous callbacks often leads to callback hell. There comes a point when there are so many asynchronous calls that you end up writing callbacks within a callback, and often there is no end to it. Handling errors gets even more complicated for a JavaScript ninja. The Async module helps you to overcome callback hell, providing handy methods such as each
, series
, map
and many more. These methods help us write code that is more manageable and that, in turn, appears like synchronous behavior.
In this example, rather than sending a synchronous request to SendGrid, we are sending an asynchronous request in order to send an email. Based on the response, we’ll send subsequent requests. Using each method in the Async module, we are iterating over the contactList
array and calling a function named sendEmail
. This function accepts the recipient’s details, the sender’s details, the subject line and the callback for the asynchronous call. sendEmail
sends emails using SendGrid’s API; you can explore more about the SendGrid module in the official documentation. Once an email is successfully sent, an asynchronous callback is invoked, which passes the next object from the contactList
array.
That’s it! Using Node.js, we have created a command line application that accepts CSV input and sends email.
Decorating The Output
Now that our application is ready to send emails, let’s see how can we decorate the output, such as errors and success messages. To do so, we’ll use the Chalk module, which is used to style command line inputs.
…
stream
.on("error", function (err) {
return console.error(err.response);
})
.on("data", function (data) {
let name = data[0] + " " + data[1];
let email = data[2];
contactList.push({ name : name, email : email });
})
.on("end", function () {
inquirer.prompt(questions).then(function (ans) {
async.each(contactList, function (recipient, fn) {
__sendEmail(recipient, ans.sender, ans.subject, fn);
}, function (err) {
if (err) {
return console.error(chalk.red(err.message));
}
console.log(chalk.green('Success'));
});
});
});
In the snippet above, we have added a callback function while sending emails, and that function is called when the asynchronous each
loop is either completed or broken due to runtime error. Whenever a loop is not completed, it sends an error
object, which we print to the console in red. Otherwise, we print a success message in green.
If you go through Chalk’s documentation, you will find many options to style this input, including a range of console colors (magenta, yellow, blue, etc.) underlining and bolded text.
Making It A Shell Command
Now that our tool is complete, it is time to make it executable like a regular shell command. First, let’s add a shebang at the top of broadcast.js
, which will tell the shell how to execute this script.
#!/usr/bin/env node
const program = require("commander");
const inquirer = require("inquirer");
…
Now, let’s configure the package.json
to make it executable.
…
"description": "CLI utility to broadcast emails",
"main": "broadcast.js",
"bin" : {
"broadcast" : "./broadcast.js"
}
…
We have added a new property named bin
, in which we have provided the name of the command from which broadcast.js
will be executed.
Now for the final step. Let’s install this script at the global level so that we can start executing it like a regular shell command.
$ npm install -g
Before executing this command, make sure you are in the same project directory. Once the installation is complete, you can test the command.
$ broadcast --help
This should print all of the available options that we get after executing node broadcast –help
. Now you are ready to present your utility to the world.
One thing to keep in mind: During development, any change you make in the project will not be visible if you simply execute the broadcast
command with the given options. If you run which broadcast
, you will realize that the path of broadcast
is not the same as the project path in which you are working. To prevent this, simply run npm link
in your project folder. This will automatically establish a symbolic link between the executable command and the project directory. Henceforth, whatever changes you make in the project directory will be reflected in the broadcast command as well.
Beyond JavaScript
The scope of the implementation of these kinds of CLI tools goes well beyond JavaScript projects. If you have some experience with software development and IT, then Bash tools will have been a part of your development process. From deployment scripts to cron jobs to backups, you could automate anything using Bash scripts. In fact, before Docker, Chef and Puppet became the de facto standards for infrastructure management, Bash was the savior. However, Bash scripts always had some issues. They do not easily fit in a development workflow. Usually, we use anything from Python to Java to JavaScript; Bash has rarely been a part of core development. Even writing a simple conditional statement in Bash requires going through endless documentation and debugging.
However, with JavaScript, this whole process becomes simpler and more efficient. All of the tools automatically become cross-platform. If you want to run a native shell command such as git
, mongodb
or heroku
, you could do that easily with the Child Process module in Node.js. This enables you to write software tools with the simplicity of JavaScript.
I hope this tutorial has been helpful to you. If you have any questions, please drop them in the comments section below or tweet me.