DailyJS

Code Review: Search

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

language code-review

Code Review: Search

Posted by Alex R. Young on .
Featured

language code-review

Code Review: Search

Posted by Alex R. Young on .
*Code Review* is a series on DailyJS where I take a look at an open source project to see how it's built. Along the way we'll learn patterns and techniques by JavaScript masters. If you're looking for tips to write better apps, or just want to see how they're structured in established projects, then this is the tutorial series for you.

Search by TJ Holowaychuk is a small ack inspired utility for searching
source code. I picked this project out in particular because it
showcases TJ's nifty
Commander.js library and demonstrates how easy it is to build fast command line utilities with
Node.

Installation and Usage

Search can be installed with npm install -g search. This
clashes with my Sphinx binaries, so I
installed it in ~/ and set up an alias in my shell.

To use it, navigate to a directory with lots of code and run
search text, where text is a case insensitive
regular expression.

Structure

Like TJ's other projects, this is distributed with a README, Makefile,
history, and all of the code is in bin/search. I'd argue
against putting all of the code in bin/search so I could
require core modules elsewhere (potentially making testing
easier), but the binary itself could be tested as well.

Set Up

As I mentioned, this project uses Commander.js, which makes setting
everything up a breeze:

var program = require('commander')
  , path = require('path')
  , join = path.join
  , fs = require('fs');

// options

program
  .version('0.0.4')
  .usage('[options]  [path ...]')
  .option('-H, --hidden', 'search hidden files and directories')
  .parse(process.argv);

// no args

if (!program.args.length) {
  process.stdout.write(program.helpInformation());
  process.exit(0);
}

The chainable API allows everything that describes the program to be
described in a concise manner.

Next, the paths and query are processed. The regular expression is
instantiated once for performance reasons:

var query = program.args.shift()
  , paths = program.args
  , pending = paths.length
  , re = new RegExp('(' + query + ')', 'ig');

Searching

The main searching code is similar to code I've written a few times for
my Node apps, which is one of the reasons I picked this for a code
review. It uses Node's asynchronous file system APIs to recursively walk
over each path and their children. However, the slight twist here is TJ
uses Array.prototype.(forEach|filter|map) rather than
for loops. A lot of people use for loops over
iterators for performance reasons, reducing scoping complexity, or
browser support. It's worth considering this counter example in terms of
readability.

I've added some comments to explain how it works:

function search(path) {
  // Does this file exist?
  fs.stat(path, function(err, stat){
    if (err) throw err;

    // Is it a directory?
    if (stat.isDirectory()) {
      // If it's a directory, remove hidden files using the hidden() function (defined below),
      // then generate a list of file names with the current path, then run search() again on the resulting paths
      fs.readdir(path, function(err, files){
        if (err) throw err;
        files.filter(hidden).map(function(file){
          return join(path, file);
        }).forEach(search);
      });

The next part reads through each file and searches each line for the
regular expression. Output is printed directly with
console.log, and colour codes are inserted to make the
matches easier to spot.

    } else if (stat.isFile()) {
      var lines = [];
      fs.readFile(path, 'utf8', function(err, str){
        if (err) throw err;
        str.split('\n').forEach(function(line, i){
          if (!re.test(line)) return;
          lines.push([i, line]);
        });

        if (lines.length) {
          console.log('\n  \033[36m%s\033[0m', path);
          lines.forEach(function(line){
            var i = line[0]
              , line = line[1];
            line = line.replace(re, '\033[37;43m$1\033[0;90m');
            console.log('  \033[90m%d: %s\033[0m', i+1, line);
          });
        }
      });

Conclusion

TJ makes writing command line apps look easy. That's partly because it
actually is! The next time you're itching to solve an interesting
console-based problem, try scripting something with Node. There are lots
of projects similar to Search
that you can reference to get a head start.