How To Develop An Interactive Command Line Application Using Node.js

How To Develop An Interactive Command Line Application Using Node.js

Over the last five years, Node.js1 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 Gulp2, Angular CLI3, Cordova4, Yeoman5 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: Link

In this tutorial, we will develop a command line application that accepts a CSV file of customer information, and using the SendGrid API10, we will send emails to them. Here are the contents of this tutorial:

  1. “Hello, World”11
  2. Handling command line arguments12
  3. Runtime user inputs13
  4. Asynchronous network communication14
  5. Decorating the CLI output15
  6. Making it a shell command16
  7. Beyond JavaScript17

“Hello, World”

This tutorial assumes you have installed Node.js on your system. In case you have not, please install it18. Node.js also comes with a package manager named npm19. Using npm, you can install many open-source packages. You can get the complete list on npm’s official website20. 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.jsonin npm’s documentation21.

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 dependencies22 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 Async3923, Chalk4524, Commander3025, CSV3126, Inquirer.js27 and SendGrid3628. 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.argv29 to read them. However, parsing their values and options is a cumbersome task. So, instead of reinventing the wheel, we will use the Commander3025 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 CSV3126 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 Email
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 System32 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.js33 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 prompt34, 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 documentation35. 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 SendGrid3628 module, we need to get an API key. You can generate this API key from SendGrid’s dashboard37 (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.env38.

In the code above, we are sending asynchronous email using SendGrid’s API and the Async3923 module. The Async module is one of the most powerful Node.js modules. Handling asynchronous callbacks often leads to callback hell40. 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 each41, series42, map43 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 documentation44. 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 Chalk4524 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 shebang46 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 bin47, 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 tools48 will have been a part of your development process. From deployment scripts to cron jobs49 to backups, you could automate anything using Bash scripts. In fact, before Docker50, Chef51 and Puppet52 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 Process53 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 me54.

(rb, al, il)

Footnotes Link

  1. 1 https://nodejs.org/
  2. 2 http://gulpjs.com
  3. 3 https://cli.angular.io
  4. 4 https://cordova.apache.org
  5. 5 http://yeoman.io
  6. 6 https://www.smashingmagazine.com/2017/02/a-detailed-introduction-to-webpack/
  7. 7 https://www.smashingmagazine.com/2014/05/detailed-introduction-nodejs-mongodb/
  8. 8 https://www.smashingmagazine.com/2016/03/server-side-rendering-react-node-express/
  9. 9 https://www.smashingmagazine.com/2011/09/useful-node-js-tools-tutorials-and-resources/
  10. 10 https://github.com/sendgrid/sendgrid-nodejs
  11. 11 #hello-world
  12. 12 #cli-arguments
  13. 13 #runtime-inputs
  14. 14 #async-communication
  15. 15 #chalk
  16. 16 #shell-command
  17. 17 #beyond-js
  18. 18 https://nodejs.org/
  19. 19 https://www.npmjs.com
  20. 20 https://www.npmjs.com
  21. 21 https://docs.npmjs.com/files/package.json
  22. 22 https://docs.npmjs.com/files/package.json#dependencies
  23. 23 http://caolan.github.io/async/docs.html
  24. 24 https://github.com/chalk/chalk
  25. 25 https://www.npmjs.com/package/commander
  26. 26 https://www.npmjs.com/package/csv
  27. 27 https://github.com/SBoudrias/Inquirer.js/
  28. 28 https://github.com/sendgrid/sendgrid-nodejs
  29. 29 https://nodejs.org/docs/latest/api/process.html
  30. 30 https://www.npmjs.com/package/commander
  31. 31 https://www.npmjs.com/package/csv
  32. 32 https://nodejs.org/api/fs.html
  33. 33 https://github.com/SBoudrias/Inquirer.js
  34. 34 https://github.com/SBoudrias/Inquirer.js#methods
  35. 35 https://github.com/SBoudrias/Inquirer.js/
  36. 36 https://github.com/sendgrid/sendgrid-nodejs
  37. 37 https://app.sendgrid.com/settings/api_keys
  38. 38 https://nodejs.org/api/process.html#process_process_env
  39. 39 http://caolan.github.io/async/docs.html
  40. 40 http://stackoverflow.com/a/25098230/757449
  41. 41 http://caolan.github.io/async/docs.html#each
  42. 42 http://caolan.github.io/async/docs.html#series
  43. 43 http://caolan.github.io/async/docs.html#map
  44. 44 https://sendgrid.com/docs/API_Reference/Web_API_v3/index.html
  45. 45 https://github.com/chalk/chalk
  46. 46 http://unix.stackexchange.com/a/87600
  47. 47 https://docs.npmjs.com/files/package.json#bin
  48. 48 https://en.wikipedia.org/wiki/Bash_(Unix_shell)
  49. 49 https://en.wikipedia.org/wiki/Cron
  50. 50 https://www.docker.com
  51. 51 https://www.chef.io
  52. 52 https://puppet.com
  53. 53 https://nodejs.org/api/child_process.html
  54. 54 https://twitter.com/nihar_sawant

↑ Back to topTweet itShare on Facebook

Leave a Reply

Your email address will not be published. Required fields are marked *