Writing large command line tools in Node
Tim Santeford
Sr. Software Engineer at AppNexus
Source Code:
Plus it's fun!
It's Javascript! I know this...
DONE!
5th most depended on npm module after underscore, async, request, and lodash
var program = require('commander');
// define program and options
...
// define commands and their options
...
// kick off the parser
program.parse(process.argv);
program
.command('hello <name>')
.description('Say hello to <name>')
.action(function(name, command) {
console.log('Hello ' + name);
});
[ ] < >
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
...
});
Quick Demo
Usage: cli <command> [options]
Commands:
countdown [options] <count>
Count down timer
Options:
-h, --help output usage information
-V, --version output the version number
├── bin
│ └── cli
├── commands
│ ├── command-a.js
│ ├── command-b.js
│ └── index.js
├── lib
│ └── ...
├── node_modules
│ └── ...
├── test
│ └── ...
├── index.js
└── package.json
5
2
3
1
4
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
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;
};
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
...
});
program.log = function () {
program.successMessage = function () {
program.errorMessage = function () {
program.handleError = function () {
Text
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);
}
});
}
Source Code: