Build a command-line interface app using node.js and dastoor

15 August 2015   Comments   nodejs cli command-line terminal

While building a command-line interface using node for myself (onetime-cli), I felt the need for a lean, convention-based and well-structured tool for building command-line interface apps.

This is how dastoor (in Farsi meaning “command”) was born.

In this article I will take you through how to create a simple (with the potential to become complicated) CLI.

The example that we will implement is an app with a command to sum any number of input arguments.

To start, create an empty nodejs app and install dastoor:

mkdir my-cli
cd my-cli
npm init
npm install --save dastoor

Now add index.js with the following code:

#! /usr/bin/env node
var dastoor = require('dastoor'),
    runner  = new dastoor.Runner(),

    app     = require('./commands');

runner.run(app, process.argv.splice(2));

This code creates an instance of Runner and pass input arguments and app (which we are going to define below) to it.
this is enough for our simple CLI to be wired.

Then create a commands.js file to define your command structure in.
As your app becomes bigger and bigger you can break this file down into many other files.

var cli = require('dastoor').builder;

var root = cli.node('my-cli', function() {
    console.log('hello world!');
});

module.exports = root;

OK, it’s time to see some output.
Before we would be able to run our CLI from terminal, we need to register it with npm. To do so add the following to your package.json:

  "bin": {
    "my-cli": "index.js"
  }

This will tell npm that my-cli is a command we are adding to terminal. When users install your cli via npm install -g, npm will add my-cli as a terminal command.

On dev machine however, you would have to manually do the linking by running the following command from your app directory:

npm link

Now by running

my-cli

You should get:

hello world!

Now let’s add some real command to do the sum. We are going to add it to commands.js file:

cli.node('my-cli.sum', { terminal: true})
.controller(function(args) {
   var res = args.initial || args.i || 0;
   args._.forEach(function (i) {
       res += +i;
   });

   console.log('Sum: ' + res);
});

The first line tells dastoor that we are defining the sum command as a sub-command of my-cli. terminal: true tells the runner that this is a terminal node (i.e. doesn’t have any sub-nodes). We need to specify this otherwise runner will try to interpret our numbers (in my-cli sum 1 2 3 command) as sub-command names.

Then we call into .controller() function to define the controller function of our sum command – i.e. where we do the sum. args input is the minimist parsed version of all terminal arguments after my-cli sum. which in this instance is all of our numbers in _ (free parameters) and i (named parameter).

We simply sum all of the numbers and the i parameter and print the result in the output.

Now entering this into terminal:

my-cli sum 1 2 3 -i 100

will output:

Sum: 106

Let’s add some help to our command. Add the following method call to the end of the .node('my-cli.sum) command:

.help({
    description: 'sums numbers',
    options: [{
        name: '-i, --initial',
        description: 'initial value'
    }],
    usage: [
        'e.g. my-cli sum 1 2 3 -i 100'
    ]
});

This will tell dastoor all it needs to know to render help output.

my-cli sum -h
my-cli sum --help
>   sums numbers

    options:
      -i, --initial        initial value
      -h, --help           show this help

    usage:
      e.g. my-cli sum 1 2 3 -i 100

Similarly for .node('my-cli'):

.help('my test cli')
my-cli --help
>   my test cli

    options:
      -h, --help           show this help

    usage:
      my-cli sum                 sums numbers

I’m going to stop here with the example but there are all sorts of possibilities with dastoor. For more dastoor checkout the API Reference

The full source for the sample CLI in this article can be found on github – mvalipour/dastoor-sample.

Conclusion

dastoor helps you build a CLI tool very easily and with most of things like help and argument parsing, coming out-of-the-box.

Also given, the API style it has (inspired by angular.js) it structures our app in a way to keep the code required to wire-up a command in one place, resulting in more cocise code structure.


blog comments powered by Disqus