CLI (Command-line interface) is a very common way to run a program in a terminal. As a software engineer you use different CLIs every day - git, Docker, npm, etc.

Today I want to share my experience building CLIs using Node.js and a few helpful packages.

Goal Link to this heading

I’m going to only focus on npm in this article, simply because I don’t have enough experience with other package managers for Node.js and apparently npm is still the most popular choice.

So, our goal is to have something like this in the end:

npm install -g awesometool
awesometool run --fast

npm setup Link to this heading

First of all, let’s make sure we have a proper npm configuration and folder structure!

bin/awesometool.js:

js
1#!/usr/bin/env node
2
3require("../lib/awesometool")

Snippet above should look really straightforward - we simply require the main file (or entry point) of the app, in our case ./lib/awesometool.js.

package.json:

js
 1{
 2  "name": "awesometool",
 3  "version": "0.1.2",
 4  ...
 5  "main": "./lib/awesometool.js",
 6  "bin": {
 7    "awesometool": "bin/awesometool.js"
 8  },
 9  ...
10}

The most important things in the package.json are:

  • name: this is what we use for naming (in npm ecosystem)
  • version: every time you change your program you’ll have to update the version and publish it
  • main: simply an entry point
  • bin.awesometool: should point to our bin/awesometool.js file. Also, this name is going to be used after the installation as a terminal command for our program

Designing the CLI Link to this heading

Now we have the basic setup and it’s time to think about the CLI itself. Usually every Command-line interface description consists from a few sections:

  • usage information
  • a list of available commands
  • a list of available options

So, you need to decide how to structure the CLI - what commands do you need, what options are available, what are the safe defaults, etc.

A few resources that can help with that:

Also I encourage to get some inspiration from well-known tools like Docker or Heroku CLI.

After you realize what kind of commands and options you would need, instead of implementing command-line arguments parsing from scratch (yay) and solving a bunch of terminal rendering issues we’re going to use commander.

commander is an awesome tool to help us define CLI commands, options and related actions. Here’s an example:

js
 1import commander from 'commander'
 2
 3commander
 4    .option('--fast', 'running things even faster')
 5
 6commander
 7    .command('run')
 8    .description('Run something')
 9    .action(() => {
10        console.log('Working!')
11        console.log(commander.dryRun)
12    })
13
14commander.parse(process.argv)
15
16if (commander.rawArgs.length <= 2) {
17    commander.help()
18}

I like how concise and expressive it is. We define one command with an action plus an option that’s available for all commands.

This is how it looks in the terminal:

  Usage: awesometool [options] [command]

  Commands:

    run   Run something

  Options:

    -h, --help  output usage information
    --fast  running things even faster

Bonus: inquiry session flow Link to this heading

If you build a complex CLI you might need to ask user a few questions before proceeding. To make things easier for the end user it’s usually a good idea to introduce validation, default values and some other helpers. Also, sometimes user needs to choose from a set of predefined values instead of entering a custom one. All these things are handled by inquirer!

Let me show you the wonderful pizza example they have:

js
 1/**
 2 * Pizza delivery prompt example
 3 * run example by writing `node pizza.js` in your console
 4 */
 5
 6'use strict';
 7var inquirer = require('..');
 8
 9console.log('Hi, welcome to Node Pizza');
10
11var questions = [
12  {
13    type: 'confirm',
14    name: 'toBeDelivered',
15    message: 'Is this for delivery?',
16    default: false
17  },
18  {
19    type: 'input',
20    name: 'phone',
21    message: 'What\'s your phone number?',
22    validate: function (value) {
23      var pass = value.match(/^([01]{1})?[-.\s]?\(?(\d{3})\)?[-.\s]?(\d{3})[-.\s]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?){1}(?:\d+)?)?$/i);
24      if (pass) {
25        return true;
26      }
27
28      return 'Please enter a valid phone number';
29    }
30  },
31  {
32    type: 'list',
33    name: 'size',
34    message: 'What size do you need?',
35    choices: ['Large', 'Medium', 'Small'],
36    filter: function (val) {
37      return val.toLowerCase();
38    }
39  },
40  {
41    type: 'input',
42    name: 'quantity',
43    message: 'How many do you need?',
44    validate: function (value) {
45      var valid = !isNaN(parseFloat(value));
46      return valid || 'Please enter a number';
47    },
48    filter: Number
49  },
50  {
51    type: 'expand',
52    name: 'toppings',
53    message: 'What about the toppings?',
54    choices: [
55      {
56        key: 'p',
57        name: 'Pepperoni and cheese',
58        value: 'PepperoniCheese'
59      },
60      {
61        key: 'a',
62        name: 'All dressed',
63        value: 'alldressed'
64      },
65      {
66        key: 'w',
67        name: 'Hawaiian',
68        value: 'hawaiian'
69      }
70    ]
71  },
72  {
73    type: 'rawlist',
74    name: 'beverage',
75    message: 'You also get a free 2L beverage',
76    choices: ['Pepsi', '7up', 'Coke']
77  },
78  {
79    type: 'input',
80    name: 'comments',
81    message: 'Any comments on your purchase experience?',
82    default: 'Nope, all good!'
83  },
84  {
85    type: 'list',
86    name: 'prize',
87    message: 'For leaving a comment, you get a freebie',
88    choices: ['cake', 'fries'],
89    when: function (answers) {
90      return answers.comments !== 'Nope, all good!';
91    }
92  }
93];
94
95inquirer.prompt(questions).then(function (answers) {
96  console.log('\nOrder receipt:');
97  console.log(JSON.stringify(answers, null, '  '));
98});

I like the balance between using declarative rules and writing custom logic for validation and default values.

Bonus: colors Link to this heading

Yes, colors in your terminal! Or colours

So easy to use:

js
1import colors from 'colors'
2
3console.log('hello'.green); // outputs green text
4console.log('i like cake and pies'.underline.red) // outputs red underlined text
5console.log('inverse the color'.inverse); // inverses the color
6console.log('OMG Rainbows!'.rainbow); // rainbow