Build a Node.js CLI using yargs
November 15, 2019
As developers, we use cli tools everyday. We use them to simplify common tasks of our job:
- Packaging
- Linting
- Building apps
- Deploying apps
- Publishing packages
- Automate a lot of stuff...
But that's not all. A lot of them aren't related to development at all! Here is a list of cli apps.
I developped myself a couple of cli tools like gitmoji-changelog. It is a changelog generator for gitmoji commit convention. I also contributed to gatsby-cli which helps developers build blazing fast websites and apps using React. All of these were made with yargs.
Why using yargs?
Since Node.js provides us with all utilities to build a cli app, why should you use yargs
?
A good example is better than a lot of explanations. Let's go through the creation of a simple cli app. When called it will display Hello world!
.
What an original example!
The cli take an argument to override world
word. It also takes a named option times
to log the message more than once.
We will build it step by step without using yargs
then refactor the codebase using it.
First of all, we create an index.js
file with the following content.
console.log('Hello world!')
We execute our file using node
and our message is printed in our console.
foo@bar:~$ node index.jsHello world!
Fine, arguments are available in the argv
property of the global variable process
. The first one is the executable path and the second one the path to the JavaScript file that was executed.
["~/.nvm/versions/node/v10.15.3/bin/node","~/index.js"]
If we call the cli with an argument, it will the third element of this array. We get its value writing process.argv[2]
and using world
as default value if it isn't provided.
const args = process.argvconst name = args[2] || 'world'console.log(`Hello${name}!`)
Call the cli, you can now override world
!
foo@bar:~$ node index.js youHello you!
Things will go wild! Remember we want to add an option to display the message more than one time. Optional arguments are usually represented like this --times 3
. They can be placed where you want.
We begin by dealing with the case the optional argument is placed after the name argument.
const args = process.argvconst name = args[2] || 'world'const times = args[4] || 1for (let i = 0;i < times; i++) {console.log(`Hello${name}!`)}
Call the cli, now the message is displayed three times!
foo@bar:~$ node index.js you --times 3Hello you!Hello you!Hello you!
The previous code won't work if we don't provide the name argument. It won't work either if you place the optional argument before the name.
We change the code to handle the use case when the optional argument is placed in first position.
// ...if (args[2] === '--times') {name = args[4]times = args[3]}// ...
We keep the same behavior when placed after the name.
// ...} else if (args[3] === '--times') {name = args[2]times = args[4]}// ...
Here is the case where the name argument is provided and the optional argument isn't.
// ...} else if (args[2] && args[2] !== '--times') {name = args[2]}// ...
Here is the final code.
const args = process.argvlet times = 1let name = 'world'if (args[2] === '--times') {name = args[4]times = args[3]} else if (args[3] === '--times') {name = args[2]times = args[4]} else if (args[2] && args[2] !== '--times') {name = args[2]}for (let i = 0;i < times; i++) {console.log(`Hello ${name}!`)}
It is a bit complex and hard to read. Moreover it won't work if we add a new positional argument.
Refactor our cli app using yargs
To build a maintainable and scalable cli app we will use yargs. It exposes a lot of functions well described in its documentation. We will use the function command. It takes four parameters, a name, a description, a builder and a handler. If you pass *
or $0
as name parameter it will be the default command.
require('yargs').command('$0 [name]', 'start the server',() => {}, () => {console.log('Hello world!')})
The code is a bit more complex to only display a Hello world!
message. It will become more interesting as our code becomes more complex. Let's add our name argument. It will be done in the builder parameter which is a function that gets yargs instance as parameter. We use the positional function to describe our argument. As you can see, it directly takes a default value.
require('yargs').command('$0 [name]', 'start the server',(yargs) => {yargs.positional('name', {describe: 'name to display',default: 'world'})}, () => {console.log(`Hello world!`)})
Arguments are passed as parameter to the handler function. It is an object with a property for each argument. We named our argument name
, its value is available in argv.name
property.
require('yargs').command('$0 [name]', 'start the server',(yargs) => {yargs.positional('name', {describe: 'name to display',default: 'world'})}, (argv) => {console.log(`Hello ${argv.name}!`)})
Time to see the power of yargs
. We add our optional argument times
using the option function which has a similar API to positional
. We don't forget to add a default value. The for
is the same as in the vanilla implementation.
require('yargs').command('$0 [name]', 'start the server',(yargs) => {yargs.positional('name', {describe: 'name to display',default: 'world'}).option('times', {alias: 't',type: 'number',default: 1,description: 'number of times the message is logged'})}, (argv) => {for (let i = 0;i < argv.times; i++) {console.log(`Hello ${argv.name}!`)}})
As you can see, we didn't have to deal with the technical complexity of writing a cli app. yargs
handles it for us.
Bonus: It comes with help option
yargs
automatically adds a command help
for you! It uses the information we provided when we described our interface.
foo@bar:~$ node index.js --helpyargs.js [name]start the serverPositionals:name name to display [default: "world"]Options:--help Print the help [boolean]--version Print the version number [boolean]--times, -t number of times the message is logged [number] [default: 1]
yargs
's API is well documented and you can find more complex examples in it.
You're set 🙌
Now you can build all the cli apps you ever imagined!
Feedback is appreciated 🙏 Please tweet me if you have any questions @YvonnickFrin!