With the right packages, writing command-line apps in NodeJS is a breeze.
One package in particular makes it extremely easy: Commander.
Let’s set the stage and walk-through how to write a command-line interface (CLI) app in NodeJS with Commander. Our goal will be to write a CLI app to list files and directories.
IDEs I love online IDEs. They abstract away a lot of headaches when it comes to dev environment setup. I personally use Cloud9 for the following reasons:
Node/NPM Version At the time of writing this article, Node is at version 5.3.0 and NPM is ad version 3.3.12.
We start by initializing our project, accept all the NPM defaults, and installing the commander package:
npm init
npm i --save commander
Resulting in:
package.json
Note:
Now that we’ve initialized our project and indicated that our entry point is index.js, let’s create index.js:
touch index.js
Now, for the actual coding part:
Typically, when executing NodeJS files, we tell the system to use the appropriate interpreter by prefixing node before the file. However, we want to be able to execute our CLI app globally from anywhere in the system, and without having to specify the node interpreter every time.
Therefore, our first line is the shebang expression:
#!/usr/bin/env node
This not only tells our system to use the appropriate interpreter, but it also tells our system to use the appropriate version of the interpreter.
From here on out, we write pure JavaScript code.
Since I’ll be writing ES6-compliant code, I’ll start with the literal expression:
'use strict';
This tells the compiler to use a stricter variant of javascript [1] and enables us to write ES6 code on Cloud9.
Let’s start by requiring the commander package:
const program = require('commander');
Now, writing CLI apps with commander issimple, and the documentation is great, but I struggled with a few concepts that I will attempt to clear up here.
There seems to be 2 designs for CLI apps. Take ls and git for example.
With ls, you pass one or more flags:
With git, you pass sub-commands, but you also have some flags:
We will explore the flexibility Commander gives us to design both types of command-line interfaces.
The Commander API is straight forward and the documentation is great.
There are 3 basic ways we can write our program:
METHOD #1: Flag-only command-line application
const program = require('commander');
program
.version('0.0.1')
.option('-o, --option','option description')
.option('-m, --more','we can have as many options as we want')
.option('-i, --input [optional]','optional user input')
.option('-I, --another-input <required>','required user input')
.parse(process.argv); // end with parse to parse through the input
Note:
The example above allows us to implement a flag-only approach to our CLI app. Users will be expected to interact with our app like so:
//Examples:
$ cli-app -om -I hello
$ cli-app --option -i optionalValue -I requiredValue
METHOD #2: Sub-command and flag-based command-line application
const program = require('commander');
program
.version('0.0.1')
.command('command <req> [optional]')
.description('command description')
.option('-o, --option','we can still have add'l options')
.action(function(req,optional){
console.log('.action() allows us to implement the command');
console.log('User passed %s', req);
if (optional) {
optional.forEach(function(opt){
console.log("User passed optional arguments: %s", opt);
});
}
});
program.parse(process.argv); // notice that we have to parse in a new statement.
Note:
Users are expected to interact with our CLI app like so:
//Example:
$ cli-app command requiredValue -o
METHOD #3: Same as METHOD #2 above, but allows for modularized code
Finally, we don’t have to bloat our one JavaScript file with all the .command().description().action() logic. We can modularize our CLI project like so:
// file: ./cli-app/index.js
const program = require('commander');
program
.version('0.0.1')
.command('command <req> [optional]','command description')
.command('command2','command2 description')
.command('command3','command3 description')
.parse(process.argv);
Note:
Now that we’ve covered the basics, it’s all downhill from here. We can take a deep breath.
*** SIGH ***
All right, now the fun begins.
If we recall our project scenario, we want to write a CLI app to list files and directories. So let’s start writing the code.
We want to give the user the ability to decide if they want to see “all” files (including hidden ones) and/or if they want to see the long listing format (including the rights/permissions of the files/folders).
So, we start by writing the basic shell of our program to see our incremental progress (we will follow signature of Method #2 for the sake of the demo) :
#!/usr/bin/env node
'use strict';
const program = require('commander');
program
.version('')
.command('')
.description('')
.option('','')
.option('','')
.action('');
program.parse(process.argv);
Let’s start filling the blanks:
#!/usr/bin/env node
'use strict';
const program = require('commander');
program
.version('0.0.1')
.command('list [directory]')
.description('List files and folders')
.option('-a, --all','List all files and folders')
.option('-l, --long','')
.action();
program.parse(process.argv);
Note:
So, in our scenario, the following calls are valid:
$ cli-app list
$ cli-app list -al
$ cli-app list --all
$ cli-app list --long
$ cli-app list /home/user -al
Now, let’s start writing code for our .action().
#!/usr/bin/env node
'use strict';
const program = require('commander');
let listFunction = (directory,options) => {
//some code here
}
program
.version('0.0.1')
...
.action(listFunction);
program.parse(process.argv);
We are going to cheat here by using the built-in ls command that’s available in all unix-like operating systems.
#!/usr/bin/env node
'use strict';
const program = require('commander'),
exec = require('child_process').exec;
let listFunction = (directory,options) => {
const cmd = 'ls';
let params = [];
if (options.all) params.push('a');
if (options.long) params.push('l');
let fullCommand = params.length
? cmd + ' -' + params.join('')
: cmd
if (directory) fullCommand += ' ' + directory;
};
program
.version('0.0.1')
...
.action(listFunction);
program.parse(process.argv);
Let’s talk reason about this code.
Now, we want to execute the fully constructed command in the shell. Child_Process.exec() gives us the ability to do so and NodeJS docs give us the signature:
child_process.exec(command, callback(error, stdout, stderr){
//"error" will be returned if exec encountered an error.
//"stdout" will be returned if exec is successful and data is returned.
//"stderr" will be returned if the shell command encountered an error.
})
So, let’s use this:
#!/usr/bin/env node
'use strict';
const program = require('commander'),
exec = require('child_process').exec;
let listFunction = (directory,options) => {
const cmd = 'ls';
let params = [];
if (options.all) params.push('a');
if (options.long) params.push('l');
let fullCommand = params.length
? cmd + ' -' + params.join('')
: cmd
if (directory) fullCommand += ' ' + directory;
let execCallback = (error, stdout, stderr) => {
if (error) console.log("exec error: " + error);
if (stdout) console.log("Result: " + stdout);
if (stderr) console.log("shell error: " + stderr);
};
exec(fullCommand, execCallback);
};
program
.version('0.0.1')
...
.action(listFunction);
program.parse(process.argv);
That’s it!
Here is the gist of my sample CLI app.
Of course, we can add a few niceties, like:
#!/usr/bin/env node
'use strict';
/**
* Require dependencies
*
*/
const program = require('commander'),
chalk = require("chalk"),
exec = require('child_process').exec,
pkg = require('./package.json');
/**
* list function definition
*
*/
let list = (directory,options) => {
const cmd = 'ls';
let params = [];
if (options.all) params.push("a");
if (options.long) params.push("l");
let parameterizedCommand = params.length
? cmd + ' -' + params.join('')
: cmd ;
if (directory) parameterizedCommand += ' ' + directory ;
let output = (error, stdout, stderr) => {
if (error) console.log(chalk.red.bold.underline("exec error:") + error);
if (stdout) console.log(chalk.green.bold.underline("Result:") + stdout);
if (stderr) console.log(chalk.red("Error: ") + stderr);
};
exec(parameterizedCommand,output);
};
program
.version(pkg.version)
.command('list [directory]')
.option('-a, --all', 'List all')
.option('-l, --long','Long list format')
.action(list);
program.parse(process.argv);
// if program was called with no arguments, show help.
if (program.args.length === 0) program.help();
Finally, we can take advantage of NPM to symbolic link our CLI application so we can use it globally in our system. Simply, in the terminal, cd into the root of our CLI app and type:
npm link
The code in this project is by no means the best code. I am fully aware that there is always room for improvement, so feedback is welcome!
Also, I want to point out a security flaw in our app. Our code does not sanitize or validate the users’ input. This violates security best practices. Consider the following scenarios where users can pass un-desired input:
$ cli-app -al ; rm -rf /
$ cli-app -al ; :(){ :|: & };:
Recommended Courses:
Node.js : Rest Apis Development Using Loopback
☞ http://on.codetrick.net/BJgUpUhWgf
React - The Beginner’s Practical Guide for ES6 & React Apps
☞ http://on.codetrick.net/SJLpU3Wgf
Learn node.js from scratch
☞ http://on.codetrick.net/rkHSknZxz
Master the MEAN Stack - Learn By Example
☞ https://goo.gl/UpbUJT
Typescript 2 Masterclass: Node REST API + Angular 2 Client
☞ http://on.codetrick.net/S1SSy3beM
☞ JavaScript Programming Tutorial Full Course for Beginners
☞ Learn JavaScript - Become a Zero to Hero
☞ Javascript Project Tutorial: Budget App
☞ E-Commerce JavaScript Tutorial - Shopping Cart from Scratch
☞ Machine Learning Zero to Hero - Learn Machine Learning from scratch
☞ PHP Tutorial for Absolute Beginners - Learn PHP from Scratch -