Developing command-line tools with Node.js
Node.js is popularly though of as "JavaScript on the backend", but that's a very limited and inaccurate description.
Node.js is much more than that; it is a general purpose programming platform which can be used for developing all sorts of programs - from web servers, to CMSes, to microcontroller programs, to HTTP agents, to git implementations, to binary compilers, to web proxies, to package managers, to torrent clients, to desktop apps, to mobile app scaffolders, and so on.
You will notice that many of them are are command-line tools - they are available as a command on your command line.
In the technological old days, command-line tools were most commonly written in compiled-languages like C, C++, Java etc. You could also make your shell and Python programs to work like command-line tools but it involved many tedious steps to manage them. However, there was no way you could use JavaScript to develop command-line tools.
Along came Node.js in 2009, which changed everything. Now you can use JavaScript for programming your command-line tools, and installing/uninstalling the "binary" is just a single npm
command away.
Ever wanted to create your own command-line tool? You have come to the right place!
How to write command-line tools using Node.js#
Let's start from scratch.
Create a directory named "show" and a file inside it named "index.js" with the following content:
console.log(process);
cd
to the "show" directory and execute "index.js":
$ node index.js
This will print out a lot of meta information about the Node version, setup, and the environment. It is already a command-line tool, albeit it is executed using the node
command.
An ideal command-line tool should be a standalone executable, which can be run using its own command.
In this tutorial, we will create a new command called show
, which will show numerous interesting information.
First step: initialize the directory as a npm package directory.
$ npm init --yes
This will create a file named "package.json" with the following content:
{
"name": "show",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
This file is required for installing the dependencies of the package and for configuring it as a command-line tool.
We will use the minimist package for parsing our tool's command-line options. We could create our own command-line options parser, but it would add to the noise; so let's keep it simple and just use minimist
.
Install minimist
:
$ npm i minimist
minimist
is one of the many command-line options parsers available. It is my preferred parser for small projects, for bigger ones I use commander. Then, there is yargs too, which is also pretty popular.Before we proceed any further, let's decide the interface and functionality of our tool.
It will be named show
, and it will have the following options:
-u, --user
- to display information about the current user-o, --os
- to display information about the operating system-n, --node
- to display information about the installed Node.js-V, --version
- to show the version of the tool-h, --help
- to show the usage message
If none of them are specified, the usage message will be shown.
That makes the signature of our command:
show [(-uon | V | h)]
[]
and <>
are conventions used to denote optional and mandatory prameters of a command. Prameters within []
are optional and those with <>
are mandatory. ()
and |
are used for creating mutually exclusive parameters. For more details refer to http://docopt.org/.We used -V
instead of the expected -v
, because -v
is commonly used as the short notation for --verbose
.
And here is the complete implementation of our tool:
const argv = require('minimist')(process.argv.slice(2));
const os = require('os');
const pkg = require('./package.json');
const u = argv.user || argv.u;
const o = argv.os || argv.o;
const n = argv.node || argv.n;
const V = argv.version || argv.V;
const h = argv.help || argv.h;
const usage = `
Usage: show [(-uon | V | h)]
-u, --user User details
-o, --os OS details
-n, --node Node details
-V, --version show version
-h, --help Show this screen
`;
if (h || (!u && !o && !n && !V)) {
console.log(usage);
} else if (V) {
console.log(` ${pkg.name} version ${pkg.version}`);
} else {
if (u) {
const user = os.userInfo();
const userInfo = `
Username: ${user.username}
Home directory: ${user.homedir}
UID: ${user.uid}
GID: ${user.gid}
`;
console.log(userInfo);
}
if (o) {
const osInfo = `
Architecture: ${os.arch()}
Platform: ${os.platform()}
CPU model: ${os.cpus()[0].model}
CPU count: ${os.cpus().length}
Total RAM: ${os.totalmem()} bytes
Free RAM: ${os.freemem()} bytes
Uptime: ${os.uptime()} seconds
`;
console.log(osInfo);
}
if (n) {
const nodeInfo = `
Version: ${process.versions.node}
Executable: ${process.execPath}
`;
console.log(nodeInfo);
}
}
Execute the file and check out the options in action.
$ node index.js
Usage: show [(-uon | V | h)]
-u, --user User details
-o, --os OS details
-n, --node Node details
-V, --version show version
-h, --help Show this screen
$ node index.js --os
Architecture: x64
Platform: darwin
CPU model: Intel(R) Core(TM) i5-6500 CPU @ 3.20GHz
CPU count: 4
Total RAM: 8589934592 bytes
Free RAM: 184418304 bytes
Uptime: 129198 seconds
$ node index.js -nou
Username: yaapa
Home directory: /Users/yaapa
UID: 501
GID: 20
Architecture: x64
Platform: darwin
CPU model: Intel(R) Core(TM) i5-6500 CPU @ 3.20GHz
CPU count: 4
Total RAM: 8589934592 bytes
Free RAM: 404602880 bytes
Uptime: 129304 seconds
Version: 12.4.0
Executable: /Users/yaapa/.nvm/versions/node/v12.4.0/bin/node
Ok, the implementation looks pretty neat, BUT we need a standalone command, not something which should be executed by node
.
Alrighty then!
Add the following line at the top of the "index.js" file, right at the very beginning:
#!/usr/bin/env node
With this, we are telling the operating system (OS) to use node
for executing the file. Instead of us typing node
, the OS will do it for us henceforth.
Now add this property to the object in the "package.json" file:
"bin": {
"show": "./index.js"
}
The show
property is the name of the command we are creating. Essentially, we are saying "Create a new command called show
, its implementation is in the ./index.js
file". You can give the command any name you want, but don't give it something which conflicts with an existing command (for your own good).
And, by the way, you can add as many commands you like under the bin
property. A single package can create more than one command.
The "package.json" file should look like this now:
{
"name": "show",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin": {
"show": "./index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"minimist": "^1.2.0"
}
}
And the final step: link the package π«
$ npm link
yarn
user, the equivalent command is yarn link
.You should see an output which looks something like this:
npm WARN show@1.0.0 No description
npm WARN show@1.0.0 No repository field.
audited 1 package in 0.738s
found 0 vulnerabilities
/Users/yaapa/.nvm/versions/node/v12.4.0/bin/show -> /Users/yaapa/.nvm/versions/node/v12.4.0/lib/node_modules/show/index.js
/Users/yaapa/.nvm/versions/node/v12.4.0/lib/node_modules/show -> /Users/yaapa/projects/hacksparrow.com/hacks/show
This is the magical step.
It does all the things (setting file permissions, creating symlinks) that are required to make a JavaScript file work like a compiled binary located in one of the directories of the PATH
environment variable.
You have created a command-line tool using Node.js. Check it out:
$ show --help
Usage: info [(-uon | V | h)]
-u, --user User details
-o, --os OS details
-n, --node Node details
-V, --version info version
-h, --help Show this screen
Dayum! Isn't that neat?
Anytime you want to uninstall this tool, just unlink it:
$ npm unlink
removed 1 package in 0.063s
What if you want others to use this amazing tool you have created? They can't be cloning your repository and doing npm link
, right?
If you want to become a global command-line tool celebrity, publish your tool on npm. Once published, others can install your command-line interface (CLI) tool globally or use it using npx.
Now that you are an expert in creating command-line tools using Node, here are some packages you may find useful in your next project:
- commander - command-line options parser (my choice)
- yargs - another command-line options parser
- chalk - add colors to printed text
- prompt - prompter
- inquirer - another prompter πΊπΈ
- enquirer - another prompter π¬π§
- ora - spinner
- listr - another spinner
With Node's rich API there are many incredible things you can do. I hope to be using one of your tools some day. All the best!
Summary#
In this tutorial I showed how to turn a regular Node.js program into a "feature-rich" command-line tool. The magic lies in the shebang interpreter directive (#!/usr/bin/env node
), the bin
property of the "package.json" file, and the npm link
command.