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:
"Hello world!" for no arguments.
"Hello [name]!" if one argument is provided.
"Hello [name1] and [name2]!" for two arguments.
"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.
No names? Fine, it's just
Hello world!
.One name? Easy,
Hi there, ${names[0]}
will print the first (and only) item of thenames
array.For two names, I have a helper function
joinTwoNames(twoNames)
that concatenates the two names in the array adding theand
in between them and this is then used in thegreetingList[2]
string template.Any more than three names will end up using the
getLongGreetings(longListOfNames)
function that will rely on the high-order methodarray.prototype.reduce()
to concatenate all individual names with a comma as its separator apart from the last two that will be linked by the conjunctionand
.
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
Top comments (0)