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.
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.
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.
Get the Creative Bloq Newsletter
Daily design news, reviews, how-tos and more, as picked by the editors.
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.
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.
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-notes
Updating 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.
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.
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.
Thank you for reading 5 articles this month* Join now for unlimited access
Enjoy your first month for just £1 / $1 / €1
*Read 5 free articles per month without a subscription
Join now for unlimited access
Try first month for just £1 / $1 / €1
The Creative Bloq team is made up of a group of design fans, and has changed and evolved since Creative Bloq began back in 2012. The current website team consists of eight full-time members of staff: Editor Georgia Coggan, Deputy Editor Rosie Hilder, Ecommerce Editor Beren Neale, Senior News Editor Daniel Piper, Editor, Digital Art and 3D Ian Dean, Tech Reviews Editor Erlingur Einarsson and Ecommerce Writer Beth Nicholls and Staff Writer Natalie Fear, as well as a roster of freelancers from around the world. The 3D World and ImagineFX magazine teams also pitch in, ensuring that content from 3D World and ImagineFX is represented on Creative Bloq.