Sponsored by

  • Intel
  • HP

JavaScriptTutorial

Build a command-line app with Node.js

JavaScript doesn’t just run in the browser. David White explores how to use Node.js to create a simple productivity application that you can run on the command line

This article first appeared in issue 235 of .net magazine – the world's best-selling magazine for web designers and developers.

Node.js (or simply Node) calls itself “a platform built on Chrome’s JavaScript runtime for easily building fast, scalable network applications”. This means it’s built on the V8 JavaScript engine incorporated into Chrome – Node itself is a series of C libraries that expose specific system-based functionality.

The application we’ll be building with Node is a simple version of the ‘rake notes’ task bundled with Ruby on Rails. We’ll want to run the application from the command line, passing in a directory as an option or leaving blank to use the current directory. The application will then search all folders and files in that directory for either a TODO, FIXME or OPTIMISE keyword where a developer may have left a comment. We’ll get the application to output the name and path of the file, along with any of the notes and the line number of the note.

Advertisement

To begin we’ll create a simple help command we can use in the application to show a user how to use it. This will also help us plan the functionality up front. Start by creating a new directory in your workspace called notes. Inside that, create another folder named bin and a file also called notes, with no extension inside the bin folder. We’ll add the following code to the notes file:

#!/usr/bin/env nodeconsole.log("Hello World!")

Before we can successfully run this, we need to set up some basic permissions and run the code below, which result in the ‘Hello World!’ being output in the command line:

chmod 755 bin/net-notes./bin/net-notes

Now we’ve got this working it’s time to implement something a little more useful. Remove the console.log statement and replace with the following:

var argv = process.argv.slice(2);var help =  "usage: net-notes [options] \n\n" +  "Searches files for TODO, FIXME or OPTIMISE tags within your project,\n" +  "displaying the line number and file name along with the tag description\n"+  "options:\n" +  "[directory_name] # Optional parameter, will run from current directory\n" +  "-h, [--help] # Show this help message and quit"var tasks = {};tasks.help = function(){  console.log(help);};if(argv[0] === "--help" || argv[0] === "-h") {  tasks.help();}

In this snippet we’re catching any arguments passed in and assigning them to a variable, argv. We are then creating a multiline string to hold our help statement and creating an empty object called tasks. We can subsequently create a function named help on tasks and output the help string.

The Node repository is a good place to stay up to date with the latest version of Node; using Git makes it easy to update at any point

Running the application now with the -h or --help option should output the help text directly in the command line. This type of functionality should be very familiar with any developer used to working within the command line, and it’s been created using just JavaScript.

Next up we want to create two methods, one for recursively traversing all directories within a specified or the current directory and a second for searching the files for TODO, FIXME or OPTIMISE statements. We’ll start with the latter.

checkFile = function(f){  // create a pattern to match on  var pattern = /(todo|fixme|optimise|optimize)\W*(.*$)/i;  var items = [];  var lineNumber = 1;  file = fs.readFileSync(f).toString().split('\n');  file.forEach(function (line) {    if(match = line.match(pattern)) {      items.push(" * [" + lineNumber.toString() + "] " + match[1].toUpperCase() + ": " + match[2]);    }    lineNumber++;  });  if(items.length > 0) {    // Output the file name    console.log(f);    // Output the matches    items.forEach(function(item) { console.log(item); });  }};

The checkFile method is fairly simple: we’re defining a simple Regex pattern for searching three keywords – four if we take into account an American spelling for ‘optimise’. (Unless you use Regex daily you may find yourself, like me, having to re-learn the intricacies of the system each time. Luckily there are now a few online tools for testing Regex commands quickly. ReFiddle seems popular, though from what I can tell it doesn’t give you a list of the different commands. Rubular, which follows Ruby’s Regex library, is simpler to use and the differences between Ruby and JS Regex are slight enough that it’s a usable app for most languages.)

We then create an array, read over each line of code to find all matches in that file and output the results using the familiar console.log statement.

When you’re stuck wondering if Node has a ‘method/module for that’ the Node docs are the best place to check

A key difference in JavaScript/Node programming is its asynchronous features. The following code collects all the notes within a specific file, only outputting them to the command line once it has them all. If the application was to output the lines each time it found one in a file we would end up with an inconsistent output, because the JavaScript would run itself over multiple files simultaneously. Now we know how to read a file and save string matches to an array we can create the search method attached to the tasks object. Paste the following code below the tasks definition file:

tasks.search = function (dir, action) {  // Assert that it's a function  if (typeof action !== "function")    action = function (error, file) { };  // Read the directory  fs.readdir(dir, function (err, list) {    // Return the error if something went wrong    if (err)      return action(err);      // For every file in the list      list.forEach(function (file) {        // Full path of that file        var path = dir + "/" + file;        // Get the file's stats        fs.stat(path, function (err, stat) {          //console.log(path + " is a file? " + stat.isFile());          // If the item is a directory          if (stat && stat.isDirectory()) {            tasks.search(path, action);          } else if (stat && stat.isFile()) {            checkFile(path);          } else {            action(null, path);          }        });      });    });  };

All we need to do now is catch any arguments passed in and decide whether the request needs to display a help message or is free to search a directory and files. We’ll amend the if statement at the bottom of the file and rewrite it to add some additional logic.

// If no arguments are passed, pass the current directoryif(typeof argv[0] === "undefined" || argv[0] === null) {  argv[0] = ".";}if(argv[0] === "--help" || argv[0] === "-h") {  tasks.help();} else {  tasks.search(argv[0]);}Testing

Now we have the application written, it’s time to take it for a spin. Ideally we would have been developing this with test-driven development using Jasmine or QUnit to write unit tests for the application, but that’s out of the scope of this tutorial so we’ll stick to testing manually. Inside your Node folder create a test folder, and inside there create the following file called extended_math.js.

var ExtendedMath;ExtendedMath = (function() {  function ExtendedMath() {}  // TODO: Allow number to be passed in to call square root on  ExtendedMath.prototype.root = function() {    return Math.sqrt;  };  // FIXME: Squaring currently cubes the result which is incorrect  ExtendedMath.prototype.square = function(x) {    return square(x) * x;  };  ExtendedMath.prototype.cube = function(x) {    return x * square(x);  };  //TODO: Add fix for JavaScript floating point calculations  return ExtendedMath;})();

All we’ve done here is create a simple object called ExtendedMath and add some basic math functionality to it. We’ve also added two TODOs and a FIXME statement for a broken method. If you run your application now on this directory with the following command:

./bin/net-notes test

… then you should see the following output:

test/extended_math.js  * [7] TODO: Allow number to be passed in to call square root on  * [12] FIXME: Squaring currently cubes the result which is incorrect  * [21] TODO: Add fix for JavaScript floating point calculations
Publishing your NPM

Node.js comes with a handy tool called NPM: similar to Gems within Ruby or PIP within Python it enables you to package applications or modules up into downloadable bundles. You’re also able to specify any application dependencies within your package.json file and can install them via an npm install command.

The NPM site is a great place to find new Node plug-ins as well as learn a little more about how Node packages work

To publish a Node package you’ll need to register yourself as a user. Do this via the command line; adding name, password and email address as prompted:

npm adduser  #=> Username: davidrhyswhite  #=> Password: *****  #=> Email: david@spry-soft.com  #=> npm http PUT https://registry.npmjs.org/-/user/org.couchdb.user:davidrhyswhite  #=> npm http 201 https://registry.npmjs.org/-/user/org.couchdb.user:davidrhyswhite

Next create a package.json file for your application. You’ll need to give it a name, description and version number; a repository is optional but recommended: other devs may want to check out the code before installing.

{  "name": "net-notes",  "description": "Command line todo application similar to Ruby on Rails 'rake notes', build for a .Net magazine tutorial.",  "version": "0.0.1",  "repository": {    "type": "git",    "url": "git://github.com/davidrhyswhite/net-notes.git"  },  "author": "David White <david@spry-soft.com>",  "bin": { "net-notes": "./bin/net-notes" },  "engines": {    "node": "*"  },  "dependencies" : {  }}

We can now publish the project to the NPM repository via this command:

npm publish

Once that’s finished we can run the following command with the -g flag to install the package globally, meaning we can run net-notes from any directory.

npm install -g net-notesUpdating the application

Updating a Node package is even more straightforward than publishing one. Once you’ve made the changes you require to your application, you will need to bump the version number inside the package.json file to a higher number and simply run the npm publish command to update the application.

The JavaScript Show provides a great podcast show with links to new and existing JavaScript projects


It’s always a good idea to follow the major, minor and patch versioning as specified in Semantic Versioning. As both NPM and Node follow this system it makes sense to help you avoid breaking users’ code if you make changes to your application/package that aren’t backwards compatible with previous versions and increase the wrong version number. An example would be if we had the following dependencies listed in our package.json file.

"dependencies" : {  "coffee-script" : "1.2.x",  "less" : ">= 0.1.8"}

If a user installed this they would get the CoffeeScript package version 1.2.x, where x is the highest number available on the server for version 1.2. If there was a version 1.3.0, that version wouldn’t be downloaded because we’ve specified the minor number and any changes to the patch number shouldn’t affect the code we’re running. With the less package we’ve specified a full version number; however, we’ve specified it with a greater than or equal to. This will get the latest version of this package, if there’s a version with a 1.0.0 it will download that over 0.1.8 as we’ve said we want versions greater than.

StackOverflow is a great place to find Node help; the community is prominent and always willing to assist
Conclusion

We’ve covered quite a bit of ground with this tutorial, from creating a command line application to distributing it and updating it with NPM, and briefly looked at how dependencies work. If you’re going to create your own NPM packages then ideally you’ll be testing them with both unit and integration/acceptance tests to make sure you’re confident about the stability of your package.

Thanks to Tom Hughes-Croucher for his peer review of this tutorial
 

Discover 45 top examples of JavaScript at our sister site, Creative Bloq.

Log in to Creative Bloq with your preferred social network to comment

OR

Log in with your Creative Bloq account

site stat collection