Using Node.js to Create Powerful, Beautiful, User-Friendly CLIs

The NodeSource Blog

You have reached the beginning of time!

Using Node.js to Create Powerful, Beautiful, User-Friendly CLIs

Not every Node.js application is meant to live in the web; Node.js is a popular runtime allowing you to write multiple types of applications running on a variety of platforms—from the cloud to many IoT devices. Naturally, Node.js can also run in your local shell, where powerful tools can perform magic, executing useful tasks enhancing your developer capabilities.

A Command Line Interface (CLI), can perform anything from a simple operation—like printing ASCII art in the terminal like yosay—to entirely generating the code for a project based on your choices using multiple templates like Yeoman yo. These programs can be installed globally from npm, or executed directly using npx if they are simple enough.

Let's explore the basics of building a simple CLI using Node.js. In this example, we’re creating a simple command which receives a name as an argument displaying an emoji and a greeting.

The first thing you should do as in every application is to create a folder for it and execute:

$ npm init

The previous command will ask for some information like the package name, version, license, and others creating the package.json at the end, looking like this:

{
  "name": "hello-emoji",
  "version": "1.0.0",
  "description": "A hello world CLI with a nice emoji",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "edsadr",
  "license": "MIT"
}

As we want our CLI to be available as a command, we need to configure our package.json to treat our program like that, to do it we add a bin section like this:

"bin": {
  "hello-emoji": "./index.js"
}

In this case, hello-emoji is the command we are registering to execute our program, and ./index.js is the file to be executed when the command is invoked.

To display emojis, let's add a package:

$ npm install node-emoji -S

Now, let's create the file to be executed, index.js:

#!/usr/bin/env node
'use strict'

const emojis = require('node-emoji')

if (!process.argv[2]) {
  console.error(`${emojis.get('no_entry')} Please add your name to say hello`)
  process.exit(1)
}

console.log(`${emojis.random().emoji}  Hello ${process.argv['2']}!`)

Note that we add #!/usr/bin/env node at the top. This tells the system what interpreter to pass that file to for execution; in our case the interpreter is Node.js. After that, the code is fairly straightforward. It requires the node-emoji module and validates process.argv[2], which is the first argument placed by the user. By default process.argv[0] is the path for Node.js binary, and process.argv[1] is the path for the script being executed.

After adding this code, our command is ready to be executed; you can get a 'Hello world!' in your console by running:

$ node index.js world

If you want to run it using the command specified at the bin section of our package.json, you’ll need to install the package globally from npm. But just for development purposes to run it locally we can use:

$ npm link

After executing this command, you can try to execute:

$  hello-emoji world

Arguments parsing

After examining the code we just wrote, you’ll likely realize that the main issue when writing this kind of application is to control the user's input parsing the arguments included in the command execution. Fortunately, the npm ecosystem offers plenty of choices to solve this problem.

Here are some modules helping you to parse user-entered arguments. Some even provide some guidelines to structure your CLI's code:

These packages allow you to create a CLI supporting multiple operations and include parameters; you could efficiently structure something for our CLI to do things like:

$ hello-emoji --name=world --json

Printing a JSON object with our greeting

$ hello-emoji --name=world --emoji=coffee

Instead of a random emoji, this one prints the coffee emoji

Here is an example implementing minimist to do the parsing to execute commands like the ones above:

#!/usr/bin/env node

'use strict'

const emojis = require('node-emoji')
const minimist = require('minimist')
const opts = minimist(process.argv.slice(2))

const emoji = opts.emoji ? emojis.get(opts.emoji) : emojis.random().emoji

if (!opts.name) {
  console.error(`${emojis.get('no_entry')} Please add your name to say hello using the '--name=' parameter`)
  process.exit(1)
}

if (!emojis.has(opts.emoji)) {
  console.error(`${opts.emoji} is not a valid emoji, please check https://www.webfx.com/tools/emoji-cheat-sheet/`)
  process.exit(1)
}

const greeting = `${emoji}  Hello ${opts.name}!`

if (opts.json) {
  console.log(JSON.stringify({greeting}))
} else {
  console.log(greeting)
}

Going interactive

So far, we have been working with information coming from the command execution. However, there is also another way to help make your CLI more interactive and request information at execution time. These modules can help to create a better experience for the user:

With a package like the ones above, you could ask the user directly to input the desired information in many different styles. The example below is using inquirer to ask the users for the name if it was not included as an argument. It also validates the emoji and requests a new one if the input is not valid.

#!/usr/bin/env node

'use strict'

const emojis = require('node-emoji')
const inquirer = require('inquirer')
const minimist = require('minimist')
const opts = minimist(process.argv.slice(2))

let emoji = opts.emoji ? emojis.get(opts.emoji) : emojis.random().emoji

async function main () {
  if (!opts.name) {
    const askName = await inquirer.prompt([{
      type: 'input',
      name: 'name',
      message: `Please tell us your name: `,
      default: () => 'world',
      validate: (answer) => answer.length >= 2
    }])

    opts.name = askName.name
  }

  if (opts.emoji && !emojis.hasEmoji(opts.emoji)) {
    console.error(`${opts.emoji} is not a valid emoji, please check https://www.webfx.com/tools/emoji-cheat-sheet/`)
    const askEmoji = await inquirer.prompt([{
      type: 'input',
      name: 'emoji',
      message: `Please input a valid emoji: `,
      default: () => 'earth_americas',
      validate: (emoji) => emojis.hasEmoji(emoji)
    }])

    emoji = emojis.get(askEmoji.emoji)
  }

  const greeting = `${emoji}  Hello ${opts.name}!`

  if (opts.json) {
    console.log(JSON.stringify({
      greeting
    }))
  } else {
    console.log(greeting)
  }
}

main()

Adding some Eye Candy

Even if the interface for this kind of application is reduced to what you can have in a shell, it does not mean that the UI should look bad. There are plenty of tools that can help make your apps look good; here are some different libraries that will add a nice touch to the look of your CLI output:

  • Chalk or Colors will allow you to set the color of your text.
  • To include images translated to ASCII art, try asciify-image or ascii-art
  • If you have to output much information a well-organized output could be in tables, try Table or Cli-table
  • If your CLI requires processes taking some time, like consuming external APIs, querying databases or even writing files, you can add a cute spinner with Ora or Cli-spinner.

Conclusion

Creating user-friendly, useful and beautiful CLIs is part science and part art. After exploring the basics of creating a CLI tool, you can go and explore a universe of possibilities with the packages available through the npm registry. Hopefully, you’ll soon be creating functional and user-friendly tooling that’s missing from your current inventory thanks to the power of Node.js.

The NodeSource platform offers a high-definition view of the performance, security and behavior of Node.js applications and functions.

Start for Free