From 7f973cbdc15c57a9a1a8d6e4b08a4b3b1adae925 Mon Sep 17 00:00:00 2001 From: Beau Gunderson Date: Thu, 22 Aug 2013 13:47:31 -0700 Subject: [PATCH] Improved support for extensions - Fixes mattmcmanus/node-helmsman#3 - Fixes mattmcmanus/node-helmsman#4 --- helmsman.js | 116 +++++++++++------- test/bin/extension-test-subcommand | 5 + .../testcommand-subcommandWithExtension.js | 5 + test/extension-test.js | 13 ++ test/local.test.js | 9 +- 5 files changed, 100 insertions(+), 48 deletions(-) create mode 100644 test/bin/extension-test-subcommand create mode 100644 test/bin/testcommand-subcommandWithExtension.js create mode 100644 test/extension-test.js diff --git a/helmsman.js b/helmsman.js index d4728f7..744165e 100644 --- a/helmsman.js +++ b/helmsman.js @@ -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 @@ -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 = {}; @@ -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: '', description: 'Show the --help for a specific command' - } + }; } util.inherits(Helmsman, events.EventEmitter); @@ -94,7 +112,7 @@ util.inherits(Helmsman, events.EventEmitter); * * var cli = helmsman(); * cli.parse(); - * + * * @param {Object} options Contructor options * @return {Helmsman} A new helmsman */ @@ -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! @@ -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); @@ -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); @@ -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; } @@ -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(); -}; \ No newline at end of file +}; diff --git a/test/bin/extension-test-subcommand b/test/bin/extension-test-subcommand new file mode 100644 index 0000000..224cf19 --- /dev/null +++ b/test/bin/extension-test-subcommand @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +exports.command = { + description: 'A test' +}; \ No newline at end of file diff --git a/test/bin/testcommand-subcommandWithExtension.js b/test/bin/testcommand-subcommandWithExtension.js new file mode 100644 index 0000000..224cf19 --- /dev/null +++ b/test/bin/testcommand-subcommandWithExtension.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +exports.command = { + description: 'A test' +}; \ No newline at end of file diff --git a/test/extension-test.js b/test/extension-test.js new file mode 100644 index 0000000..a60711c --- /dev/null +++ b/test/extension-test.js @@ -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'); +}); diff --git a/test/local.test.js b/test/local.test.js index f611f67..82087ad 100644 --- a/test/local.test.js +++ b/test/local.test.js @@ -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){ @@ -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"'}) -}) \ No newline at end of file + 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"'}); +});