Creating command line tools using Node

Introduction

Hi everybody, there are a few things we need to work daily and some tiny tools help us do that job. For example, most of us have formatted our JSON files with those online formatters. There is a lot of our daily task we can automate, by creating tiny tools to help us, with Node .  Let’s see it by an example.

What Tech we will be using ?

  •  Node,  you should have it installed in your system.
  • JavaScript, You should be comfortable with it.

What we are building ?

For this example, we will be building a simple command line tool with Node that formats JSON files passed to it. Even more, it ignores non-existing and non-JSON files.

What will we learn ?

While we are working on a tiny JSON formatter, It’s not our goal to do this single thing.  Instead, we will figure about how we can use Node to create simple command line tools or utilities to automate our daily tasks. Sometimes, It takes less time than googling for a solution and digging into ton of websites, trying different things.

So, Lets jump to our steps.

Step 1. Add a shebang

Probably, our non-UNIX friends will ask, what are you talking about ? If you don’t already know, a shebang ensures that our JavaScript file is treated as an executable and node is used to execute it. Consider it a signal for our shell to invoke Node to run our script file.

Let’s create a file named jf.js and add following content in it.

#!/usr/bin/env node

That’s our shebang. Now let’s give this file executable permission.

Step 2. Require necessary modules

Node have wide range of modules and we will need two of them for our example, fs to handle file read/write and path to manipulate file paths. We can use these modules by requiring them using built-in require function of Node .

const fs = require('fs')
const path = require('path')

Step 3. Function to filter JSON files

Now, let’s write our first function for filtering JSON files.

function isJSON (file) {
  return path.extname(file) === '.json'
}

The modern JavaScript can help us in simplifying it with arrow functions.

const isJSON = file => path.extname(file) === '.json'

As we can see, we pass file name as argument and return true if and only if our file have a json extension. We used extname function from path module to check for file extension.

Step 4. Function to filter existing files

Similarly, we can write our second function for filtering files that exist. We don’t want random non-existing files passed to us.

function isJSON (file) {
  return fs.existsSync(path.join(process.cwd(), file))
}

Or, like earlier, we can use arrow functions here.

const exists = file => fs.existsSync(path.join(process.cwd(), file))

Again, we are passing file name as argument, and returning true only if that file exists. For this, we are using existsSync method from fs module to check if file exists. Further process.cwd() returns the absolute path to current directory and we are using join method from path module to join it with our file name to get absolute path to our file .

Step 5. Getting arguments and filtering files

Arguments in a node script are stored in process.argv which is an Array. furthermore, the first two arguments are —

  • Absolute path of node executable
  • and, absolute path of script file we are executing
if (process.argv.length > 2) {
  const files = process.argv.slice(2, process.argv.length)
} else {
  console.error('No argument passed')
  process.exit(1)
}

Here, we are checking whether an argument is passed or not. If anything is passed, the length of process.argvarray should be more than 2. We are removing first two arguments by slicing the array from second position to end, hence getting list of arguments or JSON filenames in this case.

As files contains file names now, we need to filter files ending with JSON extension and check whether they exist or not. Here will use filter method of Array to do this filtering.

Now, It’s time for us to use isJSON and exists functions we created earlier. We can pass a function to  filter method and that function will be applied on every element to check whether that element satisfies our function, and only then that element will be passed down in the chain.

if (process.argv.length > 2) {
  const files = process.argv
    .slice(2, process.argv.length)
    .filter(isJSON)
    .filter(exists)
} else {
  console.error('No argument passed')
  process.exit(1)
}

Our files array now contains only existing JSON files.

Step 6. Reading ugly files and writing formatted files

As we are done with parsing arguments and filtering relevant files, let’s do some real work now, and format each file we received followed by writing it back to a different file.

We can read and parse JSON file using readFileSync method from fs module and passing its output to JSON.parse method.
Assuming file is our file and is in current directory, we can write

let content = JSON.parse(fs.readFileSync(file, 'utf8'))

We can format it with indent of two spaces using JSON.stringify method. The second argument is a replacer function, we are not using that so we are passing null to it. Further, we will write the output, using writeFileSync method from fs module.

fs.writeFileSync(
  'formatted-' + path.basename(file),
  JSON.stringify(content, null, 2)
)

Note that before writing a file, we are appending formatted- before our file name.

Combining these, We can wrap entire read-format-write thing in a try-catch block, and print a nice message on error if something goes wrong.
Assuming we have a file path named file, we can write —

try {
  let content = JSON.parse(fs.readFileSync(file, 'utf8'))
  fs.writeFileSync(
    'formatted-' + path.basename(file),
    JSON.stringify(content, null, 2)
  )
} catch (err) {
  console.log('Failed to read file ' + file + '\n' + err)
  process.exit(1)
}

Our Final Program

Now, looping over our files array and utilizing code from previous steps, We can write our final program as —

Finally, we created a command line tool to format our JSON files and we can use it by passing JSON files to it.

./jf.js a.json b.json data.json

Only correct files are parsed, formatted. For example, if only data.json  exists, we will end up having a formatted-data.json  file.

Further

We used synchronous methods of files system module, so you can try building asynchronous one using callback, promises or async/await. Possibilities are unlimited. Look for your tiny problems you can solve, hunt API docs, try building tiny tools and gradually increase complexity. Share it on GitHub and If no-one cares, at least you don’t have to look again somewhere else to solve that problem.

Best of Luck.

Be the first to comment

Leave a Reply