Permalink
Browse files

add git-style executable subcommand support. Closes #94

  • Loading branch information...
1 parent 7ab5bd1 commit 21b41693980f91974cef19c605f91f8afa9a646b @tj committed Nov 17, 2012
Showing with 119 additions and 7 deletions.
  1. +26 −0 examples/pm
  2. +21 −0 examples/pm-install
  3. +72 −7 lib/commander.js
View
@@ -0,0 +1,26 @@
+#!/usr/bin/env node
+
+var program = require('..');
+
+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);
+
+// here .command() is invoked with a description,
+// and no .action(callback) calls to handle sub-commands.
+// this tells commander that you're going to use separate
+// executables for sub-commands, much like git(1) and other
+// popular tools.
+
+// here only ./pm-install(1) is implemented, however you
+// would define ./pm-search(1) and ./pm-list(1) etc.
+
+// Try the following:
+// ./examples/pm
+// ./examples/pm help install
+// ./examples/pm install -h
+// ./examples/pm install foo bar baz
+// ./examples/pm install foo bar baz --force
View
@@ -0,0 +1,21 @@
+#!/usr/bin/env node
+
+var program = require('..');
+
+program
+ .option('-f, --force', 'force installation')
+ .parse(process.argv);
+
+var pkgs = program.args;
+
+if (!pkgs.length) {
+ console.error('packages required');
+ process.exit(1);
+}
+
+console.log();
+if (program.force) console.log(' force: install');
+pkgs.forEach(function(pkg){
+ console.log(' install : %s', pkg);
+});
+console.log();
View
@@ -9,9 +9,13 @@
*/
var EventEmitter = require('events').EventEmitter
- , path = require('path')
+ , spawn = require('child_process').spawn
, keypress = require('keypress')
+ , fs = require('fs')
+ , exists = fs.existsSync
+ , path = require('path')
, tty = require('tty')
+ , dirname = path.dirname
, basename = path.basename;
/**
@@ -141,20 +145,35 @@ Command.prototype.__proto__ = EventEmitter.prototype;
* program.parse(process.argv);
*
* @param {String} name
+ * @param {String} [desc]
* @return {Command} the new command
* @api public
*/
-Command.prototype.command = function(name){
+Command.prototype.command = function(name, desc){
var args = name.split(/ +/);
var cmd = new Command(args.shift());
+ if (desc) cmd.description(desc);
+ if (desc) this.executables = true;
this.commands.push(cmd);
cmd.parseExpectedArgs(args);
cmd.parent = this;
+ if (desc) return this;
return cmd;
};
/**
+ * Add an implicit `help [cmd]` subcommand
+ * which invokes `--help` for the given command.
+ *
+ * @api private
+ */
+
+Command.prototype.addImplicitHelpCommand = function() {
+ this.command('help [cmd]', 'display help for [cmd]');
+};
+
+/**
* Parse expected `args`.
*
* For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`.
@@ -340,6 +359,9 @@ Command.prototype.option = function(flags, description, fn, defaultValue){
*/
Command.prototype.parse = function(argv){
+ // implicit help
+ if (this.executables) this.addImplicitHelpCommand();
+
// store raw args
this.rawArgs = argv;
@@ -348,11 +370,54 @@ Command.prototype.parse = function(argv){
// process argv
var parsed = this.parseOptions(this.normalize(argv.slice(2)));
- this.args = parsed.args;
+ var args = this.args = parsed.args;
+
+ // executable sub-commands, skip .parseArgs()
+ if (this.executables) return this.executeSubCommand(argv, args, parsed.unknown);
+
return this.parseArgs(this.args, parsed.unknown);
};
/**
+ * Execute a sub-command executable.
+ *
+ * @param {Array} argv
+ * @param {Array} args
+ * @param {Array} unknown
+ * @api private
+ */
+
+Command.prototype.executeSubCommand = function(argv, args, unknown) {
+ args = args.concat(unknown);
+
+ if (!args.length) this.help();
+ if ('help' == args[0] && 1 == args.length) this.help();
+
+ // <cmd> --help
+ if ('help' == args[0]) {
+ args[0] = args[1];
+ args[1] = '--help';
+ }
+
+ // executable
+ var dir = dirname(argv[1]);
+ var bin = basename(argv[1]) + '-' + args[0];
+
+ // check for ./<bin> first
+ var local = path.join(dir, bin);
+ if (exists(local)) bin = local;
+
+ // run it
+ args = args.slice(1);
+ var proc = spawn(bin, args, { stdio: 'inherit', customFds: [0, 1, 2] });
+ proc.on('exit', function(code){
+ if (code == 127) {
+ console.error('\n %s(1) does not exist\n', bin);
+ }
+ });
+};
+
+/**
* Normalize `args`, splitting joined short flags. For example
* the arg "-abc" is equivalent to "-a -b -c".
* This also normalizes equal sign and splits "--abc=def" into "--abc def".
@@ -679,14 +744,14 @@ Command.prototype.commandHelp = function(){
: '[' + arg.name + ']';
}).join(' ');
- return cmd._name
+ return pad(cmd._name
+ (cmd.options.length
? ' [options]'
- : '') + ' ' + args
+ : '') + ' ' + args, 18)
+ (cmd.description()
- ? '\n' + cmd.description()
+ ? ' ' + cmd.description()
: '');
- }).join('\n\n').replace(/^/gm, ' ')
+ }).join('\n').replace(/^/gm, ' ')
, ''
].join('\n');
};

0 comments on commit 21b4169

Please sign in to comment.