Creating HelloWorld CLI Command with NodeJS

Creating HelloWorld CLI Command with NodeJS

·

5 min read

I have recently returned from the World Congress conference organised by WeAreDevelopers in Berlin. It's been the second time I attended and I saw some very inspiring talks. One of them caught my attention in particular. It was Phil Nash's (Sonar) talk titled 4 Steps from Javascript to Typescript. Apart from the lesson on Typescript, he was using NodeJS to create a CLI command.

Now, that's nothing unseen before, we all use various CLI commands written in JS (think npm, trash-cli, etc.), but it has never occurred to write my own. In the past, when I needed a CLI script, I wrote it in ZSH. However, one of them was getting a bit too complex that I caught myself thinking: "Next time I'll write this in another language." At Phil's talk, I had that a-ha moment. Of course, I can use JS!

The Brief

This is my first little exercise to learn the ins and outs of creating a CLI command in NodeJS. This is the brief I gave myself.

Create a CLI command that accepts zero to many arguments and displays:

  1. "Hello world!" for no arguments.

  2. "Hello [name]!" if one argument is provided.

  3. "Hello [name1] and [name2]!" for two arguments.

  4. "Hello [name1], [name2], … [nameX]!" for three and more arguments.

Pre-requisites

  • Node – get some recent version, 18+, it will likely work on ancient versions too, but if you have to use an old version of Node, you probably know what you're doing anyway.

  • PNPM – Node package manager

The Solution

TLDR;

For those of you who are results-oriented, you can find the repo on my Github account.

Create a Node package

First I've created a new Node package. Nowadays, my package manager of choice is pnpm – it's fast, more performant and takes up less space on the disk thanks to the way it deals with Node modules.

# Create and enter a new folder
take hello-world-cli
# Initialise a new Node package
pnpm init

This will create a package.json file with the basic default metadata. We will need that file for installing our dependencies.

Basic file structure

./
├── LICENSE
├── README.md
├── bin
│   └── hello
├── node_modules
├── package.json
├── pnpm-lock.yaml
└── src
    ├── cli.js
    └── greetings.js

The CLI command hello

First, we create the file hello: touch /bin/hello. This is gonna be the actual command that the user will launch. Make sure to make it executable chmod a+x ./bin/hello.

#!/usr/bin/env node

const { run } = require('../src/cli');
run(process.argv).catch((err) => console.error(err));

As you can see I specify the shebang to execute this file using node as an interpreter. This enables us to use JS to import the run() function from the /src/cli.js and call it asynchronously passing the array of arguments (process.argv) from the command line as its parameter. The first element process.argv[0] is read-only and is usually reserved for the absolute path of the processor (in my case: /Users/myuser/.nvm/versions/node/v20.5.1/bin/node. The second array item process.argv[1] is the path to the command we're running: /Users/myuser/Sites/_tuts/hello-word-cli/hello.

In case there's an error, I catch it and display it in the console.

run() – process the command

First of all, let's install yargs package to make our life easier processing all the possible parameters: pnpm install yargs. I will touch just the surface of what this package can do for you but, this is a good template even for more complex CLI commands.

This is the code in /scr/cli.js.

const yargs = require('yargs');
const { hideBin } = require('yargs/helpers');
const { showGreeting } = require('./greetings');
async function run(args) {
    const { _: names } = yargs(hideBin(args)).argv;
    showGreeting(names);
}
module.exports = { run };

I import the yargs() function and its helper function hideBin(). The hideBin(args) is a shorthand for args.slice(2). This way, you can make your command work even in not-so-standard environments like Electron.

I define the asynchronous function run(args) that takes an array as its argument. Process this argument using the yargs() and hideBin() functions.

I desctructure the _ property from the argv returned by yargs and rename this variable as names for the convenience of my code and pass the names to my showGreeting(names) function.

That's it! As far as the processing of a CLI command is concerned, we're done. The rest of it is just plain JS as usual.

showGreeting(names) - display the right greeting

The file /src/greetings.js serves as a library of functions related to displaying greetings.

const joinTwoNames = (twoNames) => {
    return `${twoNames.join(' and ')}`;
};

const getLongGreetings = (longListOfNames) => {
    return longListOfNames.reduce(
        (acc, name, currentIndex) =>
            currentIndex < longListOfNames.length - 1
                ? `${acc}${name}, `
                : `${acc.slice(0, acc.length - 2)} and ${name}`,
        ''
    );
};

const getGreetings = (names) => {
    const greetingsList = [
        'Hello world!',
        `Hi there ${names[0}!`,
        `Hi there ${joinTwoNames(names)}!`,
        `Hi there ${getLongGreetings(names)}!`,
    ];
    const greetingsIndex = names.length >= 3 ? 3 : names.length;
    return `${greetingsList[greetingsIndex]}`;
};

const showGreeting = (names) => {
    const greeting = getGreetings(names);
    console.log(greeting);
};

module.exports = {
    getGreetings,
    showGreeting,
};

I export the showGreeting() function as well as getGreetings() for convenience even though I don't use getGreetings() anywhere else for now.

There is a list of string templates saved in the getGreetings(names) function that is used to decide which greeting to use depending on how many names are passed as its argument.

  1. No names? Fine, it's just Hello world!.

  2. One name? Easy, Hi there, ${names[0]} will print the first (and only) item of the names array.

  3. For two names, I have a helper function joinTwoNames(twoNames) that concatenates the two names in the array adding the and in between them and this is then used in the greetingList[2] string template.

  4. Any more than three names will end up using the getLongGreetings(longListOfNames) function that will rely on the high-order method array.prototype.reduce() to concatenate all individual names with a comma as its separator apart from the last two that will be linked by the conjunction and.

That's it, folks!

I hope you found this useful, please let me know if you have any questions or comments. I intended to demonstrate in a simple and easy-to-understand way the basics of creating a simple CLI command in Node.


Cover image by Emma Plunkett © 2016