Skip to Content
Course content

147: Building a Basic CLI Tool with Node

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When I first started building tools for my own workflow, I fell into a trap that I see a lot of developers fall into today: I thought that to build a "real" CLI tool, I had to start by installing a massive library like Commander or Yargs. I figured that since these tools are industry standard, the "correct" way to handle command-line arguments was through a framework.

Thinking you need a framework for basic arguments

Here is why that mindset is a mistake. If you're building a tool that just takes one or two arguments—like a filename or a search term—adding a dependency just to parse a string is overkill. It bloats your project and adds a layer of abstraction over something Node.js already gives you for free.

Take a look at this. If you run a script like node count-words.js my-file.txt, Node doesn't just ignore my-file.txt. It stores it in a built-in array. If you console.log(process.argv), you'll see something like this:

[
  '/usr/bin/node', 
  '/Users/me/projects/count-words.js', 
  'my-file.txt'
]

The first two elements are always the path to the Node executable and the path to the script being run. Everything after that is your actual user input. You don't need a framework to access process.argv[2]. You just need a basic understanding of how Node handles the process.

Using process.argv and the magic shebang line

Now, typing node script.js every single time is tedious. You want to be able to just type count-words my-file.txt from anywhere in your terminal. To do this, you need to tell the operating system that this file should be executed using Node, regardless of the file extension.

This is where the "shebang" comes in. By adding #!/usr/bin/env node to the very first line of your file, you're giving the shell a hint. I've seen people forget this and spend an hour wondering why their script works with node ... but fails with a "command not found" or "permission denied" error when run directly.

#!/usr/bin/env node

const fs = require('fs');

const filePath = process.argv[2];

if (!filePath) {
  console.error('Please provide a file path!');
  process.exit(1);
}

try {
  const content = fs.readFileSync(filePath, 'utf8');
  const words = content.trim().split(/\s+/).length;
  console.log(`Word count: ${words}`);
} catch (err) {
  console.error('Could not read file:', err.message);
  process.exit(1);
}

Turning a script into a global command via package.json

The final piece of the puzzle isn't in the code, but in the configuration. To make your tool available globally on your machine, you use the bin field in your package.json. This creates a symbolic link from your script to a folder that's already in your system's PATH.

Inside your package.json, add a mapping where the key is the command name you want to use and the value is the path to your script:

{
  "name": "word-count-cli",
  "version": "1.0.0",
  "bin": {
    "count-words": "./count-words.js"
  }
}

After saving this, run npm link in your terminal. This is a developer's secret weapon. It tells npm to link the current package globally. Now, you can navigate to any folder on your hard drive, type count-words some-text-file.txt, and it will just work. It feels like a native system tool, but it's just a few lines of JavaScript and a clever symlink.




📋 Practical Task

Build a 'Log-Keyword-Filter' CLI Tool

Your task is to build a CLI tool that reads a log file and prints only the lines that contain a specific keyword (e.g., "ERROR" or "WARN").

  • Requirement 1: The tool must take two arguments: the path to a text file and the keyword to search for.
  • Requirement 2: Include the shebang line at the top of your script.
  • Requirement 3: If the user forgets to provide either the file path or the keyword, the tool should print a helpful usage message and exit with a non-zero status code.
  • Requirement 4: Configure the bin field in package.json so the tool can be run as log-filter.

Test your tool: Create a dummy app.log file with several lines of text, then run log-filter app.log ERROR to ensure only the error lines are displayed.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.