6 var EventEmitter = require('events').EventEmitter;
7 var spawn = require('child_process').spawn;
8 var fs = require('fs');
9 var exists = fs.existsSync;
10 var path = require('path');
11 var dirname = path.dirname;
12 var basename = path.basename;
15 * Expose the root command.
18 exports = module.exports = new Command;
24 exports.Command = Command;
30 exports.Option = Option;
33 * Initialize a new `Option` with the given `flags` and `description`.
35 * @param {String} flags
36 * @param {String} description
40 function Option(flags, description) {
42 this.required = ~flags.indexOf('<');
43 this.optional = ~flags.indexOf('[');
44 this.bool = !~flags.indexOf('-no-');
45 flags = flags.split(/[ ,|]+/);
46 if (flags.length > 1 && !/^[[<]/.test(flags[1])) this.short = flags.shift();
47 this.long = flags.shift();
48 this.description = description || '';
58 Option.prototype.name = function(){
65 * Check if `arg` matches the short or long flag.
72 Option.prototype.is = function(arg){
73 return arg == this.short
78 * Initialize a new `Command`.
80 * @param {String} name
84 function Command(name) {
93 * Inherit from `EventEmitter.prototype`.
96 Command.prototype.__proto__ = EventEmitter.prototype;
101 * The `.action()` callback is invoked when the
102 * command `name` is specified via __ARGV__,
103 * and the remaining arguments are applied to the
104 * function for access.
106 * When the `name` is "*" an un-matched command
107 * will be passed as the first arg, followed by
108 * the rest of __ARGV__ remaining.
114 * .option('-C, --chdir <path>', 'change the working directory')
115 * .option('-c, --config <path>', 'set config path. defaults to ./deploy.conf')
116 * .option('-T, --no-tests', 'ignore test hook')
120 * .description('run remote setup commands')
121 * .action(function(){
122 * console.log('setup');
126 * .command('exec <cmd>')
127 * .description('run the given remote command')
128 * .action(function(cmd){
129 * console.log('exec "%s"', cmd);
134 * .description('deploy the given env')
135 * .action(function(env){
136 * console.log('deploying "%s"', env);
139 * program.parse(process.argv);
141 * @param {String} name
142 * @param {String} [desc]
143 * @return {Command} the new command
147 Command.prototype.command = function(name, desc){
148 var args = name.split(/ +/);
149 var cmd = new Command(args.shift());
150 if (desc) cmd.description(desc);
151 if (desc) this.executables = true;
152 if (desc) this._execs[cmd._name] = true;
153 this.commands.push(cmd);
154 cmd.parseExpectedArgs(args);
156 if (desc) return this;
161 * Add an implicit `help [cmd]` subcommand
162 * which invokes `--help` for the given command.
167 Command.prototype.addImplicitHelpCommand = function() {
168 this.command('help [cmd]', 'display help for [cmd]');
172 * Parse expected `args`.
174 * For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`.
176 * @param {Array} args
177 * @return {Command} for chaining
181 Command.prototype.parseExpectedArgs = function(args){
182 if (!args.length) return;
184 args.forEach(function(arg){
187 self._args.push({ required: true, name: arg.slice(1, -1) });
190 self._args.push({ required: false, name: arg.slice(1, -1) });
198 * Register callback `fn` for the command.
204 * .description('display verbose help')
205 * .action(function(){
206 * // output help here
209 * @param {Function} fn
210 * @return {Command} for chaining
214 Command.prototype.action = function(fn){
216 this.parent.on(this._name, function(args, unknown){
217 // Parse any so-far unknown options
218 unknown = unknown || [];
219 var parsed = self.parseOptions(unknown);
221 // Output help if necessary
222 outputHelpIfNecessary(self, parsed.unknown);
224 // If there are still any unknown options, then we simply
225 // die, unless someone asked for help, in which case we give it
226 // to them, and then we die.
227 if (parsed.unknown.length > 0) {
228 self.unknownOption(parsed.unknown[0]);
231 // Leftover arguments need to be pushed back. Fixes issue #56
232 if (parsed.args.length) args = parsed.args.concat(args);
234 self._args.forEach(function(arg, i){
235 if (arg.required && null == args[i]) {
236 self.missingArgument(arg.name);
240 // Always append ourselves to the end of the arguments,
241 // to make sure we match the number of arguments the user
243 if (self._args.length) {
244 args[self._args.length] = self;
249 fn.apply(this, args);
255 * Define option with `flags`, `description` and optional
258 * The `flags` string should contain both the short and long flags,
259 * separated by comma, a pipe or space. The following are all valid
260 * all will output this way when `--help` is used.
268 * // simple boolean defaulting to false
269 * program.option('-p, --pepper', 'add pepper');
275 * // simple boolean defaulting to false
276 * program.option('-C, --no-cheese', 'remove cheese');
285 * // required argument
286 * program.option('-C, --chdir <path>', 'change the working directory');
292 * // optional argument
293 * program.option('-c, --cheese [type]', 'add cheese [marble]');
295 * @param {String} flags
296 * @param {String} description
297 * @param {Function|Mixed} fn or default
298 * @param {Mixed} defaultValue
299 * @return {Command} for chaining
303 Command.prototype.option = function(flags, description, fn, defaultValue){
305 , option = new Option(flags, description)
306 , oname = option.name()
307 , name = camelcase(oname);
309 // default as 3rd arg
310 if ('function' != typeof fn) defaultValue = fn, fn = null;
312 // preassign default value only for --no-*, [optional], or <required>
313 if (false == option.bool || option.optional || option.required) {
314 // when --no-* we make sure default is true
315 if (false == option.bool) defaultValue = true;
316 // preassign only if we have a default
317 if (undefined !== defaultValue) self[name] = defaultValue;
320 // register the option
321 this.options.push(option);
323 // when it's passed assign the value
324 // and conditionally invoke the callback
325 this.on(oname, function(val){
327 if (null != val && fn) val = fn(val);
329 // unassigned or bool
330 if ('boolean' == typeof self[name] || 'undefined' == typeof self[name]) {
331 // if no value, bool true, and we have a default, then use it!
333 self[name] = option.bool
334 ? defaultValue || true
339 } else if (null !== val) {
349 * Parse `argv`, settings options and invoking commands when defined.
351 * @param {Array} argv
352 * @return {Command} for chaining
356 Command.prototype.parse = function(argv){
358 if (this.executables) this.addImplicitHelpCommand();
364 this._name = this._name || basename(argv[1]);
367 var parsed = this.parseOptions(this.normalize(argv.slice(2)));
368 var args = this.args = parsed.args;
370 var result = this.parseArgs(this.args, parsed.unknown);
372 // executable sub-commands
373 var name = result.args[0];
374 if (this._execs[name]) return this.executeSubCommand(argv, args, parsed.unknown);
380 * Execute a sub-command executable.
382 * @param {Array} argv
383 * @param {Array} args
384 * @param {Array} unknown
388 Command.prototype.executeSubCommand = function(argv, args, unknown) {
389 args = args.concat(unknown);
391 if (!args.length) this.help();
392 if ('help' == args[0] && 1 == args.length) this.help();
395 if ('help' == args[0]) {
401 var dir = dirname(argv[1]);
402 var bin = basename(argv[1]) + '-' + args[0];
404 // check for ./<bin> first
405 var local = path.join(dir, bin);
408 args = args.slice(1);
409 var proc = spawn(local, args, { stdio: 'inherit', customFds: [0, 1, 2] });
410 proc.on('error', function(err){
411 if (err.code == "ENOENT") {
412 console.error('\n %s(1) does not exist, try --help\n', bin);
413 } else if (err.code == "EACCES") {
414 console.error('\n %s(1) not executable. try chmod or run with root\n', bin);
418 this.runningCommand = proc;
422 * Normalize `args`, splitting joined short flags. For example
423 * the arg "-abc" is equivalent to "-a -b -c".
424 * This also normalizes equal sign and splits "--abc=def" into "--abc def".
426 * @param {Array} args
431 Command.prototype.normalize = function(args){
437 for (var i = 0, len = args.length; i < len; ++i) {
439 i > 0 && (lastOpt = this.optionFor(args[i-1]));
441 if (lastOpt && lastOpt.required) {
443 } else if (arg.length > 1 && '-' == arg[0] && '-' != arg[1]) {
444 arg.slice(1).split('').forEach(function(c){
447 } else if (/^--/.test(arg) && ~(index = arg.indexOf('='))) {
448 ret.push(arg.slice(0, index), arg.slice(index + 1));
458 * Parse command `args`.
460 * When listener(s) are available those
461 * callbacks are invoked, otherwise the "*"
462 * event is emitted and those actions are invoked.
464 * @param {Array} args
465 * @return {Command} for chaining
469 Command.prototype.parseArgs = function(args, unknown){
470 var cmds = this.commands
476 if (this.listeners(name).length) {
477 this.emit(args.shift(), args, unknown);
479 this.emit('*', args);
482 outputHelpIfNecessary(this, unknown);
484 // If there were no args and we have unknown options,
485 // then they are extraneous and we need to error.
486 if (unknown.length > 0) {
487 this.unknownOption(unknown[0]);
495 * Return an option matching `arg` if any.
497 * @param {String} arg
502 Command.prototype.optionFor = function(arg){
503 for (var i = 0, len = this.options.length; i < len; ++i) {
504 if (this.options[i].is(arg)) {
505 return this.options[i];
511 * Parse options from `argv` returning `argv`
512 * void of these options.
514 * @param {Array} argv
519 Command.prototype.parseOptions = function(argv){
526 var unknownOptions = [];
529 for (var i = 0; i < len; ++i) {
532 // literal args after --
543 // find matching Option
544 option = this.optionFor(arg);
549 if (option.required) {
551 if (null == arg) return this.optionMissingArgument(option);
552 this.emit(option.name(), arg);
554 } else if (option.optional) {
556 if (null == arg || ('-' == arg[0] && '-' != arg)) {
561 this.emit(option.name(), arg);
564 this.emit(option.name());
569 // looks like an option
570 if (arg.length > 1 && '-' == arg[0]) {
571 unknownOptions.push(arg);
573 // If the next argument looks like it might be
574 // an argument for this option, we pass it on.
575 // If it isn't, then it'll simply be ignored
576 if (argv[i+1] && '-' != argv[i+1][0]) {
577 unknownOptions.push(argv[++i]);
586 return { args: args, unknown: unknownOptions };
590 * Argument `name` is missing.
592 * @param {String} name
596 Command.prototype.missingArgument = function(name){
598 console.error(" error: missing required argument `%s'", name);
604 * `Option` is missing an argument, but received `flag` or nothing.
606 * @param {String} option
607 * @param {String} flag
611 Command.prototype.optionMissingArgument = function(option, flag){
614 console.error(" error: option `%s' argument missing, got `%s'", option.flags, flag);
616 console.error(" error: option `%s' argument missing", option.flags);
623 * Unknown option `flag`.
625 * @param {String} flag
629 Command.prototype.unknownOption = function(flag){
631 console.error(" error: unknown option `%s'", flag);
638 * Set the program version to `str`.
640 * This method auto-registers the "-V, --version" flag
641 * which will print the version number when passed.
643 * @param {String} str
644 * @param {String} flags
645 * @return {Command} for chaining
649 Command.prototype.version = function(str, flags){
650 if (0 == arguments.length) return this._version;
652 flags = flags || '-V, --version';
653 this.option(flags, 'output the version number');
654 this.on('version', function(){
662 * Set the description `str`.
664 * @param {String} str
665 * @return {String|Command}
669 Command.prototype.description = function(str){
670 if (0 == arguments.length) return this._description;
671 this._description = str;
676 * Set / get the command usage `str`.
678 * @param {String} str
679 * @return {String|Command}
683 Command.prototype.usage = function(str){
684 var args = this._args.map(function(arg){
686 ? '<' + arg.name + '>'
687 : '[' + arg.name + ']';
690 var usage = '[options'
691 + (this.commands.length ? '] [command' : '')
693 + (this._args.length ? ' ' + args : '');
695 if (0 == arguments.length) return this._usage || usage;
702 * Return the largest option length.
708 Command.prototype.largestOptionLength = function(){
709 return this.options.reduce(function(max, option){
710 return Math.max(max, option.flags.length);
715 * Return help for options.
721 Command.prototype.optionHelp = function(){
722 var width = this.largestOptionLength();
724 // Prepend the help information
725 return [pad('-h, --help', width) + ' ' + 'output usage information']
726 .concat(this.options.map(function(option){
727 return pad(option.flags, width)
728 + ' ' + option.description;
734 * Return command help documentation.
740 Command.prototype.commandHelp = function(){
741 if (!this.commands.length) return '';
746 , this.commands.map(function(cmd){
747 var args = cmd._args.map(function(arg){
749 ? '<' + arg.name + '>'
750 : '[' + arg.name + ']';
754 + (cmd.options.length
756 : '') + ' ' + args, 22)
758 ? ' ' + cmd.description()
760 }).join('\n').replace(/^/gm, ' ')
766 * Return program help documentation.
772 Command.prototype.helpInformation = function(){
775 , ' Usage: ' + this._name + ' ' + this.usage()
776 , '' + this.commandHelp()
779 , '' + this.optionHelp().replace(/^/gm, ' ')
786 * Output help information for this command
791 Command.prototype.outputHelp = function(){
792 process.stdout.write(this.helpInformation());
797 * Output help information and exit.
802 Command.prototype.help = function(){
808 * Camel-case the given `flag`
810 * @param {String} flag
815 function camelcase(flag) {
816 return flag.split('-').reduce(function(str, word){
817 return str + word[0].toUpperCase() + word.slice(1);
822 * Pad `str` to `width`.
824 * @param {String} str
825 * @param {Number} width
830 function pad(str, width) {
831 var len = Math.max(0, width - str.length);
832 return str + Array(len + 1).join(' ');
836 * Output help information if necessary
838 * @param {Command} command to output help for
839 * @param {Array} array of options to search for -h or --help
843 function outputHelpIfNecessary(cmd, options) {
844 options = options || [];
845 for (var i = 0; i < options.length; i++) {
846 if (options[i] == '--help' || options[i] == '-h') {