Writing Node.js Command Line Tools
Glen Arrowsmith Mar 2015 twitter.com/garrows
Why?
- Cross Platform
- NPM
- JavaScript
- 'nuff said
LookoutWorld
#!/usr/bin/env node
console.log('Lookout World');
mkdir lookout-world
cd lookout-world
npm init --yes
vim index.js
node ./index.js
chmod 700 index.js
./index.js
package.json
{
"name": "lookout-world",
"...":"...",
"bin": {
"lookout-world": "index.js",
"low": "index.js"
},
"preferGlobal": "true"
}
lookout-world
npm link
low
npm publish
npm install -g lookout-world
Test Everything Always!
npm install --save-dev mocha should
vim test.js
var child_process = require('child_process'),
should = require('should');
describe('Lookout World', function() {
describe('printing', function() {
it('should say "Lookout World" and not error', function(done) {
var program = child_process.spawn('./index.js', []),
gotOutput = false;
program.stdout.on('data', function(data) {
data.toString().should.eql('Lookout World\n');
gotOutput = true;
});
program.on('exit', function(exitCode) {
exitCode.should.eql(0);
gotOutput.should.eql(true);
done();
});
program.on('error', function(err) {
should(err).not.exist;
});
});
});
});
package.json
{
"name": "lookout-world",
"...": "...",
"scripts": {
"test": "mocha test.js"
},
"devDependencies": {
"mocha": "^2.1.0",
"should": "^5.0.1"
}
}
npm test
Argument Parsing
The hard way
console.log(process.argv);
lookout-world BrisJS
[ 'node', '/usr/local/bin/low', 'BrisJS' ]
#!/usr/bin/env node
var subject = 'World';
if (process.argv.length == 3) {
subject = process.argv[2];
}
console.log('Lookout ' + subject);
Test All The Things
it('should say "Lookout CampJS" and not error', function(done) {
var program = child_process.spawn('./index.js', ['CampJS']);
program.stdout.on('data', function(data) {
data.toString().should.eql('Lookout CampJS\n');
});
program.on('exit', function(exitCode) {
exitCode.should.eql(0);
done();
});
program.on('error', function(err) {
should(err).not.exist;
});
});
Problems
lookout-world --help
lookout-world --bold BrisJS
lookout-world BrisJS --bold
lookout-world -b BrisJS
lookout-world --color blue BrisJS --bold World Internet
Argument Parsing
The easy way
npm install --save commander
#!/usr/bin/env node
var program = require('commander'),
clc = require('cli-color'),
message,
formatter;
program
.version('1.0.0')
.usage('[options] <name ...>')
.option('-b, --bold', 'Bold the name')
.option('-c, --color <color>', 'Change the name to <color>', 'white')
.parse(process.argv);
if (program.args.length === 0) {
program.args.push('World');
}
for (var i = 0; i < program.args.length; i++) {
formatter = clc[program.color];
if (program.bold) {
formatter = formatter.bold;
}
console.log(formatter('Lookout ' + program.args[i]));
}
Built In Flags
Built In Sanity Checks
Usage
Git Style Sub Commands
program
.version('0.0.1')
.command('install [name]', 'install one or more packages')
.command('search [query]', 'search with optional query')
.command('list', 'list packages installed')
.parse(process.argv);
Looks for `scriptBasename-subcommand.js`
e.g. index-install.js
Streaming Input
//[SNIP]
program
.version('1.0.0')
.usage('[options] <name ...>')
.option('-b, --bold', 'Bold the name')
.option('-c, --color <color>', 'Change the name to <color>', 'white')
.parse(process.argv);
var outputter = function(name) {
formatter = clc[program.color];
if (program.bold) {
formatter = formatter.bold;
}
console.log(formatter('Lookout ' + name));
};
console.log('Enter names. Ctrl+d to end.');
process.stdin.setEncoding('utf8');
process.stdin.on('data', outputter);
var exiting = function() {
console.log('Bytes Read', process.stdin.bytesRead);
};
process.stdin.on('end', exiting);
process.on('SIGINT', function() {
process.stdin.end();
exiting();
});
Usage
Usage - Piping
Prompting
// [SNIP]
var outputter = function(name) {
formatter = clc[program.color];
if (program.bold) {
formatter = formatter.bold;
}
console.log(formatter('Lookout ' + name));
};
if (program.args.length === 0) {
var rl = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('What is your name? ', function(answer) {
outputter(answer);
rl.close();
});
}
for (var i = 0; i < program.args.length; i++) {
outputter(program.args[i]);
}
Testing Inputs
var clc = require('cli-color');
// [SNIP]
it('should prompt for a name and print it', function(done) {
var program = child_process.spawn('./index.js', []);
var askingQuestion = true;
program.stdout.on('data', function(data) {
if (askingQuestion) {
data.toString().should.eql('What is your name? ');
program.stdin.write('CampJS\n');
program.stdin.end();
askingQuestion = false;
} else {
data.toString().should.eql(clc.white('Lookout CampJS') + '\n');
}
});
program.on('exit', function(exitCode) {
exitCode.should.eql(0);
done();
});
program.on('error', function(err) {
done(err);
});
});
// [SNIP]
Keypresses
var keypress = require('keypress');
// make `process.stdin` begin emitting "keypress" events
keypress(process.stdin);
// listen for the "keypress" event
process.stdin.on('keypress', function (ch, key) {
console.log('got "keypress"', key);
if (key && key.ctrl && key.name == 'c') {
process.stdin.pause();
}
});
process.stdin.setRawMode(true);
process.stdin.resume();
Window Size
process.stdout.on('resize', function() {
console.log('screen size has changed!');
console.log(process.stdout.columns + 'x' + process.stdout.rows);
});
Spinner
ar Spinner = require('cli-spinner').Spinner;
var spinner = new Spinner('processing.. %s');
spinner.setSpinnerString('|/-\\');
spinner.start();
setTimeout(function() {
spinner.stop(true);
}, 2000);
How does the magic happen?
- ANSI escape codes
- Character literals
- ASCII control characters
Beep
console.log("\007"); // Beep
Colors
process.stdout.write('\x1b[31m'); // Red foreground
console.log('Red foreground');
process.stdout.write('\x1b[42m'); // Green background
console.log('Green background');
process.stdout.write('\x1b[39;49m'); // Reset colors
console.log('Reset');
but use cli-color instead
Roll your own spinner
var i = 0;
var readline = require('readline');
setInterval(function() {
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
process.stdout.write(i.toString());
i++;
}, 100);
var i = 0;
setInterval(function() {
process.stdout.write(i + '\r');
i++;
}, 100);
Have some fun
#!/usr/bin/env node
var keypress = require('keypress'),
readline = require('readline');
console.log('\033[2J'); // Clear the screen
keypress(process.stdin);
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.on('keypress', function(ch, key) {
switch (key.name) {
case 'up':
readline.moveCursor(process.stdout, -1, -1);
process.stdout.write('🍪');
break;
case 'down':
readline.moveCursor(process.stdout, -1, 1);
process.stdout.write('🍪');
break;
case 'left':
readline.moveCursor(process.stdout, -3, 0);
process.stdout.write('🍪');
break;
case 'right':
readline.moveCursor(process.stdout, 1, 0);
process.stdout.write('🍪');
break;
}
if (key && key.ctrl && key.name == 'c') {
process.stdin.pause();
}
});
Questions?
Writing Node.js Command Line Tools
By Glen Arrowsmith
Writing Node.js Command Line Tools
- 3,164