Conquering Commander.js
Writing large command line tools in Node
Tim Santeford
Sr. Software Engineer at AppNexus
Source Code:
Overview
- why write cli tools in Node.js?
- what is Commander.js
- how and why to use it
- project organization
- adding missing pieces
- quick demo
Why Building CLI Tools Rocks
- testing and learning APIs
- provide missing functionally while UI is in development
- automation and ETL
- tools for less technical co-workers
- curl is not practical for all users
Plus it's fun!
- great libraries available via npm
- asynchronous io
- easy installation and updates for users
- Beats shell scripts
Why write cli tools in Node.js?
It's Javascript! I know this...
brew install node
npm install <your-cli> -g
DONE!
What is Commander.js
- Command-line argument parser
- commands and options with values
- used by 1000 other npm modules
- Actively maintained
5th most depended on npm module after underscore, async, request, and lodash
Why use Commander.js?
- inspired by the Ruby gem Commander
- command routing
- clean syntax
- auto generated help
Alternatives
The Program Object
var program = require('commander');
// define program and options
...
// define commands and their options
...
// kick off the parser
program.parse(process.argv);
Defining Commands
- name with [optional] or <required> argument
- description for help
- action callback
program
.command('hello <name>')
.description('Say hello to <name>')
.action(function(name, command) {
console.log('Hello ' + name);
});
[ ] < >
Defining Options
- options can be command specific
- optional or required values
- can take a coercion function
- accessed on the program object
example up next
--option
--option [value]
--option <value>
program
.command('countdown <count>')
.description('Countdown timer')
.option(
'-i, --interval <interval>',
'The delay between ticks',
parseInterval, // coercion function
1000 // default value
).action(function(count, command) {
// option values are placed on
// the command object
command.interval
...
});
Command & Option Example
cli countdown 10 --interval 1000
- defined in package.json under "bin"
- command defined on the program object
- command argument <required>
- command option
- option value <required>
Quick Demo
--help
Usage: cli <command> [options]
Commands:
countdown [options] <count>
Count down timer
Options:
-h, --help output usage information
-V, --version output the version number
What Commander.js does not come with:
- a good way to organize large projects
- user prompting
- progress indication
- debugging
- logging
- testing
Folder Structure
├── bin
│ └── cli
├── commands
│ ├── command-a.js
│ ├── command-b.js
│ └── index.js
├── lib
│ └── ...
├── node_modules
│ └── ...
├── test
│ └── ...
├── index.js
└── package.json
- application entry point
- shell script used by npm
- commands folder
- command loader
- shared library folder
5
2
3
1
4
bin\cli
the cli will be executed with the name specified in the package.json file.
"bin": {
"<cli-name>": "./bin/<cli-name>"
}
package.json
bin/<cli-name>
#!/usr/bin/env node
require('../index');
use npm link while developing
Command Loader
module.exports = function commandLoader(program) {
var commands = {};
var loadPath = path.dirname(__filename);
// Loop though command files
fs.readdirSync(loadPath).filter(function (filename) {
return (/\.js$/.test(filename) && filename !== 'index.js');
}).forEach(function (filename) {
var name = filename.substr(0, filename.lastIndexOf('.'));
// Require command
var command = require(path.join(loadPath, filename));
// Initialize command
commands[name] = command(program);
});
return commands;
};
User Input with Prompt
- use prompt
- initialize in index.js
- place on the program object
example up next
supports password masking
program.prompt.get({
properties: {
port: {
description: 'Enter port number:',
conform: validatePort, // function
pattern: /^\d+$/
}
}
}, function (err, result) {
// Input values placed on the result object
result.port
...
});
Prompt example
Success & Error Messaging
- create common methods
- constant looking output
- logging errors is easier
- debugging is easier too (--debug)
- use color
- log to a file
program.log = function () {
program.successMessage = function () {
program.errorMessage = function () {
program.handleError = function () {
Text
Use a common Request object
Wrap request for debugging and logging.
program.request = function (opts, next) {
// Start loading indicator
if (program.debug) { // log request options
program.log(opts.uri);
return request(opts, function (err, res, body) {
// Stop loading indicator
if (err) {
if (program.debug) {
program.errorMessage(err.message);
}
return next(err, res, body);
} else {
if (program.debug) { // log response & body
return next(err, res, body);
}
});
}
Final Thoughts
Demo
Summary
- Use Commander.js
- Organize / Unit Test
- Use common logging, message / error handling, and request objects for each task
Questions?
Source Code:
Conquering Commander.js
By Tim Santeford
Conquering Commander.js
Writing large command line tools in Node
- 14,223