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
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
First of all, let’s make sure we have a proper npm configuration and folder structure!
bin/awesometool.js:
js1#!/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 itmain
: simply an entry pointbin.awesometool
: should point to ourbin/awesometool.js
file. Also, this name is going to be used after the installation as a terminal command for our program
Designing the CLI
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:
- https://trevorsullivan.net/2016/07/11/designing-command-line-tools/
- https://softwareengineering.stackexchange.com/questions/307467/what-are-good-habits-for-designing-command-line-arguments
- http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap12.html#tag_12_01c
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
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
Yes, colors in your terminal! Or colours…
So easy to use:
js1import 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