How to Create a Node.js CLI Application

One of my absolute favorite things about Node is how easy it is to create simple command line interface (CLI) tools. Between argument parsing with yargs to managing tools with npm, Node just makes it easy.

Some examples of the kinds of tools I'm referring to are:

When installed (with the -g option), these packages can be executed from anywhere on the command line and work much like the built-in Unix tools.

I've been creating a few Node.js applications for the command line lately and thought it might be helpful to write up a post on it to help you get started. So throughout this article I'll be showing you how to create a command line tool to get location data for IP addresses and URLs.

If you've seen the Stack Abuse article on learning Node.js, you may recall we created a package called twenty that had similar functionality. We'll be building off of that project and turning it in to a proper CLI tool with more functionality.

Setting up the project

Let's start by creating a new directory and setting up the project using npm:

$ mkdir twenty
$ npm init

Press enter for all of the prompts in the last command, and you should have your package.json file.

Note that since I've already taken the package name twenty on npm, you'll have to rename it to something else if you actually want to publish. Or you could also scope your project.

Then, create the index.js file:

$ touch index.js

This is all we really need to get started for now, and we'll be adding to the project as we move on.

Parsing arguments

Most CLI apps take in arguments from the user, which is the most common way of getting input. For most cases, parsing the arguments isn't too difficult since there are usually only a handful of commands and flags. But as the tool becomes more complex, more flags and commands will be added, and argument parsing can become surprisingly difficult.

To help us with this, we'll be using a package called yargs, which is the successor to the popular optimist package.

yargs was created to help you parse commands from the user, like this:

var argv = require('yargs').argv;  

Now complex optstrings like node index.js install -v --a=22 -cde -x derp can be easily accessed:

var argv = require('yargs').argv;

argv._[0]   // 'install'  
argv.v      // true  
argv.a      // 22  
argv.c      // true  
argv.d      // true  
argv.e      // true  
argv.x      // 'derp'  

yargs will even help you with specifying the command interface, so if the user's input doesn't meet certain requirements it'll show them an error message. So, for example, we can tell yargs we want at least 2 arguments:

var argv = require('yargs')  
    .demand(2)
    .argv

And if the user doesn't provide at least two, they'll see this default error message:

$ node index.js foo

Not enough non-option arguments: got 1, need at least 2  

There is a lot more to yargs than just this, so check out the readme for more info.

For twenty, we'll be taking in a few optional arguments, like an IP address and some flags. For now, we'll be using yargs like this:

var argv = require('yargs')  
    .alias('d', 'distance')
    .alias('j', 'json')
    .alias('i', 'info')
    .usage('Usage: $0 [options]')
    .example('$0 -d 8.8.8.8', 'find the distance (km) between you and Google DNS')
    .describe('d', 'Get distance between IP addresses')
    .describe('j', 'Print location data as JSON')
    .describe('i', 'Print location data in human readable form')
    .help('h')
    .alias('h', 'help')
    .argv;

Since none of our arguments are required, we wont' be using .demand(), but we do use .alias(), which tells yargs that the user can use the short or long form of each flag. We've also added some help documentation to show the user for when they need it.

Structuring the application

Now that we can get input from the user, how do we take that input and translate it to a command with the optional arguments? There are a few modules out there designed to help you do this, including:

With many of these frameworks the argument parsing is actually done for you, so you don't even need to use yargs. And in commander's case, most of its functionality is a lot like yargs, although it does provide ways to route commands to functions.

Since our application is fairly simple we'll just stick with using yargs for now.

Adding the code

We won't spend too much time here since it is specific to just our CLI app, but here is the code specific to our application:

var dns = require('dns');  
var request = require('request');

var ipRegex = /(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/;

var toRad = function(num) {  
    return num * (Math.PI / 180);
};

var getIpInfo = function(server, callback) {  
    var ipinfo = function(p, cb) {
        request('http://ipinfo.io/' + p, function(err, response, body) {
            var json = JSON.parse(body);
            cb(err, json);
        });
    };

    if (!server) {
        return ipinfo('json', callback);
    } else if (!server.match(ipRegex)) {
        return dns.lookup(server, function(err, data) {
            ipinfo(data, callback);
        });
    } else {
        return ipinfo(server, callback);
    }
};

var ipDistance = function(lat1, lon1, lat2, lon2) {  
    // Earth radius in km
    var r = 6371;

    var dLat = toRad(lat2 - lat1);
    var dLon = toRad(lon2 - lon1);
    lat1 = toRad(lat1);
    lat2 = toRad(lat2);

    var a = Math.sin(dLat / 2.0) * Math.sin(dLat / 2.0) + 
        Math.sin(dLon / 2.0) * Math.sin(dLon / 2.0) * Math.cos(lat1) * Math.cos(lat2);
    var c = 2.0 * Math.atan2(Math.sqrt(a), Math.sqrt(1.0 - a));
    return r * c;
};

var findLocation = function(server, callback) {  
    getIpInfo(server, function(err, data) {
        callback(null, data.city + ', ' + data.region);
    });
};

var findDistance = function(ip1, ip2, callback) {  
    var lat1, lon1, lat2, lon2;

    getIpInfo(ip1, function(err, data1) {
        var coords1 = data1.loc.split(',');
        lat1 = Number(coords1[0]);
        lon1 =  Number(coords1[1]);
        getIpInfo(ip2, function(err, data2) {
            var coords2 = data2.loc.split(',');
            lat2 =  Number(coords2[0]);
            lon2 =  Number(coords2[1]);

            var dist = ipDistance(lat1, lon1, lat2, lon2);
            callback(null, dist);
        });
    });
};

For the full source code, you can find the repository here.

The only thing left we have to do with the code is hook up the CLI arguments with the application code above. To make it easy, we'll put all of this in a function called cli(), which we'll use later.

Encapsulating the argument parsing and command mapping within cli() helps keep the application code separate, thus allowing this code to be imported as a library with require().

var cli = function() {  
    var argv = require('yargs')
        .alias('d', 'distance')
        .alias('j', 'json')
        .alias('i', 'info')
        .usage('Usage: $0 [IP | URL] [--d=IP | URL] [-ij]')
        .example('$0 -d 8.8.8.8', 'find the distance (km) between you and Google DNS')
        .describe('d', 'Get distance between IP addresses')
        .describe('j', 'Print location data as JSON')
        .describe('i', 'Print location data in human readable form')
        .help('h')
        .alias('h', 'help')
        .argv;

    var path = 'json';
    if (argv._[0]) {
        path = argv._[0];
    }

    if (argv.d) {
        findDistance(path, argv.d, function(err, distance) {
            console.log(distance);
        });
    } else if (argv.j) {
        getIpInfo(path, function(err, data) {
            console.log(JSON.stringify(data, null, 4));
        });
    } else if (argv.i) {
        getIpInfo(path, function(err, data) {
            console.log('IP:', data.ip);
            console.log('Hostname:', data.hostname);
            console.log('City:', data.city);
            console.log('Region:', data.region);
            console.log('Postal:', data.postal);
            console.log('Country:', data.country);
            console.log('Coordinates:', data.loc);
            console.log('ISP:', data.org);
        });
    } else {
        findLocation(path, function(err, location) {
            console.log(location);
        });
    }
};

exports.info = getIpInfo;  
exports.location = findLocation;  
exports.distance = findDistance;  
exports.cli = cli;  

Here you can see we basically just use if...else statements to determine which command to run. You could get a lot fancier and use Flatiron to map regex strings to commands, but that's a bit of overkill for what we're doing here.

Making it executable

In order for us to be able to execute the app, we need to specify a few things in our package.json file, like where the executable resides. But first, let's create the executable and its code. Create a file called twenty in the directory twenty/bin/ and add this to it:

#!/usr/bin/env node
require('../index').cli();  

The shebang (#!/usr/bin/env node) tells Unix how to execute the file, allowing us to leave out the node prefix. The second line just loads the code from above and calls the cli() function.

In package.json, add the following JSON:

"bin": {
    "twenty": "./bin/twenty"
}

This just tells npm where to find the executable when installing the package with the -g (global) flag.

So now, if you install twenty as a global...

$ npm install -g twenty

...you can then get the locations of servers and IP addresses:

$ twenty 198.41.209.141 #reddit
San Francisco, California

$ twenty rackspace.com
San Antonio, Texas

$ twenty usa.gov --j
{
    "ip": "216.128.241.47",
    "hostname": "No Hostname",
    "city": "Phoenix",
    "region": "Arizona",
    "country": "US",
    "loc": "33.3413,-112.0598",
    "org": "AS40289 CGI TECHNOLOGIES AND SOLUTIONS INC.",
    "postal": "85044"
}

$ twenty stackabuse.com
Ashburn, Virginia  

And there you have it, the Stack Abuse server is located in Asburn, Virginia. Interesting =)

For the full source code, check out the project on Github.