DEV Community

Cover image for Build a CLI with Node.js
Rushan Khan
Rushan Khan

Posted on • Updated on

Build a CLI with Node.js

Command-line utilities are the most basic and beautiful apps ever created, the apps that started it all.

We use command-line utilities every day, whether it be git, grep, awk, npm, or any other terminal app. CLIs are super useful and usually the fastest way to get something done. Do you have something in your specific workflow that you have to do over and over again? Chances are that can be automated with a CLI.

We are going to use Node.js to make our CLI if it wasn’t clear from the title itself. Why? Because the Node.js ecosystem has thousands of extremely useful packages that we can utilize to achieve what we are trying to do. Whatever it may be that you are trying to do, it is highly probable that there exists a package for it on npm, also node has built-in libraries to do a lot of things like handling files, launching other applications, asserting tests, etc. Apart from that CLIs built in Node.js are highly portable, meaning they are easy to install on different OSs.

For the purpose of this tutorial, we’ll be building a simple CLI to translate between languages. We’ll accept string type arguments, parse them into a sentence, shoot them off to a translation API which will fetch us the translations, and then display the result. The complete code for this can be found on the Github repository. Let’s dive right into it!


Prerequisites

Here are the tools that are required to make the CLI so make sure you have them before starting:

  1. A recent version of Node.js installed.
  2. A text editor.

and that’s probably it.

Setting up the project

We’ll be setting up a basic Node.js project:

  1. Open up your terminal.

2. Create a folder for the project.

~$mkdir termTranslate
Enter fullscreen mode Exit fullscreen mode

3. Navigate to it.

~$cd termTranslate 
Enter fullscreen mode Exit fullscreen mode

4. Initialize a Node.js project in that folder.

~$npm init
Enter fullscreen mode Exit fullscreen mode

5. Fill in the prompt.

Your project is now set up.

Building the basic CLI

Now that we have our node project ready we move to actually making the CLI. Here’s what you have to do:

  1. Create a folder named bin in the root directory of your project.
  2. Inside bin create a file called index.js This is going to be the entry point of our CLI.
  3. Now open the package.json file and change the “main” part to bin/index.js.
  4. Now manually add another entry into the package.json file called bin and set it’s key to tran and it’ s value to ./bin/index.js. The addition should look something like this:
"bin": {  
    "tran": "./bin/index.js"  
  }
Enter fullscreen mode Exit fullscreen mode

The key, tran, is the keyword for calling the CLI. This is the keyword that people will type in the terminal for using your CLI. Be free to name it whatever you like, although I would suggest keeping the name short
and semantic so that it’s quick to type and easy to remember.

The name defined here is not permanent and can be changed whenever you like.

Your entire package.json file should look something like this:

{  
    "name": "termtran",  
    "version": "1.0.0",  
    "description": "A CLI to translate between languages in the terminal",  
    "main": "bin/index.js",  
    "scripts": {  
 "test": "echo "Error: no test specified" && exit 1"  
    },  
    "keywords": [  
 "cli"  
    ],  
    "bin": {  
 "tran": "./bin/index.js"  
    },  

    "author": "Your Name",  
    "license": "ISC"  
}
Enter fullscreen mode Exit fullscreen mode

Note: Don’t forget to add the extra comma after adding the new entry into the package.json file. That is an easy mistake.



5. Open the index.js file in the bin folder. And put the following code in it:

#! /usr/bin/env node
console.log("Hello World!");
Enter fullscreen mode Exit fullscreen mode

The first line starting with #! is called a shebang line or a bang line. A shebang line is used to specify the absolute path to the interpreter that will run the below code. The shebang line used here is for Linux or UNIX type systems but node requires it for Windows and macOS too, for proper installation and execution of the script.



Now let’s install and test our CLI.

People may call our CLI from anywhere in the system so let's install it globally.

Navigate to the root directory of the project and then run

~$npm install -g .
Enter fullscreen mode Exit fullscreen mode

The -g flag tells npm to install the package globally on the system.

Test the CLI by typing the specified keyword in the terminal.

~$tran
Enter fullscreen mode Exit fullscreen mode

If everything was done correctly then you should be greeted with the message which we console.logged in the in
dex.js
file.


Something like this:

All good!

Handling Command Line arguments

Our basic CLI is ready and now we move to adding further functionality.

The most basic task that any CLI does is handling command-line arguments. In our CLI, we will be receiving the language name and the sentence to be translated as arguments and then we will parse it.

Although Node.js offers built-in functionality for handling command line arguments, we are going to use an npm package called yargs 🏴‍☠ which is specifically made for building CLI
s. yargs will simplify our process of parsing arguments and help us organize command line flags.

  1. Install yargs
~$npm i yargs
Enter fullscreen mode Exit fullscreen mode

2. After installing it, include the module in your index.js :

~$const yargs = require("yargs");
Enter fullscreen mode Exit fullscreen mode

3. Then create the options object containing all your command line flags:

const usage = "\nUsage: tran <lang_name> sentence to be translated";const options = yargs  
      .usage(usage)  
      .option("l", {alias:"languages", describe: "List all supported languages.", type: "boolean", demandOption
: false })                                                                                                    
      .help(true)  
      .argv;
Enter fullscreen mode Exit fullscreen mode

In the above code, I have defined an option -l which, when passed will print all the supported languages by the API, we will implement this later. Yargs provides us with --help and --version flags by default.

If you want an option to be compulsory then you can set it’s demandOption value to true , this will get yar
gs to throw a Missing argument error if the flag is not provided.


Testing it:

Nice!

All the arguments that you pass with the command gets stored under the listyargs.argv._ unless the argument begin with a or a -- in that case, it is treated as a flag with a default value of boolean. You can console.log yargs.argv to get a better picture of how the arguments are stored.

Access the value of the passed flags using yargs.argv.flagname.

Adding Utility Functions

Now it’s time to add utility functions.

I plan to take input as:

~$tran lang_name the sentence to be translated 
Enter fullscreen mode Exit fullscreen mode

So we will need to parse the arguments.

We can write all the utility functions in our index.js but that wouldn’t look neat so I will make a separate file utils.js for all functions. Here’s what we need to do:

  1. Create another file called utils.js in the bin folder.
  2. Include the file in your index.js :
const utils = require('./utils.js')
Enter fullscreen mode Exit fullscreen mode

3. Create a function for parsing the sentence:

Write the function inutils.js and then export it:

module.exports = { parseSentence: parseSentence };function parseSentence(words) {  
    var sentence = "";  
    for(var i = 1; i < words.length; i++) {  
 sentence = sentence + words[i] + " ";  
    }
Enter fullscreen mode Exit fullscreen mode

Call it in index.js :

var sentence = utils.parseSentence(yargs.argv._);
Enter fullscreen mode Exit fullscreen mode

4. Create a function to show help when no argument is passed:

Create a function in your utils.js :

module.exports = { showHelp: showHelp, parseSentence: parseSentence };const usage = "\nUsage: tran <lang_name
> sentence to be translated";
function showHelp() {                                                            
    console.log(usage);  
    console.log('\nOptions:\r')  
    console.log('\t--version\t      ' + 'Show version number.' + '\t\t' + '[boolean]\r')  
    console.log('    -l, --languages\t' + '      ' + 'List all languages.' + '\t\t' + '[boolean]\r')  
    console.log('\t--help\t\t      ' + 'Show help.' + '\t\t\t' + '[boolean]\n')  
}
Enter fullscreen mode Exit fullscreen mode

Call it in index.js :

if(yargs.argv._[0] == null){  
    utils.showHelp();  
    return;  
}
Enter fullscreen mode Exit fullscreen mode

5. Write a function in utils.js to show all supported languages:

module.exports = { showAll: showAll, showHelp: showHelp, parseSentence: parseSentence};  
function showAll(){  
    console.log(chalk.magenta.bold("\nLanguage Name\t\tISO-639-1 Code\n"))  
    for(let [key, value] of languages) {  
 console.log(key + "\\t\\t" + value + "\\n")  
    }  
}
let languages = new Map();
languages.set('afrikaans',      'af')  
languages.set('albanian', 'sq')  
languages.set('amharic', 'am')  
languages.set('arabic',         'ar')  
languages.set('armenian', 'hy')  
languages.set('azerbaijani', 'az')  
languages.set('basque',         'eu')  
languages.set('belarusian', 'be')  
languages.set('bengali', 'bn')  
languages.set('bosnian', 'bs')  
languages.set('bulgarian', 'bg')  
languages.set('catalan', 'ca')  
languages.set('cebuano', 'ceb')   
languages.set('chinese',        'zh')   
languages.set('corsican', 'co')  
languages.set('croatian', 'hr')  
languages.set('czech',         'cs')  
languages.set('danish',         'da')  
languages.set('dutch',         'nl')  
languages.set('english', 'en')  
languages.set('esperanto', 'eo')  
languages.set('estonian', 'et')  
languages.set('finnish', 'fi')  
languages.set('french',         'fr')  
languages.set('frisian', 'fy')  
languages.set('galician', 'gl')  
languages.set('georgian', 'ka')  
languages.set('german',         'de')  
languages.set('greek',         'el')  
languages.set('gujarati', 'gu')  
languages.set('haitian creole', 'ht')  
languages.set('hausa',         'ha')  
languages.set('hawaiian', 'haw') // (iso-639-2)  
languages.set('hebrew',         'he') //or iw  
languages.set('hindi',         'hi')  
languages.set('hmong',         'hmn') //(iso-639-2)  
languages.set('hungarian', 'hu')  
languages.set('icelandic', 'is')  
languages.set('igbo',         'ig')  
languages.set('indonesian', 'id')  
languages.set('irish',         'ga')  
languages.set('italian', 'it')  
languages.set('japanese', 'ja')  
languages.set('javanese', 'jv')  
languages.set('kannada', 'kn')  
languages.set('kazakh',         'kk')  
languages.set('khmer',         'km')  
languages.set('kinyarwanda', 'rw')  
languages.set('korean',         'ko')  
languages.set('kurdish', 'ku')  
languages.set('kyrgyz',         'ky')  
languages.set('lao',         'lo')  
languages.set('latin',         'la')  
languages.set('latvian', 'lv')  
languages.set('lithuanian', 'lt')  
languages.set('luxembourgish', 'lb')  
languages.set('macedonian', 'mk')  
languages.set('malagasy', 'mg')  
languages.set('malay',         'ms')  
languages.set('malayalam', 'ml')  
languages.set('maltese', 'mt')  
languages.set('maori',         'mi')  
languages.set('marathi', 'mr')  
languages.set('mongolian', 'mn')  
languages.set('burmese', 'my')  
languages.set('nepali',         'ne')  
languages.set('norwegian', 'no')  
languages.set('nyanja',         'ny')  
languages.set('odia',         'or')  
languages.set('pashto',         'ps')  
languages.set('persian', 'fa')  
languages.set('polish',         'pl')  
languages.set('portuguese', 'pt')  
languages.set('punjabi', 'pa')  
languages.set('romanian', 'ro')  
languages.set('russian', 'ru')  
languages.set('samoan',         'sm')  
languages.set('scots',          'gd')//gd gaelic  
languages.set('serbian', 'sr')  
languages.set('sesotho', 'st')  
languages.set('shona',         'sn')  
languages.set('sindhi',         'sd')  
languages.set('sinhalese', 'si')  
languages.set('slovak',         'sk')  
languages.set('slovenian', 'sl')  
languages.set('somali',         'so')  
languages.set('spanish', 'es')  
languages.set('sundanese', 'su')  
languages.set('swahili', 'sw')  
languages.set('swedish', 'sv')  
languages.set('tagalog', 'tl')  
languages.set('tajik',         'tg')  
languages.set('tamil',         'ta')  
languages.set('tatar',         'tt')  
languages.set('telugu',         'te')  
languages.set('thai',         'th')  
languages.set('turkish', 'tr')  
languages.set('turkmen', 'tk')  
languages.set('ukrainian', 'uk')  
languages.set('urdu',         'ur')  
languages.set('uyghur',         'ug')  
languages.set('uzbek',         'uz')  
languages.set('vietnamese', 'vi')  
languages.set('welsh',         'cy')  
languages.set('xhosa',         'xh')  
languages.set('yiddish',        'yi')  
languages.set('yoruba',         'yo')  
languages.set('zulu',    'zu')
Enter fullscreen mode Exit fullscreen mode



Here I have created a hash map to map all the language names to their ISO-639–1 code. This will serve two purposes, firstly it will help display all languages when needed, secondly, the API only takes the language code so even if the user enters the language name we can swap it with the language code before passing it to the API. Sneaky! 🤫. The swap would be in constant time since we are using a hash map.

Call the showAll() function in your index.js if the -l or -languages flag is true:

if(yargs.argv.l == true || yargs.argv.languages == true){  
    utils.showAll();  
    return;  
}
Enter fullscreen mode Exit fullscreen mode

6. Now write the function to do the dirty deed we talked about in your utils.js :

module.exports = { parseLanguage: parseLanguage, showAll: showAll, showHelp: showHelp, parseSentence: parseSent
ence };
function parseLanguage (language) {                                                                    
    if(language.length == 2){  
 return language;  
    }  
    if(languages.has(language)){  
 return languages.get(language)  
    }  
    else {  
 console.error("Language not supported!")  
 return; //returning null if the language is unsupported.  
    }  
};
Enter fullscreen mode Exit fullscreen mode

Convert the language to lower case and then call the function in index.js

if(yargs.argv._[0])  
var language = yargs.argv._[0].toLowerCase(); // stores the language.
//parsing the language specified to the ISO-639-1 code.                                                                                              
language = utils.parseLanguage(language);
Enter fullscreen mode Exit fullscreen mode

7. Now check if the sentence is empty, if not send it to the API:

Include the API at the top of your index.js :

const translate = require('[@vitalets/google-translate-api](http://twitter.com/vitalets/google-translate-api)')
;if(sentence == ""){                                                                                          
    console.error("\nThe entered sentence is like John Cena, I can't see it!\n")  
    console.log("Enter tran --help to get started.\n")  
    return;
}translate(sentence, {to: language}).then(res => {console.log("\n" + "\n" + res.text + "\n" + "\n";}).catch
(err => {                                                                                                     
     console.error(err);  
 });
Enter fullscreen mode Exit fullscreen mode

Your CLI is complete now! One thing more that you can do is to decorate the output and errors with boxes and colors, we can do that using boxen and chalk.


Beautification using Boxen and Chalk

We can use terminal colors using chalk and boxes to decorate our output using boxen.

  1. Install chalk and boxen.
npm install chalk boxen
Enter fullscreen mode Exit fullscreen mode

2. Include them in your index.js and utils.js

const chalk = require('chalk');  
const boxen = require('boxen');
Enter fullscreen mode Exit fullscreen mode

3. Add color to the usage constant.

const usage = chalk.hex('#83aaff')("\\nUsage: tran <lang\_name> sentence to be translated");
Enter fullscreen mode Exit fullscreen mode

4. Display the output using a beautiful box.

translate(sentence, {to: language}).then(res => {console.log("\n" + boxen(chalk.green("\n" + res.text + "\n"
), {padding: 1, borderColor: 'green', dimBorder: true}) + "\n");}).catch(err => {                            
     console.error(err);  
 });
Enter fullscreen mode Exit fullscreen mode

Feel free to explore both the packages and add customization to your heart’s content. :)

The CLI in all its glory:


Ahh yes



Hope you had fun learning how to build your own and fully portable CLI :) because I had a lot of fun.

Happy coding!

The complete code for this can be found at: https://github.com/RushanKhan1/termTranslate

Fork me on Github maybe :)

Connect with me on LinkedIn.

Edit: This post is now also featured on Hackernoon.

Top comments (16)

Collapse
 
crimsonmed profile image
Médéric Burlet

You should look into oclif it comes with a whole cli management api

Collapse
 
rushankhan1 profile image
Rushan Khan • Edited

Thanks for informing, I'll check it out.

Collapse
 
hedirose profile image
Hedi O

Cool, very good quick intro to yargs.
For the help function you couldve just used yarg.showHelp()
They already made that.

Collapse
 
rushankhan1 profile image
Rushan Khan • Edited

Thanks for reading! I did use the showHelp() function but that only showed the usage instructions when I used the --help flag, I wanted to show the usage too when no argument was passed hence I also made a custom usage template to print it out in the absence of an argument.

Collapse
 
uguremirmustafa profile image
uguremirmustafa

Thanks for the post! I would like to use .env variables in my cli app but when I npm link it, it cannot reach my .env files from any other directories. Do you have any advice solving this problem?

Collapse
 
rushankhan1 profile image
Rushan Khan • Edited

Thanks for reading!
Have you tried setting the exact path of your .env file in the .env config as so:

require('dotenv').config({ path: '/absolute/path/to/your/.env/file' })
Enter fullscreen mode Exit fullscreen mode
Collapse
 
dhren2019 profile image
Dhren

Wow nice post 😲

Collapse
 
rushankhan1 profile image
Rushan Khan

Thank you!

Collapse
 
mdbetancourt profile image
Michel Betancourt

hello!, i made a lib to create cli github.com/mdbetancourt/soly

Collapse
 
jupiteris profile image
Jupiter Programmer

Good Post!!!

Collapse
 
rushankhan1 profile image
Rushan Khan

Thanks for reading!

Collapse
 
cmlandaeta profile image
cmlandaeta

Ohhh q bueno...

Collapse
 
rushankhan1 profile image
Rushan Khan

Thanks!

Some comments may only be visible to logged-in visitors. Sign in to view all comments.