Skip to content

Commit

Permalink
Improved support for extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
beaugunderson committed Aug 22, 2013
1 parent 2076592 commit 7f973cb
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 48 deletions.
116 changes: 72 additions & 44 deletions helmsman.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@
/*!
* Module dependencies.
*/
var fs = require('fs');
var util = require('util');
var events = require("events");
var path = require('path');
var glob = require('glob');
var colors = require('colors');
var spawn = require('child_process').spawn;
var _s = require('underscore.string');

var domain = require('domain').create();

require('colors');

/**
* Exports
*/

module.exports = exports = helmsman;
exports.Helmsman = Helmsman;

exports.Helmsman = Helmsman;

/**
* The Helmsman constructor
Expand All @@ -41,12 +44,14 @@ function Helmsman(options){
this.localDir = path.resolve(options.localDir);
}

// Guess the prefix. Assume if one isn't given and that executable doesn't equal
// Guess the prefix. Assume if one isn't given and that executable doesn't equal
// the root command filename, use the filename of the root command
if (!options.prefix && path.basename(process.argv[1]) !== path.basename(require.main.filename)) {
this.prefix = path.basename(require.main.filename)
this.prefix = path.basename(require.main.filename,
path.extname(require.main.filename));
} else {
this.prefix = options.prefix || path.basename(process.argv[1]);
this.prefix = options.prefix || path.basename(process.argv[1],
path.extname(process.argv[1]));
}

this.availableCommands = {};
Expand All @@ -58,28 +63,41 @@ function Helmsman(options){
}

// Local files in files in the /bin folder for an application
this.localFiles = glob.sync(self.prefix+"*", {cwd: self.localDir});

this.localFiles.forEach(function(file){
// Figure out the longest command name for printing --help
var commandData = require(path.join(self.localDir, file)).command;
this.localFiles = glob.sync(self.prefix + "*", { cwd: self.localDir })
.map(function (file) {
return path.join(self.localDir, file);
});

this.localFiles.forEach(function(file) {
var extension = path.extname(file);
var name = path.basename(file, extension).substr(self.prefix.length);

var commandData = require(file).command;

if (!commandData) {
util.error('The file ('+file+') did not export.command. Please ensure your commands are setup properly and your prefix is correct'.red);
process.exit(1);
}

self.availableCommands[file.substr(self.prefix.length)] = commandData;
commandData.path = file;

var fullCommand = (commandData.options) ? file + ' ' + commandData.command : file
self.availableCommands[name] = commandData;

if (fullCommand.length > self.commandMaxLength) { self.commandMaxLength = file.length; }
var fullCommand = commandData.arguments ?
name + ' ' + commandData.arguments :
name;

// Figure out the longest command name for printing --help
if (fullCommand.length > self.commandMaxLength) {
self.commandMaxLength = name.length;
}
});

self.availableCommands['help'] = { // help is always available!
// help is always available!
self.availableCommands['help'] = {
arguments: '<sub-command>',
description: 'Show the --help for a specific command'
}
};
}

util.inherits(Helmsman, events.EventEmitter);
Expand All @@ -94,7 +112,7 @@ util.inherits(Helmsman, events.EventEmitter);
*
* var cli = helmsman();
* cli.parse();
*
*
* @param {Object} options Contructor options
* @return {Helmsman} A new helmsman
*/
Expand All @@ -111,51 +129,53 @@ function helmsman(options){
* * Explicit match (status === status)
* * Shorthand (st === status)
* * Levenshtein of <=2 (sratus === status)
*
*
* @param {String} cmd The command given to the script
* @param {[String]} availableCommands An array of all the available commands
* @return {String} The actual command that will be run
*/
Helmsman.prototype.getCommand = function(cmd, availableCommands){
var self = this;

if (!availableCommands) {
availableCommands = Object.keys(self.availableCommands);
}

// If there is an exact match, return it
if (~availableCommands.indexOf(cmd)) {
return cmd;
}

// Detirmine how many commands match the iterator. Return one if command,
// Determine how many commands match the iterator. Return one if command,
function isOneOrMore(commands, iterator){
var list = commands.filter(iterator)
var list = commands.filter(iterator);

if (list.length === 1) {
return list[0];
} else if (list.length > 1) {
return new Error(util.format('There are %d potential options for "%s": %s', list.length, cmd, list));
return new Error(util.format('There are %d options for "%s": %s',
list.length, cmd, list.join(', ')));
} else {
return false;
}
}

// If there is a shorthand match, return it
var shortHandCmd = isOneOrMore(availableCommands, function(command){
var shortHandCmd = isOneOrMore(availableCommands, function(command){
return (command.indexOf(cmd) === 0);
})
});
if (shortHandCmd) { return shortHandCmd; }

// If there is a close match, return it
var similarCmd = isOneOrMore(availableCommands, function(command){
var similarCmd = isOneOrMore(availableCommands, function(command){
return (_s.levenshtein(cmd, command) <= 2);
})
});
if (similarCmd) { return similarCmd; }

// If nothing, then get outta here
return new Error(util.format('There are no commands by the name of "%s"', cmd));
}
return new Error(util.format('There are no commands by the name of "%s"',
cmd));
};

/**
* GO!
Expand All @@ -172,24 +192,29 @@ Helmsman.prototype.parse = function(argv){

// Much of the following heavily inspired or simply taken from component/bin
// https://github.com/component/component/blob/master/bin/component

// Print the modules version number
if (args[0] === '--version') {
// BOLD assumption that the file is in ./bin
var packagePath = path.join(path.dirname(require.main.filename), '..', 'package.json');
var packagePath = path.join(path.dirname(require.main.filename), '..',
'package.json');
var pkg = require(packagePath);
return console.log(pkg.name + ": " + pkg.version)
return console.log(pkg.name + ": " + pkg.version);
}

// Print the command list if --help is called
if (args[0] === '--help' || !args.length || args[0][0] === '-' || (args[0] === 'help' && args.length === 1)) {
if (args[0] === '--help' ||
!args.length ||
args[0][0] === '-' ||
(args[0] === 'help' && args.length === 1) ||
(self.getCommand(args[0]) === 'help' && args.length === 1)) {
self.emit('--help');
return self.showHelp();
}

var cmd = self.getCommand(args.shift());

if ('object' === typeof cmd && cmd.name === "Error") {
if (util.isError(cmd)) {
util.error(cmd.message.red);
self.showHelp();
process.exit(1);
Expand All @@ -201,22 +226,23 @@ Helmsman.prototype.parse = function(argv){
cmd = args.shift();
args = ['--help'];
}

var binPath = path.join(self.localDir, self.prefix + cmd);

var fullPath = self.availableCommands[cmd] &&
self.availableCommands[cmd].path;

domain.on('error', function(err) {
if (err.code === 'EACCES') {
console.error('');
console.error('Could not execute the subcommand: ' + self.prefix + cmd);
console.error('');
console.error('Consider running:\n chmod +x '+binPath);
console.error('Consider running:\n chmod +x', fullPath);
} else {
console.error(err.stack.red);
}
});

domain.run(function() {
var subcommand = spawn(binPath, args, { stdio: 'inherit' });
var subcommand = spawn(fullPath, args, { stdio: 'inherit' });

subcommand.on('close', function(code){
process.exit(code);
Expand All @@ -236,10 +262,10 @@ Helmsman.prototype.showHelp = function(){
console.log('');
console.log('Commands:');
console.log('');

for (var command in self.availableCommands) {
var fullCommand = command;
// console.log(command, self.availableCommands[command].options, "\n")

if (self.availableCommands[command].arguments) {
fullCommand += ' ' + self.availableCommands[command].arguments;
}
Expand All @@ -253,12 +279,14 @@ Helmsman.prototype.showHelp = function(){

fullCommands.forEach(function(command){
var diff = (self.commandMaxLength - command[0].length);

// Pad spaces at the end of each command so help descriptions line up
for (var i = 0; i < diff; i++) {
command[0]+=' ';
command[0] += ' ';
}

console.log(' %s %s', command[0], command[1]);
})
});

process.exit();
};
};
5 changes: 5 additions & 0 deletions test/bin/extension-test-subcommand
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

exports.command = {
description: 'A test'
};
5 changes: 5 additions & 0 deletions test/bin/testcommand-subcommandWithExtension.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

exports.command = {
description: 'A test'
};
13 changes: 13 additions & 0 deletions test/extension-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
var test = require("tap").test;

var helmsman = require('..');

var cli = helmsman({ localDir: './bin' });

test('construct an instance of a helmsman', function(t){
t.plan(3);

t.equal(cli.localDir.substr(-8), 'test/bin', 'The localDir is set');
t.equal(cli.prefix, 'extension-test-', 'The prefix is properly set');
t.equal(cli.availableCommands.subcommand.description, 'A test', 'A subcommand\'s meta data is loaded');
});
9 changes: 5 additions & 4 deletions test/local.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ var helmsman = require('..');
var cli = helmsman({ prefix: 'testcommand', localDir: './bin'});

test('construct an instance of a helmsman', function(t){
t.plan(3);
t.plan(4);

t.equal(cli.localDir.substr(-8), 'test/bin', 'The localDir is set');
t.equal(cli.prefix, 'testcommand-', 'The prefix is properly set');
t.equal(cli.availableCommands.subcommand.description, 'A test', 'A subcommand\'s meta data is loaded');
t.equal(cli.availableCommands.subcommandWithExtension.description, 'A test', 'A subcommand\'s meta data is loaded');
});

test('Guess the right command', function(t){
Expand All @@ -20,6 +21,6 @@ test('Guess the right command', function(t){
t.equal(cli.getCommand('status', availableCommands), 'status', '"status" returned status');
t.equal(cli.getCommand('st', availableCommands), 'status', '"st" returned status');
t.equal(cli.getCommand('isntall', availableCommands), 'install', '"isntall" returned install');
t.similar(cli.getCommand('in', availableCommands), {message:'There are 2 potential options for "in": install,info'})
t.similar(cli.getCommand('delete', availableCommands), {message:'There are no commands by the name of "delete"'})
})
t.similar(cli.getCommand('in', availableCommands), {message:'There are 2 options for "in": install, info'});
t.similar(cli.getCommand('delete', availableCommands), {message:'There are no commands by the name of "delete"'});
});

0 comments on commit 7f973cb

Please sign in to comment.