Updated on Jun 3rd, 20216 min readjavascriptnodecli

How To Build A Command Line Application With Node

JavaScript can be used for more than just manipulating the DOM in the browser. NodeJS can be used to build powerful command line applications. Over recent months I have developed my own Node CLI application to speed up repetitive tasks at work.

The project we build in this article is a small part of the application I use on the job everyday. I promise it will do more than print "Hello World" to your console.

Getting Started

NodeJS

Project Source Code on GitHub

After installing Node, the node command should be in your PATH. This means you can execute Node from your terminal.

Print the version of NodeJS currently installed on your local machine.

node -v

Option 1 - Clone Source Code

Download the ZIP file or, better yet, clone the source code repository to your local machine.

git clone https://github.com/benjaminadk/node-cli-tutorial.git

Option 2 - Build From Scratch

If you want to code along with me and build from scratch just continue reading. When I follow tutorials I usually take this approach.

Create a folder for the project.

mkdir node-cli
cd node-cli

Initialize this folder as an NPM project and create a file to start writing code in. Node Package Manager.

npm init -y
touch index.js

If any of the instructions above were unfamiliar, or didn't work, you may want to do some googling to learn more about Node, NPM, Bash and the command line in general.

Our Node CLI Application

I suppose now would be a good time to let you know what this Node command line application is actually going to do.

We are going to make thumbnail images! More specifically, we will be able to navigate to a directory of full sized images and invoke our command. This will create a new directory full of thumbnails with a size we determine. In this use case we will be making 225x190 pixel thumbnails from 800x800 images and saving them as 40% quality JPEGs. We will use a library named Jimp to help with image manipulation. Commander will be used as a framework for this command line application.

Make sure you are in the project directory, or the same level as package.json when running the following command.

npm install jimp commander rimraf

To make sure everything is working correctly, add a little code to index.js.

index.js
console.log('Hello World')

And we're done!. šŸ˜Ž

Just kidding. This is just to make sure Node is working. I try to write tutorials that beginners can follow. From inside our node-cli directory we can now run our index.js file.

node ./index.js

Quick tip. index.js is recognized as a sort of default filename in Node. The following works as well, as Node will automatically look for index.js when executed in a directory.

node .

You should see Hello World output in the terminal.

How To Make Our App Available From CLI

The code above passes a relative filepath to the node command. Since the goal of this exercise is to make a command line tool, we need to make some modifications to be able to type a command anywhere on our computer and have our code execute.

First add the following line to the top of index.js. Understanding how this line works isn't important right now. It uses what is commonly called a Shebang #!.

index.js
#!/usr/bin/env node 

console.log('Hello World')

The package.json file also needs to be updated. The important lines are highlighted. This bin key is telling NPM that when we type make-thumbs on the command line we want to run index.js. I named it make-thumbs just to avoid any conflicts with node-cli, but this name is arbitrary.

Commands usually have some relation to what they do. Common commands include cd, ls, curl, mkdir, to name a couple. It is important to understand that when these commands are entered there is code running somewhere behind the scenes.

package.json
{
  "name": "node-cli",
  "version": "1.0.0",
  "description": "Command line tutorial",
  "main": "index.js",
  "bin": {
    "make-thumbs": "./index.js" 
  }, 
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": {
    "name": "benjaminadk",
    "email": "benjaminadk@gmail.com",
    "url": "https://github.com/benjaminadk"
  },
  "license": "ISC",
  "dependencies": {
    "commander": "4.1.0",
    "jimp": "0.9.3"
  }
}

Now type make-thumbs in the command line. It should throw and error something like what you see below.

Command Line Output For Make Thumbs
Command Line Output For Make Thumbs

There is one more step to wire the global command to work on our system. Make sure you are in the root of the project.

npm link

This should trigger the following output. NPM is working magic behind the scenes.

Command Line Output For NPM Link
Command Line Output For NPM Link

Try typing make-thumbs in the command line one more time.

Command Line Output For Make Thumbs Works!
Command Line Output For Make Thumbs Works!

šŸ‘

Note that this link can be undone via npm unlink. On a Windows machine you can check "~\AppData\Roaming\npm to see that NPM has created a .cmd file corresponding to the command name. ~ refers to C:\Users\your-user-name aka the HOME directory. This information is not crucial but nice to know for a broader understanding.

Now this project is setup and we can add some useful code.

index.js
#!/usr/bin/env node

const program = require('commander')

program
  .version('1.0.0')
  .name('make-thumbs')
  .description('An image resizer to make thumbnails')
  .option('-s,--source [folder]', 'Source images directory', 'images')
  .option(
    '-d,--destination [folder]',
    'Directory to be created for thumbnails',
    'thumbnails'
  )
  .parse(process.argv)

Coding Our Node CLI App

Commander is a great framework which helps to set up options and produces help menus automatically. Here I am assigning a version, name and description, as well as some options. Finally, we are parsing process.argv. These are the arguments provided to the command. With just this code we already have a working command line tool.

make-thumbs --help

Output for our help command.

Command Line Output For Help Command
Command Line Output For Help Command

The options allow us to input a path to a directory of source images and a path to the directory we want to save the new thumbnails in. These are relative to the current working directory and not absolute paths. Relative paths make sense because the nature of a command line application is that is portable. Relative paths are also shorter to type. These options will be passed into our underlying image manipulation logic.

Create a separate folder and file to hold some of this logic to keep things organized.

mkdir lib
cd lib
touch index.js

I want to take advantage of Async/Await code so I am using promisify. These utilities help to read directories, make directories, remove directories and check if directories exist. Consult the Node File System API Documentation for more information on these. I have also including the Jimp logic to create a thumbnail to our specs.

lib/index.js
const jimp = require('jimp')
const rimraf = require('rimraf')

const fs = require('fs')
const { promisify } = require('util')

const thumbnail = async (src, dest) => {
  const image = await jimp.read(src)
  await image.resize(225, 190, jimp.RESIZE_BICUBIC)
  image.quality(40)
  await image.writeAsync(dest)
}

const directoryExists = (filepath) => {
  return fs.existsSync(filepath)
}

const readdir = promisify(fs.readdir)
const mkdir = promisify(fs.mkdir)
const rm = promisify(rimraf)

module.exports = {
  thumbnail,
  directoryExists,
  readdir,
  mkdir,
  rm,
}

Here is the finished code for index.js with our utilities imported.

index.js
#!/usr/bin/env node

const program = require('commander')
const path = require('path')

const { thumbnail, directoryExists, readdir, mkdir, rm } = require('./lib')

program
  .version('1.0.0')
  .name('make-thumbs')
  .description('An image resizer to make thumbnails')
  .option('-s,--source [folder]', 'Source images directory', 'images')
  .option(
    '-d,--destination [folder]',
    'Directory to be created for thumbnails',
    'thumbnails'
  )
  .parse(process.argv)

const main = async () => {
  try {
    // Use current working dir vs __dirname where this code lives
    const cwd = process.cwd()

    // Use user input or default options
    const { source, destination } = program
    const srcPath = path.join(cwd, source)
    const destPath = path.join(cwd, destination)

    // Remove destination directory is it exists
    if (directoryExists(destPath)) {
      await rm(destPath)
    }

    // Create destination directory
    await mkdir(destPath)

    // Read source directory
    const imagesAll = await readdir(srcPath)

    // Create thumbnails
    for (let image of imagesAll) {
      const src = path.join(srcPath, image)
      const dest = path.join(destPath, image)
      console.log(`Creating thumbnail at: ${dest}`)
      thumbnail(src, dest)
    }

    console.log('Thumbnails created successfully!')
  } catch (error) {
    console.log('Error creating thumbnails.')
  }
}

main()

All of our logic is placed inside the main function which is executed at the bottom of the code. Within main there is a try/catch block. This is helpful in that we can handle errors and simplify error output. With this structure the message inside the catch block is all the user will see if there is an error. This can be customized to any message desired, including part or all of the actual error thrown. In development you can simply log the error here to troubleshoot.

One important aspect is the use of process.cwd(). This command line application works based on the directory level the user is located in, or the current working directory. This is used to create the paths to the source and destination folders. The destination folder is deleted and created again if it exists prior to command execution. Then the contents of the source directory are read into memory.

Finally, these files are looped over and a thumbnail is created for each image and saved into the destination folder. I added some logs to give a sense of the program working. These can be removed or even replaced with some sort of progress logic. It is all up to you!

To make sure everything is working I have included a test directory in the source files. To test functionality do the following.

cd test
make-thumbs
Benjamin Brooke Avatar
Benjamin Brooke

Hi, I'm Ben. I work as a full stack developer for an eCommerce company. My goal is to share knowledge through my blog and courses. In my free time I enjoy cycling and rock climbing.