I was once told that the best blog post is something you had to find out for yourself.
So on that basis, this post will cover how to create a basic CLI tool in NodeJS using Commander.
I’ve published the resulting code from the post to my GitHub which you can find here if you want to skip to that.

Installation

To start with we need to initialize a new NPM package using npm init. Once done, create an index.js file alongside your package.json.
Now let’s add our dependencies - run the command npm install —save commander.
Commander is the package we’ll be using to make the command line app.

Getting Started

Now we can get started with a very basic example. With Commander we specify commands, what is input on the terminal, and actions to be taken when one of those commands is encountered.

'use strict';

const program = require('commander');

program.command('say-hello').action(() => {
    console.log('hello');
});

program.parse(process.argv);

And that’s all that’s needed to create a command line app! You could publish that package, install it globally, and use it anywhere from your terminal.

Running Locally

To test this, you can install the local module using npm install -g ./ - but I don’t like installing half baked packages, as I have a habit of forgetting about them. Instead, I prefer to simply invoke the script manually for local development by using node index.js say-hello. Running that command will run our action, and print out the word ‘hello’ to our terminal. Astounding!

App Info and Help

The next step is to add a bit more information to our program to help our fellow developers out.
The first bit to add is the version:
program.version('0.0.1');
This is the version that will be printed out when you invoke the CLI with the --version flag.
Next, I want to update the help information. Right now, asking for help isn’t very… helpful. Running node index.js --help will output:

Usage: index [options] [command]

Options:
  -h, --help       output usage information

Commands:
  say-hello|hi

There are two ways we can go about updating this. You can override everything that’s output:

program.help(() => {
    return 'Only the weak need help!';
});

But that’s a bit too much work for me. I’d prefer to let Commander generate the bulk of the help output, I just want to append some information to the end of the help content - perhaps to provide a point of contact if things are unclear:

program.on('--help', () => {
    console.log('');
    console.log('Blame Ian for this mess');
});

This will be appended at the very end of the content output from running node index.js --help - the blank console.log is simply to add a blank line between the original help output and my added line, just for niceness.

One last thing - if you look at our previous help output you’ll see our app is named index which isn’t right. To change this, we have to set our application name with Commander:
program.name('node-cli').usage('command [options]')
The usage extension I’ve added for personal preference. Whenever I call a CLI tool myself, I put the command first, followed by the options. The default help output does it wrong the other way around, so I changed it. You can change anything you want here, but I didn’t see the need to.
Now let’s run the help command again, and see what gets output:

Usage: node-cli command [options]

Options:
  -h, --help  output usage information

Commands:
  say-hello

Blame Ian for this mess

Now we have our appended line, our name and usage has been updated!

Improving Commands

Now let’s explore some of the functionality of Commander so that we can actually make something useful. One thing that can be useful is adding an alias to a command - my use case for this would be to add a shorthand version, as I know how much other developers hate inefficiency (not me - I’m incredibly inefficient at all times). For example, as well as the more descriptive say-hello we could add an alias of hi to make it shorter to call the command:

program.command('say-hello')
			.alias('hi')
			.action(() => {
   			console.log('hello');
			});

Now you can instead run node index.js hi and get the same output.

The next thing to do is to update our command to add a description:

program.command('say-hello')
			.alias('hi')
			.description(`Feeling lonely? Need a friend? I'll talk to you.`)
			.action(() => {
   			console.log('hello');
			});

Doing this will update the help information that gets output so that the command has a description, rather than just specifying how to invoke it.

To make our CLI actually useful we need to be able to take input, and do something based on that, rather than just echoing the same thing back every time. So to do this we’re going to add in a parameter, and there are two ways you can do this. The first is to add a named parameter to the command using angular brackets:

program.command('say-hello <name>')
			.alias('hi')
			.description(`Feeling lonely? Need a friend? I'll talk to you.`)
			.action((name) => {
   			console.log(`Hello ${name}!`);
			});

Doing it this way, if you run our previous command node index.js say-hello you’ll now get an error because of the missing argument:
error: missing required argument 'name'
And we access the parameter through a named argument to the action, as shown above.

The second way to add arguments is through flags - this is done by adding an option to our command:

program.command('say-hello <name>')
        .alias('hi')
        .option('-c, --compliment', 'Need a self esteem boost?', false)
        .action((name, args) => {
            let output = `Hello ${name}!`;
            if(args.compliment) {
                output = `${output} You look beautiful today!`;
            }
            console.log(output);
        });

Here I’ve added an optional flag - you can omit this one no problem. I’ve also added a description, which is output from the commands help flag node index.js say-hello --help:

Usage: node-cli say-hello|hi [options] <name>

Options:
  -c, --compliment  Need a self esteem boost? (default: false)
  -h, --help        output usage information

By default, the option will be undefined if omitted - I’ve set it to false as a personal preference. These values are accessed by named properties on an args parameter we pass to the action.
This example is purely for a boolean value, if you want to receive string input you can add a name value in angular brackets, the same we did for the command itself - except for an option is always optional (funnily enough). So if you depend on a value, be sure to validate it.

Subcommands

One last thing of note is the use of subcommands.
To configure subcommands we define a command as usual, however we provide an object containing an executableFile parameter. I’ve added a ‘quote’ command to demonstrate this:

program.command('quote [command]', 'Get a quote!', {executableFile: 'lib/commands/quote'});

In the root of the project I’ve created a new folder lib and another under that two more folders: actions - in which I placed a new quoter.js file.
The structure here is personal preference, but I wanted to separate the definition of my commands from the actions they invoke.
The executableFile parameter points to the commands/quote JS file which specifies the actions:

'use strict';

const program = require('commander');

const quoter = require('../actions/quoter.js');

program.command('harry-potter')
        .description('Get a Harry Potter quote!')
        .action(() => {
            quoter.harryPotter();
        });

program.command('car-insurance')
        .description('Get a very reasonable car insurance quote! (£10k minimum per annum)')
        .action(() => {
            quoter.carInsurance();
        });

program.parse(process.argv);

The action for this use case is very simple, just printing out a console log, but you could have it do whatever your heart desires:

'use strict';

const quoter = {
    harryPotter: () => {
        console.log(`"I hope you're pleased with yourselves. We could all have been killed — or worse, expelled" - Hermione Granger`);
    },
    carInsurance: () => {
        console.log('More than your car is worth. I never said this made sense.');
    }
};

module.exports = quoter;

So now you can run the command node index.js quote harry-potter and receive a wonderful Harry Potter quote, or node index.js quote car-insurance to get a very reasonable car insurance quote.
And if you run the help command on the quote command: node index.js quote --help you can see that all the arguments are listed:

Usage: quote [options] [command]

Options:
  -h, --help     output usage information

Commands:
  harry-potter   Get a Harry Potter quote!
  car-insurance  Get a very reasonable car insurance quote! (£10k minimum per annum)

Hopefully this was of use to someone other than just me - but if not, hey, at least I have my own reference to go back to. If you want to take a look at the full resulting code for this post, you can find it on my GitHub here.