Production Ready Node: Command Line Tooling

c

ommand Line tools are an important component to development, administration and maintenance of most all software projects - large or small. A good command line tool greatly speeds up repetitive operations through simple and flexible abstractions.

The Node.js community has a number of tools out there for quickly building out a command line interfaces, commander being one of the more popular. However, most all of them suffer from the same set of problems, making them difficult to integrate into applications in a clean an re-usable manner. Primarily speaking, there is no way to separate general functionality, from a command, from the argument parsing, or a command from the command line tool itself. This makes building for code re-use exceptionally difficult.

Separation of Concerns

When building a command line interface for your applications, you want to keep commands separate from the primary tool, and general functionality separate from each of the commands. As a rule of thumb, your commands should not do anything that can not be done from within the application itself. Ideally, you want the command line tool to be a small, simple program that knows how to find, register and run your defined commands. To accomplish this, we are going to being using the Seeli framework.

npm install --save seeli  

Primary Tool

We can use npm to define the name of our command line tool, or binary script through the bin property in the package.json of our project:

{
  "version":"0.0.0",
    "name":"project",
    "bin":{
      "fancyname":"./bin/cli.js"
    }
}

When our package is installed globally ( npm install -g ), or dynamically linked ( npm link ), this will add our program cli.js to our systems execution path as fancyname. You can call it whatever you like however.

Next, we just need to define command line tool. All our tool will need to do is load and register commands. Following the project structure we have previously established, we can assume all commands will be located in a commands folder of packages under our primary internal package directory.

// project/bin/cli.js

var cli            = require( 'seeli' )  
   , child_process = require( 'child_process' )
   , fs            = require('fs')
   , path          = require('path')
   , clone         = require('mout/lang/clone')
   , packagepath   = path.normalize( path.resolve(__dirname,'..','packages') )
   , jsregex       = /\.js$/
   , files
   ;

debug('current dir', __dirname);  
debug('package path: %s', packagepath);


fs  
  .readdirSync( packagepath )
  .forEach( function( file ){
    var searchpath = path.join( packagepath, file, 'commands' )

    if( fs.existsSync( searchpath ) && fs.statSync( searchpath ).isDirectory() ){
      fs.readdirSync(searchpath).forEach( function(module){
        if( jsregex.test( module ) ){
          var requirepath = path.join( searchpath, module )
          var cmd = require(requirepath)

          // name will be the name defined on the command
          // OR the name of the file if not specified
          var name = ((cmd.options.name ? cmd.options.name : module)).replace(jsregex,'').toLowerCase().replace('-', '_');
          try{
            // Register the command by name
            cli.use( name, cmd)
          } catch( e ){
            console.error('unable to register module %s', module )
          }
        }
      } );
    }
  });

cli.run()  

Believe it or not, this is the entirety of our command like tool. Here is what it is doing

  1. Find all directories in the internal packages directory
  2. Locate javascript files in the commands directory of each package
  3. calls require on the normalized path
  4. Register the module ( assumed to be a command ) with Seeli
  5. Runs Seeli

Simple.

Commands

Commands are simply javascript file that export a command instance as the module. To illustrate this, we can start with the obligatory hello world command. This command will be called hello and it will simply say hello to a name, which will be passed via a name flag.

.
|-- project
|   |-- packages
|   |   |-- project-foo
|   |   |   |-- package.json
|   |   |   |--commands
|   |   |       `-- hello.js
|   |   `-- README.md
|   `-- README.md
`-- README.md
// project/packages/project-foo/commands/hello.js

var cli = require( 'seeli' );  
var Hello;

Hello = new cli.Command({  
    description:"Says hello"
  , usage:[
      "hello --name=Bob",
      "hello -n Bob"
    ]
  , flags:{
      name:{
          type:String
          ,  shorthand:'n'
          ,  required:true
        }
    }
    ,run: function( directive, data, done ){
      done(null/* error */, 'hello, ' + data.name /* data*/ )
    }
});

module.exports = Hello;  

It is pretty easy to understand what this command does. Everything we've defined is effectively optional, with the exception of the run function. The run function what the command actually does and needs to call the done function if and when it is done. The done is a node style callback that accepts an error, if there is one, and an optional string which will be written to stdout.

$ fancyname hello --name=Bob 
$ hello, bob
$fancyname hello -n Sam
$ hello, Sam

Functionality

We can take this one step further and create a command that is a little more involved. We'll make a command that displays all of the version information of our internal packages. Keeping our rule of thumb about commands, we'll encapsulate the primary functionality of the command in a separate module.

// lib/loading/base.js

// locates and loads files based on a pattern.
function Loader( options ){

  options = options || {};
  // cut
}

// returns an object of file paths that we are looking for
Loader.prototype.find = function find(){  
  var searchpath = path.join( PROJECT_PATH, 'packages', searchpath' );

  // cut

    return {pkgnameA:[filepath, filepath], pkgnameB:[filepath, filepath]};
};

// returns the js modules denoted by files paths from find() 
Loader.prototype.load = function load(){  
  var packages = this.find.apply(this, arguments)

    // cut

  return {pkgnameA:[pkg,pkg], pkgnameB:[pkg,pkg]};
};

The implementation of the loader is not important here, but our little loader class has two functions, find and load. Find, as the name would imply locates a kind of file in our packages returning absolute paths to the files. In this situation it is looking for packages.json files. The load method, calls the find method and runs require over the results so we end up with an array containing each of the package manifest modules that we have found. We can use the information in this to find version information for use in our command.

Our module does most of the work, so our command really just needs to format the data, and print it out.

// packages/project-foo/commands/version
var cli = require( 'seeli' );  
var loader = new require('project-foo/lib/loading/base');  
var util = require( 'util' );

module.exports = new cli.Command({  
  description:"Display package version information"
    ,usage:[
      "fancyname version"
    ]
    ,run: function( directive, data, done ){
    var pkgs = loader.load();
        var out = [];
        var current;

        for( var pkg in pkgs ){
          current = pkgs[pkg];

            current.forEach(function( p ){
                out.push( 
                    util.format(
                        "%s@%s"
                        , current.name
                        , current.version
                   ) 
               );
            })
        }
        done(null, out.join('\n') );
    }
})

That is now we can do fancyname version and get version information for all of the private packages. Our tool is independant of the actual commands, and our commands are loosely coupled to the functionality they expose.

While these commands are simple, you could image commands that generate documentation or perform some kind of database initialization, etc. Moreover, because of our commands as modules set up, if we have to, we could directly require and invoke the commands programmatic-ally, making them multi-purpose and highly testible.

var version = require('project-foo/commands/version')

version.run(null, console.log) // print results  
version.run(null, someParseFn) // does some magic with results

// set flags progrommatically
version.setOptions({  
  args:['--someflag=1']
});
version.run(null, console.log);  

Lastly, because seeli commands don't alter argv, if you have your configuration system set up like we have talked about, you can pass additional flags through the command line tool to the configuration handlers to alter application set up and behavior.

Simple. Flexible. Powerful.

cli oop production ready seeli command line node.js