Build a Command-Line App in Node.js with Yargs

B

Consider a circumstance where you’re building a terminal project using Node.js. One way to parse arguments would be to use the process.argv array, which is shown below:

var myArgs = process.argv.slice(2);
console.log("Your website name: ", myArgs[0]);
console.log("Your favorite number", myArgs[1]);

This will be the output:

Output of the above code

While this might work, there is a minor flaw with the code. Referring to your arguments by array index is not very clean. What if the client passes in parameters in the wrong order?

Arguments in incorrect order

This is where Yargs comes in. It allows developers to build interactive command-line tools with zero difficulties. Furthermore, the library also has support for accepting flags and building help screens.

What Yargs is capable of

Now that we have talked about its fruition, let’s get started!
In this article, you will build a program that will use Mozilla’s Readability API to fetch content from any website. The app will also be global; it can run from anywhere.

Outcome of this article —fetched content

If the user passes in the -f flag, it will download the HTML of the desired page.

Saving to file

The saved HTML will look like this:

Saved HTML

Getting Started

Project initialization
To scaffold a project with NPM, run the following bash commands:

mkdir reader 
cd reader
touch index.js
mkdir utils 
npm init -y

Installation of modules
For this project, we need the following packages:

@mozilla/readability: This package will extract the text content from the desired page.
chalk: To output colored text to the terminal.
got: To get the website’s HTML code.
dompurify and jsdom: Remove malicious JavaScript from the page’s source code. As a result, this prevents XSS attacks.
yargs: To read command-line arguments.

To acquire these dependencies, run the following terminal command:

npm i @mozilla/readability chalk got dompurify jsdom yargs

When that’s done, it’s time to code our app.

Building The Project

Showing the help screen
If the user inputs the wrong argument or uses the --help parameter, then our program should output the available options to the console.
To do so, write the following code in index.js :

const argv = require("yargs/yargs")(process.argv.slice(2))
  .option("site", {
    alias: "s",
    describe: "The website to fetch"
  })
  .option("file", {
    alias: "f",
    describe: "Save the HTML to disk"
  })
  .demandOption(["site"], "Please specify the website")
  .help().argv;

console.log(argv.site, argv.file);
  • Lines 2–4: Describe the --site option. It will have an alias of -s .
  • Lines 6–8: Describe the --file argument. Its shorter alternative will be -f .
  • Line 10: Tell Yargs the --site option will be mandatory. Without it, the program will not proceed.
  • Line 13: For debugging purposes, output the client’s arguments to the terminal.

This will be the output of the code:

Output of code

Great! Our code works. Now, let’s code our utility functions.
Parsing the website
In your utils folder, create a file called readerUtils.js . Here, start by writing the following code:

const { Readability, isProbablyReaderable } = require("@mozilla/readability");
const got = require("got");
const chalk = require("chalk");
const fs = require("fs/promises");
const { JSDOM } = require("jsdom");
const window = new JSDOM("").window;
const DOMPurify = require("dompurify")(window);
  • Line 7: Pass in our JSDOM instance to DOMPurify . We will use this to sanitize incoming HTML code.
    Next, add the following code in this file: async function parseURL(site) {
    “const response = await got(site);
    const doc = new JSDOM(DOMPurify.sanitize(response.body)); if (isProbablyReaderable(doc.window.document)) {
    let reader = new Readability(doc.window.document);
    let article = reader.parse(); return { html: article.content, title: article.title, text: article.textContent }; } else {
    return { error: “The site was not readable” };
    }
    }

Let’s deconstruct this code piece by piece.

  • Line 2: Fetch the HTML from the desired page.
  • Line 3: Sanitize this response for security reasons. This will give us clean HTML as a result.
  • Lines 5–13: If the site is readable, pass this purified HTML to the Readability API and return the result.
  • Lines 14–15: Otherwise, tell the client that an error occurred.

Saving HTML to disk
If the user uses the --file option, we want our app to download the page’s HTML.
Append the following code into your utils/readerUtils.js file:

async function saveToFile(html) {
  await fs.writeFile("./content.html", html, "utf-8");
  console.log("file saved to " + process.cwd() + "/content.html");
}
  • Line 1: Our function will take in one argument. This is the HTML of the page.
  • Line 2: Write the source code to the content.html file and save it.
  • Line 3: Finally, inform the user that the file is saved.

Sending parsed data
Now that we have parsed the data, all that remains for us is to render it to the terminal.
To do so, append this block of code in utils/readerUtils.js:

async function sendData(site, save) {
  const { html, title, text, error } = await parseSite(site);

  if (!error) {
    console.log(chalk.blue.underline(title));
    console.log(text);
    if (save) {
      saveToFile(html);
      console.log(chalk.red("contents saved to file"));
    }
  } else {
    console.log(chalk.red.bold("The site could not be read"));
  }
}
module.exports = sendData;

A few inferences from this code:

  • Line 2: Perform object destructuring to get the html , title , text, and error properties from the parseSite function.
    To learn more about object destructuring, check out this article by Jim Rottinger
  • Line 4: Check if error is undefined . In other words, see if our parsing operation has succeeded.
  • Lines 5 and 6: Display the title and text property to the terminal. We have used the chalk module to render the title in blue color.
  • Line 7: If the save boolean is true , invoke the saveToFile method and notify the user.
  • Line 12: If an error occurs, inform the client.

We’re almost done! In the next section, we will integrate our utility functions with our project.

Linking utilities with our app

Backtrack to your index.js file and write the following code:

const sendData = require("./utils/readerUtils");
//further code removed.. 
(async () => {
await sendData(argv.site, argv.file);
})();

This code means that if the user runs the index.js file, the sendData method will run.
Let’s test this out!

Output of the code

Now, check if the content.html contains the text of your chosen web page.

Saved HTML code

Great! Our code works without any flaws. In the next section, we will tell Node.js to make our app global via symlinks.
In the end, index.js should look like this:

const sendData = require("./utils/readerUtils");
const argv = require("yargs/yargs")(process.argv.slice(2))
  .option("site", {
    alias: "s",
    describe: "The website to fetch",
  })
  .option("file", {
    alias: "f",
    describe: "Save the HTML to disk",
  })
  .demandOption(["site"], "Please specify the website")
  .help().argv;

(async () => {
  await sendData(argv.site, argv.file);
})();

Creating symbolic links
As the first step, go to your package.json file and change the name field to reader . Next, add the preferGlobal and bin folder like so:

{
  "name": "reader", //name of the app
  "main": "index.js",
  "preferGlobal": true,
  "bin": "./index.js",
 //..further code
  • Line 4: Tell Node.js that we can run this program from anywhere.
  • Line 5: The bin property specifies that when the user runs the reader command, the project should run the index.js module.

When that’s done, all that’s left is to create a symlink like so. This will tell NPM that our project can run from any location.

npm link

To check if our app is now working, try running the reader command:

Output of the command

We got an error. This is because we now need to inform our operating system that this program will use Node.js as its interpreter. We can solve this by using a Shebang.
In index.js , write the following line at the beginning:

Output of the code — a webpage is fetched

And we’re done!
In the end, index.js should look like this:

#!/usr/bin/env node
const sendData = require("./utils/readerUtils");
const argv = require("yargs/yargs")(process.argv.slice(2))
  .option("site", {
    alias: "s",
    describe: "The website to fetch",
  })
  .option("file", {
    alias: "f",
    describe: "Save the HTML to disk",
  })
  .demandOption(["site"], "Please specify the website")
  .help().argv;

(async () => {
  await sendData(argv.site, argv.file);
})();

Conclusion
If you want to build powerful terminal apps with Node.js, then look no further than Yargs. It will let you get the job done with relative ease and speed.
Thank you so much for making it to the end! Happy coding!

Add comment

Tutor Network